0
0
mirror of https://github.com/salesagility/SuiteCRM.git synced 2024-11-22 07:52:36 +00:00
salesagility_SuiteCRM/lib/API/v8/Controller/ApiController.php
2023-07-18 15:53:08 +01:00

365 lines
13 KiB
PHP

<?php
/**
*
* SugarCRM Community Edition is a customer relationship management program developed by
* SugarCRM, Inc. Copyright (C) 2004-2013 SugarCRM Inc.
*
* SuiteCRM is an extension to SugarCRM Community Edition developed by SalesAgility Ltd.
* Copyright (C) 2011 - 2018 SalesAgility Ltd.
*
* This program is free software; you can redistribute it and/or modify it under
* the terms of the GNU Affero General Public License version 3 as published by the
* Free Software Foundation with the addition of the following permission added
* to Section 15 as permitted in Section 7(a): FOR ANY PART OF THE COVERED WORK
* IN WHICH THE COPYRIGHT IS OWNED BY SUGARCRM, SUGARCRM DISCLAIMS THE WARRANTY
* OF NON INFRINGEMENT OF THIRD PARTY RIGHTS.
*
* 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 http://www.gnu.org/licenses or write to the Free
* Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
* 02110-1301 USA.
*
* You can contact SugarCRM, Inc. headquarters at 10050 North Wolfe Road,
* SW2-130, Cupertino, CA 95014, USA. or at email address contact@sugarcrm.com.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU Affero General Public License version 3.
*
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "Powered by
* SugarCRM" logo and "Supercharged by SuiteCRM" logo. If the display of the logos is not
* reasonably feasible for technical reasons, the Appropriate Legal Notices must
* display the words "Powered by SugarCRM" and "Supercharged by SuiteCRM".
*/
namespace SuiteCRM\API\v8\Controller;
use Interop\Container\Exception\ContainerException;
use JsonSchema\Validator;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\ContainerInterface;
use Psr\Container\NotFoundExceptionInterface;
use Psr\Http\Message\ResponseInterface as Response;
use Slim\Http\Request as Request;
use Psr\Log\LoggerAwareInterface;
use Psr\Log\LoggerInterface;
use RuntimeException;
use SuiteCRM\API\JsonApi\v1\JsonApi;
use SuiteCRM\API\v8\Exception\ApiException;
use SuiteCRM\API\v8\Exception\InvalidJsonApiRequestException;
use SuiteCRM\API\v8\Exception\NotAcceptableException;
use SuiteCRM\API\v8\Exception\UnsupportedMediaTypeException;
use SuiteCRM\ErrorMessage;
use SuiteCRM\JsonApiErrorObject;
use SuiteCRM\Utility\Paths;
use SuiteCRM\Utility\SuiteLogger as Logger;
#[\AllowDynamicProperties]
class ApiController implements LoggerAwareInterface
{
public const CONTENT_TYPE = 'application/vnd.api+json';
public const CONTENT_TYPE_JSON = 'application/vnd.api+json';
public const CONTENT_TYPE_HEADER = 'Content-Type';
public const LINKS = 'links';
public const VERSION_MAJOR = 8;
public const VERSION_MINOR = 0;
public const VERSION_PATCH = 0;
public const VERSION_STABILITY = 'ALPHA';
/**
* @var LoggerInterface $logger
*/
protected $logger;
/**
* @var ContainerInterface $containers
*/
protected $containers;
/**
* @var Paths $paths
*/
protected $paths;
/**
* ApiController constructor.
* @param ContainerInterface $containers
* @throws ContainerException
* @throws NotFoundExceptionInterface
* @throws ContainerExceptionInterface
*/
public function __construct(ContainerInterface $containers)
{
$this->containers = $containers;
$this->paths = new Paths();
}
/**
* @param Request $request
* @param Response $response
* @param array $payload
* @return Response
* @throws RuntimeException
*/
protected function generateJsonApiResponse(Request $request, Response $response, $payload)
{
$apiErrorObjectArrays = [];
try {
$negotiated = $this->negotiatedJsonApiContent($request, $response);
if (in_array($negotiated->getStatusCode(), array(415, 406), true)) {
// return error instead of response
return $negotiated;
}
$payload['meta']['suiteapi'] = array(
'major' => self::VERSION_MAJOR,
'minor' => self::VERSION_MINOR,
'patch' => self::VERSION_PATCH,
'stability' => self::VERSION_STABILITY,
);
$jsonAPI = $this->containers->get('JsonApi');
$payload['jsonapi'] = $jsonAPI->toJsonApiResponse();
// Validate Response
$data = json_decode(json_encode($payload));
$validator = new Validator();
$validator->validate($data, (object)['$ref' => 'file://' . realpath($jsonAPI->getSchemaPath())]);
if (!$validator->isValid()) {
$errors = $validator->getErrors();
$this->logger->error('[Invalid Payload Response]'. json_encode($payload));
$apiErrorObjects = [];
foreach ($errors as $error) {
$apiErrorObject = new JsonApiErrorObject();
$apiErrorObject->retrieveFromRequest($request);
$apiErrorObjectArray = array_merge($error, $apiErrorObject->export());
$apiErrorObjectArrays[] = $apiErrorObjectArray;
}
$payload['errors'] = array_merge($payload['errors'], $apiErrorObjectArrays);
}
json_encode($payload);
if (json_last_error() != JSON_ERROR_NONE) {
throw new Exception('Generating JSON payload failed: ' . json_last_error_msg());
}
if (isset($payload['errors'][0]['status'])) {
$status = $payload['errors'][0]['status'];
} else {
$status = $response->getStatusCode();
}
return $response
->withHeader(self::CONTENT_TYPE_HEADER, self::CONTENT_TYPE)
->withStatus($status)
->write(json_encode($payload));
} catch (\Exception $e) {
$errorMessage = 'Generate JSON API Response exception detected: ' . get_class($e) . ': ' . $e->getMessage() . ' (' . $e->getCode() . ')';
if (inDeveloperMode()) {
ErrorMessage::log($errorMessage);
}
throw new RuntimeException($errorMessage, $e->getCode(), $e);
}
}
/**
*
* @param Request $request
* @param \Exception $e
* @param array $payload
* @return array
* @throws RuntimeException
*/
protected function handleExceptionIntoPayloadError(Request $request, \Exception $exception, $payload)
{
try {
ErrorMessage::log($exception->getMessage());
$error = new JsonApiErrorObject();
$error->retrieveFromRequest($request)->retrieveFromException($exception);
$payload['errors'][] = $error->export();
return $payload;
} catch (Exception $e) {
$errorMessage = 'Generate JSON API Error Response exception detected: ' . get_class($e) . ': ' . $e->getMessage() . ' (' . $e->getCode() . ')';
if (inDeveloperMode()) {
ErrorMessage::log($errorMessage);
}
throw new RuntimeException($errorMessage, $e->getCode(), $e);
}
}
/**
* @param Request $request
* @param Response $response
* @param \Exception|ApiException $exception
* @return integer
* @throws RuntimeException
*/
public function generateJsonApiErrorResponse(Request $request, Response $response, \Exception $exception)
{
try {
$jsonError = array(
'code' => $exception->getCode(),
'title' => $exception->getMessage(),
);
if (null === $this->logger) {
$this->setLogger(new Logger());
}
if (is_subclass_of($exception, ApiException::class)) {
$jsonError['detail'] = $exception->getDetail();
$jsonError['source'] = $exception->getSource();
$response = $response->withStatus($exception->getHttpStatus());
$logMessage =
' Code: [' . $exception->getCode() . ']' .
' Status: [' . $exception->getHttpStatus() . ']' .
' Message: ' . $exception->getMessage() .
' Detail: ' . $exception->getDetail() .
' Source: [' . $exception->getSource()['pointer'] . ']';
$this->logger->error($logMessage);
} else {
$response = $response->withStatus(400);
$logMessage = $exception->getMessage();
$this->logger->error($logMessage);
}
$jsonError['status'] = $response->getStatusCode();
$payload = array(
'errors' => array(
$jsonError
)
);
$payload['meta']['suiteapi'] = array(
'major' => self::VERSION_MAJOR,
'minor' => self::VERSION_MINOR,
'patch' => self::VERSION_PATCH,
'stability' => self::VERSION_STABILITY,
);
/** @var JsonApi $jsonAPI */
$jsonAPI = $this->containers->get('JsonApi');
$payload['jsonapi'] = $jsonAPI->toJsonApiResponse();
$payload = $this->handleExceptionIntoPayloadError($request, $exception, $payload);
return $response
->withHeader(self::CONTENT_TYPE_HEADER, self::CONTENT_TYPE)
->write(json_encode($payload));
} catch (\Exception $e) {
$errorMessage = 'Generate JSON API Error Response exception detected: ' . get_class($e) . ': ' . $e->getMessage() . ' (' . $e->getCode() . ')';
if (inDeveloperMode()) {
ErrorMessage::log($errorMessage);
}
throw new RuntimeException($errorMessage, $e->getCode(), $e);
}
}
/**
* @param Request $request
* @param Response $response
* @return Response
* @throws NotAcceptableException
* @throws UnsupportedMediaTypeException
*/
protected function negotiatedJsonApiContent(Request $request, Response $response)
{
$contentType = $request->getContentType();
if ($contentType !== self::CONTENT_TYPE) {
throw new UnsupportedMediaTypeException('Request "Content-Type" should be "' . self::CONTENT_TYPE . '", ' . ($contentType ? '"' . $contentType . '" given' : 'request doesn\'t have "Content-Type"') . ' in header.');
}
$header = $request->getHeader('Accept');
if (empty($header)) {
throw new NotAcceptableException('Header should contains an "Accept" header.');
}
if (count($header) !== 1) {
throw new NotAcceptableException('Header should contains exactly one "Accept" header.');
}
if ($header[0] !== self::CONTENT_TYPE) {
throw new NotAcceptableException('Header "Accept" should be "' . self::CONTENT_TYPE . '", ' . ($header[0] ? '"' . $header[0] . '" given.' : 'request doesn\'t have "Accept"'));
}
if ($this->logger === null) {
$this->setLogger(new Logger());
}
$this->logger->debug('Json ApiController negotiated content type Successfully');
return $response;
}
/**
* @param Request $request
* @throws InvalidJsonApiRequestException
*/
protected function validateRequestWithJsonApiSchema(Request $request)
{
// Validate Response
$jsonAPI = $this->containers->get('JsonApi');
$data = json_decode($request->getBody());
$validator = new Validator();
$validator->validate($data, (object)['$ref' => 'file://' . realpath($jsonAPI->getSchemaPath())]);
if (!$validator->isValid()) {
$errors = $validator->getErrors();
$this->logger->error('[Invalid Payload Request]'. $request->getBody());
throw new InvalidJsonApiRequestException('Invalid Payload Request deteced: ' . $errors[0]['property']. ' ' .$errors[0]['message']);
}
}
/**
* @return int
*/
public function getVersionMajor()
{
return self::VERSION_MAJOR;
}
/**
* @return int
*/
public function getVersionMinor()
{
return self::VERSION_MINOR;
}
/**
* @return int
*/
public function getVersionPatch()
{
return self::VERSION_PATCH;
}
/**
* @return string
*/
public function getVersionStability()
{
return self::VERSION_STABILITY;
}
/**
* @param LoggerInterface $logger
*/
public function setLogger(LoggerInterface $logger)
{
$this->logger = $logger;
}
}