client/src/MobicoopBundle/Api/Service/DataProvider.php
<?php
/**
* Copyright (c) 2018, MOBICOOP. All rights reserved.
* This project is dual licensed under AGPL and proprietary licence.
***************************
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <gnu.org/licenses>.
***************************
* Licence MOBICOOP described in the file
* LICENSE
*/
namespace Mobicoop\Bundle\MobicoopBundle\Api\Service;
use Doctrine\Common\Annotations\AnnotationReader;
use Doctrine\Inflector\InflectorFactory;
use GuzzleHttp\Client;
use GuzzleHttp\Exception\ClientException;
use GuzzleHttp\Exception\ServerException;
use GuzzleHttp\Exception\TransferException;
use GuzzleHttp\RequestOptions;
use Mobicoop\Bundle\MobicoopBundle\Api\Entity\JwtToken;
use Mobicoop\Bundle\MobicoopBundle\Api\Entity\ResourceInterface;
use Mobicoop\Bundle\MobicoopBundle\Api\Entity\Response;
use Mobicoop\Bundle\MobicoopBundle\Api\Exception\ApiTokenException;
use Mobicoop\Bundle\MobicoopBundle\JsonLD\Entity\Hydra;
use Mobicoop\Bundle\MobicoopBundle\JsonLD\Entity\HydraView;
use Mobicoop\Bundle\MobicoopBundle\JsonLD\Entity\Trace;
use Symfony\Component\Cache\Adapter\FilesystemAdapter;
use Symfony\Component\HttpFoundation\File\UploadedFile;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpFoundation\Session\SessionInterface;
use Symfony\Component\Serializer\Encoder\JsonEncoder;
use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactory;
use Symfony\Component\Serializer\Mapping\Loader\AnnotationLoader;
use Symfony\Component\Serializer\Normalizer\DateTimeNormalizer;
use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
use Symfony\Component\Serializer\Serializer;
/**
* Data provider service.
* Uses an API to retrieve/send data.
*
* @author Sylvain Briat <sylvain.briat@covivo.eu>
*/
class DataProvider
{
public const SERIALIZER_ENCODER = 'json';
// possible file properties and associated getter, used for multipart/form-data
public const FILE_PROPERTIES = [
'eventFile' => 'getEventFile',
'userFile' => 'getUserFile',
'communityFile' => 'getCommunityFile',
'relayPointFile' => 'getRelayPointFile',
'relayPointTypeFile' => 'getRelayPointTypeFile',
'file' => 'getFile',
'file2' => 'getFile2',
];
// original name property for file-based entities
public const FILE_ORIGINAL_NAME_PROPERTY = 'originalName';
// accepted return format
public const RETURN_OBJECT = 1;
public const RETURN_ARRAY = 2;
public const RETURN_JSON = 3;
public const RETURN_LDJSON = 4;
private const MOB_CONNECT_CEE_LOGIN = '1';
private $uri;
private $username;
private $usernameDelegate;
private $password;
private $emailToken;
private $passwordToken;
private $ssoId;
private $ssoProvider;
private $authPath;
private $loginPath;
private $refreshPath;
private $loginTokenPath;
private $loginSsoPath;
private $tokenId;
private $authLoginPath;
private $session;
private $baseSiteUri;
/**
* @var JwtToken
*/
private $jwtToken;
private $refreshToken;
private $client;
private $resource;
private $class;
private $serializer;
private $deserializer;
private $format;
private $cache;
private $inflector;
private $request;
/**
* Constructor.
*
* @param string $uri The api uri
* @param string $username The default api username
* @param string $password The default api password
* @param string $emailToken The email token for authentification
* @param string $passwordToken The reset password token for authentification
* @param string $authPath The api path for default authentication
* @param string $loginPath The api path for user authentication
* @param string $loginDelegatePath The api path for user delegation authentication
* @param string $loginTokenPath The api path for user authentication with only validate token
* @param string $loginSsoPath The api path for user authentication by sso
* @param string $tokenId The token id
* @param Deserializer $deserializer The deserializer
* @param SessionInterface $session The session
*/
public function __construct(string $uri, string $username, ?string $emailToken = null, ?string $passwordToken = null, string $password, string $authPath, string $loginPath, string $loginDelegatePath, string $refreshPath, string $loginTokenPath, string $loginSsoPath, string $tokenId, Deserializer $deserializer, SessionInterface $session, RequestStack $requestStack)
{
$this->uri = $uri;
$this->username = $username;
$this->password = $password;
$this->emailToken = $emailToken;
$this->passwordToken = $passwordToken;
$this->authPath = $authPath;
$this->loginPath = $loginPath;
$this->loginDelegatePath = $loginDelegatePath;
$this->refreshPath = $refreshPath;
$this->loginTokenPath = $loginTokenPath;
$this->loginSsoPath = $loginSsoPath;
$this->tokenId = $tokenId;
$this->authLoginPath = $authPath;
$this->session = $session;
$this->private = false;
$this->cache = new FilesystemAdapter();
$this->inflector = InflectorFactory::create()->build();
$this->request = $requestStack->getCurrentRequest();
// use the following for debugging token related problems !
// $this->cache->deleteItem($this->tokenId.'.jwt.token');
// $this->session->remove('apiToken');
// $this->session->remove('apiRefreshToken');
// check an existing jwt token
if ($apiToken = $this->session->get('apiToken')) {
// there's an api token in session => private connection, it's a real human user
if ($apiToken->isValid()) {
// the token is still valid, we use it !
$this->jwtToken = $apiToken;
$this->private = true;
} else {
// the token is invalid, we remove it from session
$this->session->remove('apiToken');
// is there a refresh token ?
if ($refreshToken = $this->session->get('apiRefreshToken')) {
// there's a refresh token in session
$this->refreshToken = $refreshToken;
$this->private = true;
}
}
} else {
// check if there's a global api token in system cache => public connection (app)
$cachedToken = $this->cache->getItem($this->tokenId.'.jwt.token');
if ($cachedToken->isHit()) {
/**
* @var JwtToken $jwtToken
*/
$jwtToken = $cachedToken->get();
if ($jwtToken && $jwtToken->isValid()) {
$this->jwtToken = $jwtToken;
} else {
// clear cache
$this->cache->deleteItem($this->tokenId.'.jwt.token');
}
}
// check if there's a global api refresh token in system cache
$cachedRefreshToken = $this->cache->getItem($this->tokenId.'.jwt.refresh.token');
if ($cachedRefreshToken->isHit()) {
$this->refreshToken = $cachedRefreshToken->get();
}
}
$classMetadataFactory = new ClassMetadataFactory(new AnnotationLoader(new AnnotationReader()));
$encoders = [new JsonEncoder()];
// we use our custom Object Normalizer to remove unwanted null values from the json
$normalizers = [new DateTimeNormalizer(), new RemoveNullObjectNormalizer($classMetadataFactory)];
$this->serializer = new Serializer($normalizers, $encoders);
$this->deserializer = $deserializer;
$this->format = self::RETURN_OBJECT;
$this->client = new Client(['base_uri' => $this->uri, 'verify' => false]);
}
/**
* Set the username (for user authentication).
*
* @param string $username The username
*/
public function setUsername(string $username)
{
$this->username = $username;
}
/**
* Set the delegate username (for delegate user authentication).
*/
public function setUsernameDelegate(string $usernameDelegate)
{
$this->usernameDelegate = $usernameDelegate;
}
/**
* Set the email token (for user authentication with email token).
*
* @param string $emailToken The token
*/
public function setEmailToken(string $emailToken)
{
$this->emailToken = $emailToken;
}
/**
* Set the reset password token (for user authentication with reset password token).
*
* @param string $passwordToken The token
*/
public function setPasswordToken(string $passwordToken)
{
$this->passwordToken = $passwordToken;
}
public function setSsoId(string $ssoId)
{
$this->ssoId = $ssoId;
}
public function setSsoProvider(string $ssoProvider)
{
$this->ssoProvider = $ssoProvider;
}
public function setBaseSiteUri(string $baseSiteUri)
{
$this->baseSiteUri = $baseSiteUri;
}
/**
* Set the password (for user authentication).
*
* @param string $password The password
*/
public function setPassword($password)
{
$this->password = $password;
}
/**
* Set the authentication to private (for user authentication : change the api login path and the token storage system).
*
* @param bool $private True to set to private
*/
public function setPrivate(bool $private)
{
$this->private = $private;
if ($private) {
if ($this->emailToken || $this->passwordToken) {
$this->authLoginPath = $this->loginTokenPath;
} elseif ($this->ssoId && $this->ssoProvider) {
$this->authLoginPath = $this->loginSsoPath;
} elseif ($this->usernameDelegate) {
$this->authLoginPath = $this->loginDelegatePath;
} else {
$this->authLoginPath = $this->loginPath;
}
$this->jwtToken = null;
$this->refreshToken = null;
} else {
$this->authLoginPath = $this->authPath;
}
}
/**
* Get a valid token, or create one if it's not valid anymore.
*/
public function createToken()
{
if (is_null($this->jwtToken)) {
$tokens = $this->getJwtToken();
if (is_null($tokens) || !is_array($tokens)) {
throw new ApiTokenException('Bad credentials');
}
if (!isset($tokens['token']) || !isset($tokens['refreshToken'])) {
throw new ApiTokenException('Empty API or refresh token.');
}
$expiration = null;
// decode token to get the expiration
if (
3 === count($jwtParts = explode('.', $tokens['token']))
&& is_array($payload = json_decode(base64_decode($jwtParts[1]), true))
// https://tools.ietf.org/html/rfc7519.html#section-4.1.4
&& array_key_exists('exp', $payload)
) {
// Manually process the payload part to avoid having to drag in a new library
$expiration = new \DateTime('@'.$payload['exp'], new \DateTimeZone('UTC'));
}
$this->jwtToken = new JwtToken($tokens['token'], $expiration);
$this->refreshToken = $tokens['refreshToken'];
if ($this->private) {
// private request, store in session
$this->session->set('apiToken', $this->jwtToken);
$this->session->set('apiRefreshToken', $this->refreshToken);
if (isset($tokens['delegate-authentication']) && true === $tokens['delegate-authentication']) {
$this->session->set('delegate-authentication', true);
}
if (isset($tokens['logoutUrl']) && '' !== $tokens['logoutUrl']) {
$this->session->set('logoutUrl', $tokens['logoutUrl']);
}
} else {
// public request, store in system cache
$cachedToken = $this->cache->getItem($this->tokenId.'.jwt.token');
$cachedToken->set($this->jwtToken);
$cachedRefreshToken = $this->cache->getItem($this->tokenId.'.jwt.refresh.token');
$cachedRefreshToken->set($this->refreshToken);
$this->cache->save($cachedToken);
$this->cache->save($cachedRefreshToken);
}
}
}
/**
* @param string $class The name of the class
* @param null|string $resource The resource name if different than the pluralized class name
*
* @throws \ReflectionException
*/
public function setClass(string $class, $resource = null)
{
$this->class = $class;
if (null != $resource) {
$this->resource = $resource;
} else {
$this->resource = $this->pluralize((new \ReflectionClass($class))->getShortName());
}
}
/**
* Set format.
*/
public function setFormat(int $format)
{
$this->format = $format;
}
/**
* Get item operation.
*
* @param int $id The id of the item
* @param null|array $params An array of parameters
*
* @return Response the response of the operation
*/
public function getItem(int $id, ?array $params = null): Response
{
/*
* deserialization of nested array of objects doesn't work...
* only the root object deserialization works...
* see https://medium.com/@rebolon/the-symfony-serializer-a-great-but-complex-component-fbc09baa65a0
*/
// return $this->serializer->deserialize((string) $response->getBody(), $this->class, self::SERIALIZER_ENCODER);
try {
if (self::RETURN_ARRAY == $this->format) {
$headers = $this->getHeaders();
$clientResponse = $this->client->get($this->resource.'/'.$id, ['query' => $params, 'headers' => $headers]);
$value = json_decode((string) $clientResponse->getBody(), true);
} elseif (self::RETURN_JSON == $this->format) {
$headers = $this->getHeaders(['json']);
$clientResponse = $this->client->get($this->resource.'/'.$id, ['query' => $params, 'headers' => $headers]);
$value = (string) $clientResponse->getBody();
} else {
$headers = $this->getHeaders();
$clientResponse = $this->client->get($this->resource.'/'.$id, ['query' => $params, 'headers' => $headers]);
$value = $this->deserializer->deserialize($this->class, json_decode((string) $clientResponse->getBody(), true));
}
if (200 == $clientResponse->getStatusCode()) {
return new Response($clientResponse->getStatusCode(), $value);
}
} catch (ClientException|ServerException $e) {
return new Response($e->getCode(), $this->treatHydraCollection($e->getResponse()->getBody()->getContents(), true));
} catch (TransferException $e) {
return new Response($e->getCode());
}
return new Response();
}
/**
* Get special item operation.
*
* @param mixed $id The id of the item (usually an int, can be a string in rare cases !)
* @param string $operation The name of the special operation
* @param null|array $params An array of parameters
* @param bool $reverseOperationId if true Generate an alternate uri /resource/operation/id
*
* @return Response the response of the operation
*/
public function getSpecialItem($id, string $operation, ?array $params = null, bool $reverseOperationId = false): Response
{
try {
if (self::RETURN_ARRAY == $this->format) {
$headers = $this->getHeaders();
$clientResponse = $this->client->get($this->resource.'/'.$id.'/'.$operation, ['query' => $params, 'headers' => $headers]);
$value = json_decode((string) $clientResponse->getBody(), true);
} elseif (self::RETURN_JSON == $this->format) {
$headers = $this->getHeaders(['json']);
$clientResponse = $this->client->get($this->resource.'/'.$id.'/'.$operation, ['query' => $params, 'headers' => $headers]);
$value = (string) $clientResponse->getBody();
} else {
$headers = $this->getHeaders();
if (!$reverseOperationId) {
$clientResponse = $this->client->get($this->resource.'/'.$id.'/'.$operation, ['query' => $params, 'headers' => $headers]);
} else {
$clientResponse = $this->client->get($this->resource.'/'.$operation.'/'.$id, ['query' => $params, 'headers' => $headers]);
}
$value = $this->deserializer->deserialize($this->class, json_decode((string) $clientResponse->getBody(), true));
}
if (200 == $clientResponse->getStatusCode()) {
return new Response($clientResponse->getStatusCode(), $value);
}
} catch (ClientException|ServerException $e) {
return new Response($e->getCode(), $this->treatHydraCollection($e->getResponse()->getBody()->getContents(), true));
} catch (TransferException $e) {
return new Response($e->getCode());
}
return new Response();
}
/**
* Get collection operation.
*
* @param null|array $params An array of parameters
*
* @return Response the response of the operation
*/
public function getCollection(?array $params = null): Response
{
try {
if (self::RETURN_JSON == $this->format) {
$headers = $this->getHeaders(['json']);
// var_dump($this->resource, ['query' => $params, 'headers' => $headers]); exit;
$clientResponse = $this->client->get($this->resource, ['query' => $params, 'headers' => $headers]);
} else {
$headers = $this->getHeaders();
// var_dump($this->resource, ['query'=>$params, 'headers' => $headers]);die;
$clientResponse = $this->client->get($this->resource, ['query' => $params, 'headers' => $headers]);
}
if (200 == $clientResponse->getStatusCode()) {
return new Response($clientResponse->getStatusCode(), $this->treatHydraCollection($clientResponse->getBody()));
}
} catch (ClientException|ServerException $e) {
return new Response($e->getCode(), $this->treatHydraCollection($e->getResponse()->getBody()->getContents(), true));
} catch (TransferException $e) {
return new Response($e->getCode());
}
return new Response();
}
/**
* Get special collection operation.
*
* @param string $operation The name of the special operation
* @param null|array $params An array of parameters
*
* @return Response the response of the operation
*/
public function getSpecialCollection(string $operation, ?array $params = null): Response
{
try {
if (self::RETURN_JSON == $this->format) {
$headers = $this->getHeaders(['json']);
$clientResponse = $this->client->get($this->resource.'/'.$operation, ['query' => $params, 'headers' => $headers]);
} else {
// var_dump($this->resource.'/'.$operation, ['query'=>$params]);
$headers = $this->getHeaders();
$delegateAuthentication = $this->session->get('delegate-authentication');
if (true === $delegateAuthentication) {
if (is_null($params)) {
$params = [
'delegate-authentication' => true,
];
} else {
$params['delegate-authentication'] = true;
}
}
if ('bad-credentials-api' == $headers) {
return new Response(401, 'bad-credentials-api');
}
$clientResponse = $this->client->get($this->resource.'/'.$operation, ['query' => $params, 'headers' => $headers]);
}
if (200 == $clientResponse->getStatusCode()) {
return new Response($clientResponse->getStatusCode(), $this->treatHydraCollection($clientResponse->getBody()));
}
} catch (ClientException|ServerException $e) {
return new Response($e->getCode(), $this->treatHydraCollection($e->getResponse()->getBody()->getContents(), true));
} catch (TransferException $e) {
return new Response($e->getCode());
}
return new Response();
}
/**
* Get sub collection operation.
*
* @param int $id The id of the item
* @param string $subClassName The classname of the subresource
* @param string $subClassRoute The class route of the subresource (used for custom routes, if not provided the route will be the subClassName pluralized)
* @param null|array $params An array of parameters
*
* @return Response the response of the operation
*
* @throws \ReflectionException
*/
public function getSubCollection(int $id, string $subClassName, ?string $subClassRoute = null, ?array $params = null): Response
{
$route = $subClassRoute;
if (is_null($route)) {
$route = $this->pluralize((new \ReflectionClass($subClassName))->getShortName());
}
try {
if (self::RETURN_JSON == $this->format) {
$headers = $this->getHeaders(['json']);
// var_dump($this->resource.'/'.$id.'/'.$route, ['query'=>$params, 'headers' => $headers]);die;
$clientResponse = $this->client->get($this->resource.'/'.$id.'/'.$route, ['query' => $params, 'headers' => $headers]);
} elseif (self::RETURN_LDJSON == $this->format) {
$headers = $this->getHeaders(['ld+json']);
// var_dump($this->resource.'/'.$id.'/'.$route, ['query'=>$params, 'headers' => $headers]);die;
$clientResponse = $this->client->get($this->resource.'/'.$id.'/'.$route, ['query' => $params, 'headers' => $headers]);
} else {
$headers = $this->getHeaders();
// var_dump($this->resource.'/'.$id.'/'.$route, ['query'=>$params, 'headers' => $headers]);die;
$clientResponse = $this->client->get($this->resource.'/'.$id.'/'.$route, ['query' => $params, 'headers' => $headers]);
}
if (200 == $clientResponse->getStatusCode()) {
// var_dump($clientResponse->getBody()->getContents());die;
return new Response($clientResponse->getStatusCode(), $this->treatHydraCollection($clientResponse->getBody(), $subClassName));
}
} catch (ClientException|ServerException $e) {
return new Response($e->getCode(), $this->treatHydraCollection($e->getResponse()->getBody()->getContents(), true));
} catch (TransferException $e) {
return new Response($e->getCode());
}
return new Response();
}
/**
* Post collection operation.
*
* @param ResourceInterface $object An object representing the resource to post
*
* @return Response the response of the operation
*/
public function post(ResourceInterface $object, ?string $operation = null): Response
{
// var_dump($this->serializer->serialize($object, self::SERIALIZER_ENCODER, ['groups'=>['post']]));
// exit;
$op = '';
if (!is_null($operation)) {
$op = '/'.$operation;
}
try {
if (self::RETURN_ARRAY == $this->format) {
$headers = $this->getHeaders();
$clientResponse = $this->client->post($this->resource.$op, [
'headers' => $headers,
RequestOptions::JSON => json_decode($this->serializer->serialize($object, self::SERIALIZER_ENCODER, ['groups' => ['post']]), true),
]);
$value = json_decode((string) $clientResponse->getBody(), true);
} elseif (self::RETURN_JSON == $this->format) {
$headers = $this->getHeaders(['json']);
$clientResponse = $this->client->post($this->resource.$op, [
'headers' => $headers,
RequestOptions::JSON => json_decode($this->serializer->serialize($object, self::SERIALIZER_ENCODER, ['groups' => ['post']]), true),
]);
$value = (string) $clientResponse->getBody();
} else {
$headers = $this->getHeaders();
$clientResponse = $this->client->post($this->resource.$op, [
'headers' => $headers,
RequestOptions::JSON => json_decode($this->serializer->serialize($object, self::SERIALIZER_ENCODER, ['groups' => ['post']]), true),
]);
$value = $this->deserializer->deserialize($this->class, json_decode((string) $clientResponse->getBody(), true));
}
if (201 == $clientResponse->getStatusCode()) {
return new Response($clientResponse->getStatusCode(), $value);
}
} catch (ServerException $e) {
return new Response($e->getCode(), $e->getMessage());
} catch (ClientException $e) {
return new Response($e->getCode(), $e->getMessage());
}
return new Response();
}
/**
* Get a given url.
*
* @param string $url The url to post on
* @param array $parameters The parameters
*
* @return Response The response
*/
public function simpleGet(string $url, array $parameters = []): Response
{
try {
if (self::RETURN_JSON == $this->format) {
$headers = $this->getHeaders(['json']);
} else {
$headers = $this->getHeaders();
}
$clientResponse = $this->client->get($url, ['query' => $parameters, 'headers' => $headers]);
if (200 == $clientResponse->getStatusCode()) {
return new Response($clientResponse->getStatusCode(), $this->treatHydraCollection($clientResponse->getBody()));
}
} catch (ClientException|ServerException $e) {
return new Response($e->getCode(), $this->treatHydraCollection($e->getResponse()->getBody()->getContents(), true));
} catch (TransferException $e) {
return new Response($e->getCode());
}
return new Response();
}
/**
* Post on a given url.
*
* @param string $url The url to post on
* @param array $parameters The parameters
*
* @return Response The response
*/
public function simplePost(string $url, array $parameters = []): Response
{
try {
$headers = $this->getHeaders();
$clientResponse = $this->client->post($url, [
'headers' => $headers,
RequestOptions::JSON => $parameters,
]);
$value = (string) $clientResponse->getBody();
return new Response($clientResponse->getStatusCode(), $value);
} catch (ServerException $e) {
return new Response($e->getCode(), $e->getMessage());
} catch (ClientException $e) {
return new Response($e->getCode(), $e->getMessage());
}
}
/**
* Post item with special operation.
*
* @param ResourceInterface $object An object representing the resource to put
*
* @return Response the response of the operation
*/
public function postSpecial(ResourceInterface $object, ?array $groups = null, ?string $operation, ?array $params = null, bool $reverseOperationId = false): Response
{
if (is_null($groups)) {
$groups = ['post'];
}
// var_dump($this->resource."/".$operation);
// var_dump($this->serializer->serialize($object, self::SERIALIZER_ENCODER, ['groups'=>$groups]));die;
try {
$uri = $this->resource.'/'.$operation;
$headers = $this->getHeaders();
$clientResponse = $this->client->post($uri, [
'headers' => $headers,
RequestOptions::JSON => json_decode($this->serializer->serialize($object, self::SERIALIZER_ENCODER, ['groups' => $groups]), true),
'query' => $params,
]);
if (201 == $clientResponse->getStatusCode()) {
return new Response($clientResponse->getStatusCode(), $this->deserializer->deserialize($this->class, json_decode((string) $clientResponse->getBody(), true)));
}
} catch (ClientException|ServerException $e) {
return new Response($e->getCode(), $this->treatHydraCollection($e->getResponse()->getBody()->getContents(), true));
} catch (TransferException $e) {
return new Response($e->getCode());
}
return new Response();
}
/**
* Post collection operation with multipart/form-data.
*
* @param ResourceInterface $object An object representing the resource to post
*
* @return Response the response of the operation
*/
public function postMultiPart(ResourceInterface $object): Response
{
$multipart = [];
// we serialize the serializable properties
$data = json_decode($this->serializer->serialize($object, self::SERIALIZER_ENCODER, ['groups' => ['post']]), true);
foreach ($data as $key => $value) {
$multipart[] = [
'name' => $key,
'contents' => $value,
];
}
// we check for other possible file properties
foreach (self::FILE_PROPERTIES as $property => $getter) {
if (method_exists($object, $getter)) {
$file = $object->{$getter}();
if ($file instanceof UploadedFile) {
$multipart[] = [
'name' => $property,
'filename' => $file->getClientOriginalName(),
'contents' => fopen($file->getPathname(), 'rb'),
];
$multipart[] = [
'name' => self::FILE_ORIGINAL_NAME_PROPERTY,
'contents' => $file->getClientOriginalName(),
];
}
}
}
// var_dump($multipart);
// exit;
try {
$headers = $this->getHeaders();
$clientResponse = $this->client->post($this->resource, [
'headers' => $headers,
'multipart' => $multipart,
]);
// var_dump(json_decode((string) $clientResponse->getBody(), true));die;
if (201 == $clientResponse->getStatusCode()) {
return new Response($clientResponse->getStatusCode(), $this->deserializer->deserialize($this->class, json_decode((string) $clientResponse->getBody(), true)));
}
} catch (ClientException|ServerException $e) {
return new Response($e->getCode(), $this->treatHydraCollection($e->getResponse()->getBody()->getContents(), true));
} catch (TransferException $e) {
return new Response($e->getCode());
}
return new Response();
}
public function patch($id, ?string $operation = null, ?array $params = null, bool $reverseOperationId = false): Response
{
try {
$headers = $this->getHeaders();
$headers['Content-Type'] = 'application/merge-patch+json';
$clientResponse = $this->client->patch($this->resource.'/'.$id.'/'.$operation, ['body' => json_encode($params), 'headers' => $headers]);
if (200 == $clientResponse->getStatusCode()) {
return new Response($clientResponse->getStatusCode(), $this->deserializer->deserialize($this->class, json_decode((string) $clientResponse->getBody(), true)));
}
} catch (ClientException|ServerException $e) {
return new Response($e->getCode(), $this->treatHydraCollection($e->getResponse()->getBody()->getContents(), true));
} catch (TransferException $e) {
return new Response($e->getCode());
}
return new Response();
}
/**
* Put item operation.
*
* @param ResourceInterface $object An object representing the resource to put
*
* @return Response the response of the operation
*/
public function put(ResourceInterface $object, ?array $groups = null, ?array $params = null): Response
{
if (is_null($groups)) {
$groups = ['put'];
}
// var_dump($this->resource.'/'.$object->getId());
// var_dump($this->serializer->serialize($object, self::SERIALIZER_ENCODER, ['groups' => $groups]));
// exit;
try {
$headers = $this->getHeaders();
$clientResponse = $this->client->put($this->resource.'/'.$object->getId(), [
'headers' => $headers,
RequestOptions::JSON => json_decode($this->serializer->serialize($object, self::SERIALIZER_ENCODER, ['groups' => $groups]), true),
'query' => $params,
]);
if (200 == $clientResponse->getStatusCode()) {
return new Response($clientResponse->getStatusCode(), $this->deserializer->deserialize($this->class, json_decode((string) $clientResponse->getBody(), true)));
}
} catch (ClientException|ServerException $e) {
return new Response($e->getCode(), $this->treatHydraCollection($e->getResponse()->getBody()->getContents(), true));
} catch (TransferException $e) {
return new Response($e->getCode());
}
return new Response();
}
/**
* Put item with special operation.
*
* @param ResourceInterface $object An object representing the resource to put
*
* @return Response the response of the operation
*/
public function putSpecial(ResourceInterface $object, ?array $groups = null, ?string $operation, ?array $params = null, bool $reverseOperationId = false): Response
{
if (is_null($groups)) {
$groups = ['put'];
}
try {
if (!$reverseOperationId) {
$uri = $this->resource.'/'.$object->getId().'/'.$operation;
} else {
$uri = $this->resource.'/'.$operation.'/'.$object->getId();
}
// var_dump("put special");
// var_dump($uri);
// var_dump($this->serializer->serialize($object, self::SERIALIZER_ENCODER, ['groups'=>$groups]));
// die;
$headers = $this->getHeaders();
$clientResponse = $this->client->put($uri, [
'headers' => $headers,
RequestOptions::JSON => json_decode($this->serializer->serialize($object, self::SERIALIZER_ENCODER, ['groups' => $groups]), true),
'query' => $params,
]);
if (200 == $clientResponse->getStatusCode()) {
return new Response($clientResponse->getStatusCode(), $this->deserializer->deserialize($this->class, json_decode((string) $clientResponse->getBody(), true)));
}
} catch (ClientException|ServerException $e) {
return new Response($e->getCode(), $this->treatHydraCollection($e->getResponse()->getBody()->getContents(), true));
} catch (TransferException $e) {
return new Response($e->getCode());
}
return new Response();
}
/**
* Delete item operation.
*
* @param int $id The id of the object representing the resource to delete
*
* @return Response the response of the operation
*/
public function delete(int $id, ?array $data = null): Response
{
try {
$headers = $this->getHeaders();
$clientResponse = $this->client->delete($this->resource.'/'.$id, ['headers' => $headers, 'json' => $data]);
if (204 == $clientResponse->getStatusCode()) {
return new Response($clientResponse->getStatusCode());
}
} catch (ClientException|ServerException $e) {
return new Response($e->getCode(), $this->treatHydraCollection($e->getResponse()->getBody()->getContents(), true));
} catch (TransferException $e) {
return new Response($e->getCode());
}
return new Response();
}
public function getToken()
{
$this->createToken();
return $this->jwtToken->getToken();
}
/**
* Get the Client headers, including the token bearer.
* Automatically call for a token if not present.
*
* @param array $headers The headers to add
*/
private function getHeaders(array $headers = [])
{
$this->createToken();
// automatically add the bearer token
$headers['Authorization'] = 'Bearer '.$this->jwtToken->getToken();
// Add the locale
$headers['X-LOCALE'] = $this->request->headers->get('x-locale');
// additional headers
foreach ($headers as $header) {
switch ($header) {
case 'json':
$headers['accept'] = 'application/json';
break;
case 'ld+json':
$headers['accept'] = 'application/ld+json';
break;
}
}
return $headers;
}
/**
* Call for an api jwt token.
*
* @return null|array The token and refreshToken retrieved
*/
private function getJwtToken()
{
$value = null;
// is there a refresh token ?
if ($this->refreshToken) {
try {
$clientResponse = $this->client->post($this->refreshPath, [
'headers' => ['accept' => 'application/json'],
RequestOptions::JSON => [
'refreshToken' => $this->refreshToken,
],
]);
$value = json_decode((string) $clientResponse->getBody(), true);
} catch (ServerException $e) {
throw new ApiTokenException('Server error : unable to get an API token from refresh.');
} catch (ClientException $e) {
// todo : check the exception to test the different cases
// invalid credentials
$this->cache->deleteItem($this->tokenId.'.jwt.refresh.token');
}
}
// no refresh token or refresh token expired
if (is_null($value)) {
// We have a username and emailToken
if (!is_null($this->emailToken)) {
try {
$clientResponse = $this->client->post($this->authLoginPath, [
'headers' => ['accept' => 'application/json'],
RequestOptions::JSON => [
'email' => $this->username,
'emailToken' => $this->emailToken,
],
]);
$value = json_decode((string) $clientResponse->getBody(), true);
} catch (ServerException $e) {
throw new ApiTokenException('Unable to get an API token.');
} catch (ClientException $e) {
if ('401' == $e->getCode()) {
return new JsonResponse('bad-credentials-api');
}
throw new ApiTokenException('Unable to get an API token.');
}
// We have a reset password token
} elseif (!is_null($this->passwordToken)) {
try {
$clientResponse = $this->client->post($this->authLoginPath, [
'headers' => ['accept' => 'application/json'],
RequestOptions::JSON => [
'passwordToken' => $this->passwordToken,
],
]);
$value = json_decode((string) $clientResponse->getBody(), true);
} catch (ServerException $e) {
throw new ApiTokenException('Unable to get an API token.');
} catch (ClientException $e) {
// Wrong credentials
if ('401' == $e->getCode()) {
return new JsonResponse('bad-credentials-api');
}
throw new ApiTokenException('Unable to get an API token.');
}
} elseif (!is_null($this->ssoId) && !is_null($this->ssoProvider)) {
$params = [
'ssoId' => $this->ssoId,
'ssoProvider' => $this->ssoProvider,
'baseSiteUri' => $this->baseSiteUri,
];
if (!is_null($this->request->get('fromMobConnectSso') && self::MOB_CONNECT_CEE_LOGIN === $this->request->get('fromMobConnectSso'))) {
$params['fromSsoMobConnect'] = true;
}
try {
$clientResponse = $this->client->post($this->authLoginPath, [
'headers' => ['accept' => 'application/json'],
RequestOptions::JSON => $params,
]);
$value = json_decode((string) $clientResponse->getBody(), true);
} catch (ServerException $e) {
throw new ApiTokenException('Unable to get an API token.');
} catch (ClientException $e) {
// Wrong credentials
if ('401' == $e->getCode()) {
return new JsonResponse('bad-credentials-api');
}
throw new ApiTokenException('Unable to get an API token.');
}
} elseif (!is_null($this->usernameDelegate) && !is_null($this->username) && !is_null($this->password)) {
// we have a username, usernameDelegate and password
try {
$clientResponse = $this->client->post($this->authLoginPath, [
'headers' => ['accept' => 'application/json'],
RequestOptions::JSON => [
'username' => $this->username,
'username_delegate' => $this->usernameDelegate,
'password' => $this->password,
],
]);
$value = json_decode((string) $clientResponse->getBody(), true);
} catch (ServerException $e) {
throw new ApiTokenException('Unable to get an API token.');
} catch (ClientException $e) {
// Wrong credentials
if ('401' == $e->getCode()) {
return new JsonResponse('bad-credentials-api');
}
throw new ApiTokenException('Unable to get an API token.');
}
} else {
// we have a username and password
try {
$clientResponse = $this->client->post($this->authLoginPath, [
'headers' => ['accept' => 'application/json'],
RequestOptions::JSON => [
'username' => $this->username,
'password' => $this->password,
],
]);
$value = json_decode((string) $clientResponse->getBody(), true);
} catch (ServerException $e) {
throw new ApiTokenException('Unable to get an API token.');
} catch (ClientException $e) {
// Wrong credentials
if ('401' == $e->getCode()) {
return new JsonResponse('bad-credentials-api');
}
throw new ApiTokenException('Unable to get an API token.');
}
}
}
return $value;
}
private function treatHydraCollection($data, $class = null)
{
// if $class is defined, it's because our request concerns a subresource
if (!$class) {
$class = $this->class;
}
if (self::RETURN_OBJECT != $this->format) {
return json_decode((string) $data, true);
}
// $data comes from a GuzzleHttp request; it's a json hydra collection so when need to parse the json to an array
$data = json_decode($data, true);
$hydra = new Hydra();
if (isset($data['@context'])) {
$hydra->setContext($data['@context']);
}
if (isset($data['@id'])) {
$hydra->setId($data['@id']);
}
if (isset($data['@type'])) {
$hydra->setType($data['@type']);
}
if (isset($data['hydra:title'])) {
$hydra->setTitle($data['hydra:title']);
}
if (isset($data['hydra:description'])) {
$hydra->setDescription($data['hydra:description']);
}
if (isset($data['trace'])) {
$hydra->setTraces(Trace::load($data['trace']));
}
if (isset($data['hydra:totalItems'])) {
$hydra->setTotalItems($data['hydra:totalItems']);
}
if (isset($data['hydra:member'])) {
/*
* deserialization of nested array of objects doesn't work...
* only the root object deserialization works...
* see https://medium.com/@rebolon/the-symfony-serializer-a-great-but-complex-component-fbc09baa65a0
*/
/*$members = [];
foreach ($data["hydra:member"] as $key=>$value) {
$object = $this->serializer->deserialize(json_encode($value), $this->class, self::SERIALIZER_ENCODER);
// we had the @id => iri
if (isset($value['@id']) && method_exists($object, 'setIri')) $object->setIri($value['@id']);
$members[] = $object;
}
$hydra->setMember($members);*/
$members = [];
foreach ($data['hydra:member'] as $key => $value) {
$members[] = $this->deserializer->deserialize($class, $value);
}
$hydra->setMember($members);
}
if (isset($data['hydra:view'])) {
$hydraView = new HydraView();
if (isset($data['hydra:view']['@id'])) {
$hydraView->setId($data['hydra:view']['@id']);
}
if (isset($data['hydra:view']['@type'])) {
$hydraView->setType($data['hydra:view']['@type']);
}
if (isset($data['hydra:view']['hydra:first'])) {
$hydraView->setFirst($data['hydra:view']['hydra:first']);
}
if (isset($data['hydra:view']['hydra:last'])) {
$hydraView->setLast($data['hydra:view']['hydra:last']);
}
if (isset($data['hydra:view']['hydra:next'])) {
$hydraView->setNext($data['hydra:view']['hydra:next']);
}
$hydra->setView($hydraView);
}
return $hydra;
}
private function pluralize(string $name): string
{
return $this->inflector->pluralize($this->inflector->tableize($name));
}
}
/**
* This class permits to remove null values or empty arrays when normalizing.
* It also permits to replace object values by their IRI if set.
*
* @author Sylvain Briat <sylvain.briat@covivo.eu>
*/
class RemoveNullObjectNormalizer extends ObjectNormalizer
{
public function normalize($object, $format = null, array $context = [])
{
// circular references are now handled by a dedicated class in Api\Serializer
$data = parent::normalize($object, $format, $context);
if (is_array($data)) {
return $this->replaceIris(array_filter($data, function ($value) {
return (null !== $value) && (!(empty($value) && is_array($value)));
}));
}
return $data;
}
/**
* This function replaces each value in an array by its IRI value if IRI key exists.
* (recursive function).
*
* eg:
*
* [
* "id" => 1,
* "user" => [
* "id" => 2,
* "name" => "John",
* "iri" => "/users/2"
* ]
* ]
*
* will be replaced by :
*
* [
* "id" => 1,
* "user" => "/users/2"
* ]
*/
private function replaceIris(array $array): array
{
$replacedArray = [];
foreach ($array as $key => $value) {
if (is_array($value)) {
if (isset($value['iri']) && !is_null($value['iri'])) {
$replacedArray[$key] = $value['iri'];
} else {
$replacedArray[$key] = self::replaceIris($value);
}
} else {
$replacedArray[$key] = $value;
}
}
return $replacedArray;
}
}