<?php
namespace App\Controller\Api;
use App\Business\User;
use App\Entity\Callback;
use App\Extern\NeosException;
use App\Services\CallbackService;
use App\Services\UserService;
use App\Services\XmlGeneratorService;
use App\Utils\NeosParams;
use GuzzleHttp\Client;
use GuzzleHttp\Exception\BadResponseException;
use GuzzleHttp\Exception\GuzzleException;
use JSend\JSendResponse;
use Psr\Cache\CacheItemPoolInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\StreamInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
use App\Services\ActionLoggerService;
use Exception;
use App\Utils\Helper;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Core\Security;
use Symfony\Component\Security\Core\User\UserInterface;
use FOS\RestBundle\Controller\Annotations\Put;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security as SecurityAnnotation;
/**
* @package App\Controller\Api
*/
class ApiNeosController extends AbstractController
{
const INTERNAL_CODEOPS = 'ZZEBU';
public const CONTEXT_CODE_WEB_SERVICE = 'WEB_SERVICE';
public const CONTEXT_CODE_ONLINE_BOOKING = 'ONLINE_BOOKING';
private const NEOS_JSON_ENDPOINT = '/neos/rest/enet-api/';
private const NEOS_XML_ENDPOINT = '/neos/rest/enet/';
private const NEOS_PRIVATE_ENDPOINT = '/neos/rest/';
private const NEOS_PRIVATE_AUTHORIZED_EVENTS = [
65000, // Neos Event for RADIO
];
private const XML = "XML";
private const JSON = "JSON";
private const MULTIPART = "MULTIPART";
protected NeosParams $neosParams;
protected UserService $userService;
protected XmlGeneratorService $xmlGeneratorService;
protected CallbackService $callbackService;
protected Security $security;
protected ActionLoggerService $actionLoggerService;
protected CacheItemPoolInterface $cachePool;
public function __construct(
NeosParams $neosParams,
UserService $userService,
XmlGeneratorService $xmlGeneratorService,
CallbackService $callbackService,
Security $security,
ActionLoggerService $actionLoggerService,
CacheItemPoolInterface $cachePool
)
{
$this->neosParams = $neosParams;
$this->userService = $userService;
$this->xmlGeneratorService = $xmlGeneratorService;
$this->callbackService = $callbackService;
$this->security = $security;
$this->actionLoggerService = $actionLoggerService;
$this->cachePool = $cachePool;
}
/**
* @param Request $request
* @Put("/api/1.0/synopsis-notifications/organizationCode/{organizationCode}", name="api_synopsis_notification", options={"expose" = true})
* @Put("/api/1.0/synopsis-notifications/organizationCode/{organizationCode}/", name="api_synopsis_notification_slash", options={"expose" = true})
* @Put("/rest/1.0/synopsis-notifications/organizationCode/{organizationCode}", name="api_synopsis_notification_old", options={"expose" = true})
* @Put("/rest/1.0/synopsis-notifications/organizationCode/{organizationCode}/", name="api_synopsis_notification_old_slash", options={"expose" = true})
* @return JsonResponse|Response
*/
public function getSynopsisNotificationAction(Request $request)
{
$organizationCode = $request->get('organizationCode');
$xmlData = $this->callbackService->sendCallback($organizationCode, Callback::SYNOPSIS);
return $this->buildResponse($request, $xmlData);
}
/**
* @param Request $request
* @Put("/api/1.0/request-notifications/organizationCode/{organizationCode}", name="api_request_notification", options={"expose" = true})
* @Put("/api/1.0/request-notifications/organizationCode/{organizationCode}/", name="api_request_notification_slash", options={"expose" = true})
* @Put("/rest/1.0/request-notifications/organizationCode/{organizationCode}", name="api_request_notification_old", options={"expose" = true})
* @Put("/rest/1.0/request-notifications/organizationCode/{organizationCode}/", name="api_request_notification_old_slash", options={"expose" = true})
* @return JsonResponse|Response
*/
public function getRequestNotificationAction(Request $request)
{
$organizationCode = $request->get('organizationCode');
$xmlData = $this->callbackService->sendCallback($organizationCode, Callback::REQUEST);
return $this->buildResponse($request, $xmlData);
}
/**
* @param Request $request
* @param $xmlData
* @return JsonResponse|Response
*/
protected function buildResponse(Request $request, $xmlData)
{
$format = $request->get('format');
if (empty($format)) {
$format = 'xml';
}
//only xml and json format are available
if ($format !== 'json' && $format !== 'xml' && $format !== 'TEXT' && $format !== 'XML') {
throw new BadRequestHttpException("only xml and json format are available");
}
if ($format === 'xml' || $format === 'XML' || $format === 'TEXT') {
$response = new Response($xmlData, 200, ['Content-Type' => 'text/xml; charset=UTF-8']);
} else {
$jsend = new JSendResponse('success', [Helper::convertXmlToJson($xmlData)]);
$response = new JsonResponse($jsend);
}
return $response;
}
/**
* @param Request $request
* @param UserInterface $user
* @param string $call_url_suffix
* @return JsonResponse
* @throws GuzzleException
* @throws NeosException
* @Route("/enet-rest/{version}/{call_url_suffix}", name="rest_neos_call", options={"expose" = true}, requirements={"call_url_suffix"=".+"})
*/
public function callXmlNeosWebServices(Request $request, UserInterface $user, string $version, string $call_url_suffix)
{
//date have xml format by default
$myWebServiceID = $request->query->get('myWebServiceID');
$queryParams = $request->query->all();
unset($queryParams["timeStamp"]);//Check if we need to remove them after
unset($queryParams["myWebServiceID"]);
unset($queryParams["footprint"]);
unset($queryParams["responseFormat"]);
$method = $request->getMethod();
$parameters = $request->getContent();
$call_url = $this->neosParams->url . ApiNeosController::NEOS_XML_ENDPOINT . $version . "/" . $call_url_suffix;
$response = $this->callNeosWS($myWebServiceID, $user, $method, $call_url, $parameters, $queryParams, ApiNeosController::XML);
if ((Helper::contains($call_url_suffix, 'synopsis') || Helper::contains($call_url_suffix, 'synopses')) && !empty($queryParams) && !empty($queryParams["orgCode"]) && !$this->checkIfOpscodeFamily($myWebServiceID, $user, $queryParams["orgCode"])) {
$xmlData = $this->xmlGeneratorService->returnXMLError(['code' => Response::HTTP_UNAUTHORIZED, 'label' => 'Unauthorized']);
$response = new Response($xmlData, Response::HTTP_UNAUTHORIZED);
$response->headers->set('Content-Type', 'application/xml');
return $response;
}
if (!$response) {
$xmlData = $this->xmlGeneratorService->returnXMLError(['code' => Response::HTTP_NOT_FOUND, 'label' => 'Web service not found']);
$response = new Response($xmlData, Response::HTTP_NOT_FOUND);
$response->headers->set('Content-Type', 'application/xml');
return $response;
}
return $response;
}
/**
* @param Request $request
* @param UserInterface $user
* @param string $call_url_suffix
* @return JsonResponse
* @throws GuzzleException
* @throws NeosException
* @Route("/enet-api/intern/{call_url_suffix}", name="api_neos_intern_call", options={"expose" = true}, requirements={"call_url_suffix"=".+"})
* @SecurityAnnotation ("is_granted('ROLE_API_NEOS_INTERN')")
*/
public function callInternJsonNeosWebServices(Request $request, UserInterface $user, string $call_url_suffix)
{
//date have xml format by default
$myWebServiceID = $request->query->get('myWebServiceID');
$queryParams = $request->query->all();
unset($queryParams["timeStamp"]);//Check if we need to remove them after
unset($queryParams["myWebServiceID"]);
unset($queryParams["footprint"]);
unset($queryParams["responseFormat"]);
$method = $request->getMethod();
$parameters = json_decode($request->getContent(), true);
$call_url = $this->neosParams->url . ApiNeosController::NEOS_PRIVATE_ENDPOINT . $call_url_suffix;
$response = $this->callNeosWS($myWebServiceID, $user, $method, $call_url, $parameters, $queryParams, ApiNeosController::JSON);
if ((Helper::contains($call_url_suffix, 'synopsis') || Helper::contains($call_url_suffix, 'synopses')) && !empty($queryParams) && !empty($queryParams["orgCode"]) && !$this->checkIfOpscodeFamily($myWebServiceID, $user, $queryParams["orgCode"])) {
$data = [
"code" => Response::HTTP_UNAUTHORIZED,
"label" => "Unauthorized"
];
return new JsonResponse($data, Response::HTTP_UNAUTHORIZED);
}
if (!$response) {
$data = [
"code" => Response::HTTP_NOT_FOUND,
"label" => "Web service not found"
];
return new JsonResponse($data, Response::HTTP_NOT_FOUND);
}
$content = $response->getContent();
if($response->getContent() !== null) {
$jsonData = json_decode($content, true);
if(isset($jsonData["event"]["no"])){
$eventNo = $jsonData["event"]["no"];
if (in_array($eventNo, ApiNeosController::NEOS_PRIVATE_AUTHORIZED_EVENTS)) {
return $response;
} else {
$data = [
"code" => Response::HTTP_UNAUTHORIZED,
"label" => "Unauthorized"
];
return new JsonResponse($data, Response::HTTP_UNAUTHORIZED);
}
}
}
return $response;
}
/**
* @param Request $request
* @param UserInterface $user
* @param string $call_url_suffix
* @return JsonResponse
* @throws GuzzleException
* @throws NeosException
* @Route("/enet-api/{version}/{call_url_suffix}", name="api_neos_call", options={"expose" = true}, requirements={"call_url_suffix"=".+"})
*/
public function callJsonNeosWebServices(Request $request, UserInterface $user, string $version, string $call_url_suffix)
{
//date have xml format by default
$myWebServiceID = $request->query->get('myWebServiceID');
$queryParams = $request->query->all();
unset($queryParams["timeStamp"]);//Check if we need to remove them after
unset($queryParams["myWebServiceID"]);
unset($queryParams["footprint"]);
unset($queryParams["responseFormat"]);
$method = $request->getMethod();
$call_url = $this->neosParams->url . ApiNeosController::NEOS_JSON_ENDPOINT . $version . "/" . $call_url_suffix;
if (str_starts_with($request->headers->get('Content-Type', ''), 'multipart/form-data')) {
$parameters = $this->buildMultipartFromRequest($request);
$response = $this->callNeosWS($myWebServiceID, $user, $method, $call_url, $parameters, $queryParams, ApiNeosController::MULTIPART);
} else {
$parameters = json_decode($request->getContent(), true);
$response = $this->callNeosWS($myWebServiceID, $user, $method, $call_url, $parameters, $queryParams, ApiNeosController::JSON);
}
if ((Helper::contains($call_url_suffix, 'synopsis') || Helper::contains($call_url_suffix, 'synopses')) && !empty($queryParams) && !empty($queryParams["orgCode"]) && !$this->checkIfOpscodeFamily($myWebServiceID, $user, $queryParams["orgCode"])) {
$data = [
"code" => Response::HTTP_UNAUTHORIZED,
"label" => "Unauthorized"
];
return new JsonResponse($data, Response::HTTP_UNAUTHORIZED);
}
if (!$response) {
$data = [
"code" => Response::HTTP_NOT_FOUND,
"label" => "Web service not found"
];
return new JsonResponse($data, Response::HTTP_NOT_FOUND);
}
return $response;
}
/**
* @param Request $request
* @param string $url
* @return mixed|null|ResponseInterface
* @throws AccessDeniedHttpException
* @throws NeosException
* @throws GuzzleException
*/
private function callNeosWS(string $myWebServiceID, UserInterface $user, string $method, string $url, $parameters, $queryParams, $mode = ApiNeosController::JSON)
{
if ($mode === ApiNeosController::JSON) {
$contentType = 'application/json';
} elseif ($mode === ApiNeosController::MULTIPART) {
$contentType = null; // Guzzle sets Content-Type with boundary automatically
} else {
$contentType = 'application/xml';
}
$opscode = null;
$isWebServiceCall = (!empty($myWebServiceID)) ? true : false;
if ($user === null) {
throw new AccessDeniedHttpException("Unknown user");
}
$client = new Client();
if ($mode === ApiNeosController::JSON || $mode === ApiNeosController::MULTIPART) {
$headers = [
'headers' => $this->buildNeosHeaders($user, is_array($parameters) && !isset($parameters[0]) ? $parameters : null, $contentType, $isWebServiceCall, $opscode)
];
} else {
$headers = [
'headers' => $this->buildNeosHeaders($user, Helper::convertXmlToArray($parameters), $contentType, $isWebServiceCall, $opscode)
];
}
$queryParams = [
"query" => $queryParams
];
$body = $this->formatBody($parameters, $mode);
$options = array_merge($queryParams, $headers, $body);
try {
$this->actionLoggerService->logActionV2('CALL_', $method, $headers['headers']['ws_requestid'], $url,
json_encode($options));
$response = $client->request(
$method,
$url,
$options
);
$messageToLog = $response->getBody();
if ($messageToLog == null) {
$messageToLog = '';
}
$this->actionLoggerService->logActionV2('RESP_', $method, $headers['headers']['ws_requestid'],
$url, $messageToLog);
return $this->formatResponse($response, $mode);
} catch (Exception $e) {
$initErrorMessage = ApiNeosController::extractMessageError($e);
//NEOS sends error messages even for basic calls without any issue...
$this->actionLoggerService->logActionErrorV2('_RESP', $method, $headers['headers']['ws_requestid'], $url,
$initErrorMessage);
if ($e instanceof GuzzleException) {
if ($e->getCode() === 503) {
throw new NeosException(
sprintf(
"Our NEOS service is currently unavailable due to %s.
Please refresh the system and try again. If the problem persists, please contact websupport@eurovisionservices.com.",
$initErrorMessage
), $initErrorMessage, $e->getCode());
}
return $this->formatExceptionResponse($e, $mode);
} else {
throw new Exception($e);
}
}
return $response;
}
/**
* @param UserInterface $user
* @param array|null $body
* @param string $contentType
* @param bool|null $isWebServiceCall
* @param string|null $opscode
* @return array
*/
private
function buildNeosHeaders(UserInterface $user, ?array $body, ?string $contentType = 'application/json', ?bool $isWebServiceCall = false, string $opscode = null): array
{
$codeOps = $user->getCompanies() != null ? $user->getCompanies()[0] : '';
$orgCode = (!empty($opscode)) ? $opscode : $codeOps;
if (!empty($body) && !empty($body['customer']) && $codeOps === ApiNeosController::INTERNAL_CODEOPS) {
$orgCode = $body['customer'];
}
$headers = [
'ws_requestid' => hexdec(uniqid()),
'orgcode' => $orgCode,
'useid' => $user->getDisplayName(),
'appCode' => ($isWebServiceCall) ? ApiNeosController::CONTEXT_CODE_WEB_SERVICE : ApiNeosController::CONTEXT_CODE_ONLINE_BOOKING,
'wsrequestapp' => ($isWebServiceCall) ? ApiNeosController::CONTEXT_CODE_WEB_SERVICE : ApiNeosController::CONTEXT_CODE_ONLINE_BOOKING,
'ws_request_source' => ($isWebServiceCall) ? ApiNeosController::CONTEXT_CODE_WEB_SERVICE : ApiNeosController::CONTEXT_CODE_ONLINE_BOOKING,
'useemail' => $user->getContactMail(),
'uselastname' => $user->getLastName(),
'usefirstname' => $user->getFirstName(),
];
if ($contentType !== null) {
$headers['Content-Type'] = $contentType;
}
return $headers;
}
/**
* @param $ex
* @return StreamInterface
*/
public
static function extractMessageError(Exception $ex): string
{
$exceptionFromNEOS = $ex->getPrevious();
$exceptionToUse = $ex;
if ($exceptionFromNEOS != null) {
$exceptionToUse = $exceptionFromNEOS;
}
$messageError = $exceptionToUse->getMessage();
if ($exceptionToUse instanceof BadResponseException) {
/** @var BadResponseException $exceptionToUse */
$messageError = $exceptionToUse->getResponse()->getBody()->getContents();
if (Helper::isJSON($messageError)) {
$errorAsArray = json_decode($messageError, true);
$newMessage = '';
if (array_key_exists('label', $errorAsArray)) {
$newMessage = $errorAsArray['label'] . ' ';
unset($errorAsArray['label']);
}
if (array_key_exists('code', $errorAsArray)) {
$newMessage .= '(code ' . $errorAsArray['code'] . ')';
unset($errorAsArray['code']);
}
foreach ($errorAsArray as $id => $value) {
if (is_array($value)) {
$newMessage .= ', ' . $id . ' = ' . json_encode($value);
} else {
$newMessage .= ', ' . $id . ' = ' . $value;
}
}
$messageError = $newMessage;
}
}
return $messageError;
}
/**
* @param $body
* @param $mode
* @return array
*/
private function formatBody($body, $mode)
{
if ($body === null || $body === '') { //initial check
return [];
}
if ($mode === ApiNeosController::MULTIPART) {
return [
'multipart' => $body
];
}
if ($mode === ApiNeosController::JSON) {
return [
'json' => $body
];
} else { //XML Body
return [
'body' => $body
];
}
}
private function buildMultipartFromRequest(Request $request): array
{
$multipart = [];
$files = $request->files->all();
$firstFile = !empty($files) ? array_values($files)[0] : null;
foreach ($request->request->all() as $name => $value) {
if ($name === 'fileName' && $firstFile !== null) {
$extension = $firstFile->getClientOriginalExtension();
if ($extension && !str_ends_with($value, '.' . $extension)) {
$value .= '.' . $extension;
}
}
$multipart[] = ['name' => $name, 'contents' => $value];
}
foreach ($files as $name => $file) {
$multipart[] = [
'name' => $name,
'contents' => fopen($file->getPathname(), 'r'),
'filename' => $file->getClientOriginalName(),
'headers' => ['Content-Type' => $file->getMimeType()],
];
}
return $multipart;
}
/**
* @param $response
* @param $mode
* @return JsonResponse|Response
* Workflow:
* If (Method GET and response in JSON) OR If Mode = JSON -> Send in Json
* Else -> Body in XML : If desired response is JSON -> format XML in json before sending ELSE send XML body
*/
private function formatResponse($response, $mode)
{
if ($mode === ApiNeosController::JSON || $mode === ApiNeosController::MULTIPART) {
return new JsonResponse(json_decode((string)$response->getBody(), true), $response->getStatusCode());
} else {
$response = new Response($response->getBody(), $response->getStatusCode());
$response->headers->set('Content-Type', 'application/xml');
return $response;
}
}
/**
* @param $exception
* @param $mode
* @return false|JsonResponse|Response
* Workflow: If 404 and response in HTML -> It's Tomcat responding saying that the endpoint does not exist in JSON -> return false
* If (Method GET and response in JSON) OR If Mode = JSON -> Send in Json
* Else -> Body in XML : If desired response is JSON -> format XML in json before sending ELSE send XML body
*
*/
private
function formatExceptionResponse($exception, $mode) //Format response from Neos before sending to the client
{
if ($exception->getCode() === 404 && Helper::containsIgnoreCase((string)$exception->getResponse()->getBody(), '</html>')) {
return false;
}
if ($mode === ApiNeosController::JSON || $mode === ApiNeosController::MULTIPART) {
return new JsonResponse(json_decode((string)$exception->getResponse()->getBody(), true), $exception->getCode());
} else {
$response = new Response($exception->getResponse()->getBody(), $exception->getCode());
$response->headers->set('Content-Type', 'application/xml');
return $response;
}
}
/**
* Check if the opscode given is in the family of the user
* @param string $myWebServiceID
* @param UserInterface $user
* @param null|string $opscodeParam only used for logs
* @return mixed|null|bool
*/
public function checkIfOpscodeFamily(string $myWebServiceID, UserInterface $user, ?string $opscodeParam)
{
if ($user->getCompanies()[0] == $opscodeParam) {
return true;
}
$call_url = $this->neosParams->url . ApiNeosController::NEOS_XML_ENDPOINT . '1.0/organizations/' . $user->getCompanies()[0] . '/family-destinations';
$response = $this->callNeosWS($myWebServiceID, $user, 'GET', $call_url, "", [], ApiNeosController::XML);
$userOrgCodeFamilyXml = simplexml_load_string($response->getContent());
if (!empty($userOrgCodeFamilyXml)) {
foreach ($userOrgCodeFamilyXml->organization as $anOrgFamily) {
if (!empty($anOrgFamily) && !empty($anOrgFamily->code) && $anOrgFamily->code == $opscodeParam) {
return true;
}
}
}
return false;
}
}