Skip to content

Commit

Permalink
IBX-8784: Added AbstractExceptionVisitor template to allow fine-gra…
Browse files Browse the repository at this point in the history
…ined control of output
  • Loading branch information
Steveb-p authored Oct 11, 2024
1 parent a3f7066 commit fd2cbf5
Show file tree
Hide file tree
Showing 4 changed files with 173 additions and 123 deletions.
1 change: 1 addition & 0 deletions .php-cs-fixer.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
$configFactory = new InternalConfigFactory();
$configFactory->withRules([
'declare_strict_types' => false,
'phpdoc_no_empty_return' => false,
]);

return $configFactory
Expand Down
15 changes: 0 additions & 15 deletions phpstan-baseline.neon
Original file line number Diff line number Diff line change
Expand Up @@ -2890,11 +2890,6 @@ parameters:
count: 1
path: src/lib/Server/Output/ValueObjectVisitor/ContentFieldValidationException.php

-
message: "#^Method Ibexa\\\\Rest\\\\Server\\\\Output\\\\ValueObjectVisitor\\\\ContentFieldValidationException\\:\\:visit\\(\\) has no return type specified\\.$#"
count: 1
path: src/lib/Server/Output/ValueObjectVisitor/ContentFieldValidationException.php

-
message: "#^Method Ibexa\\\\Rest\\\\Server\\\\Output\\\\ValueObjectVisitor\\\\ContentList\\:\\:visit\\(\\) has no return type specified\\.$#"
count: 1
Expand Down Expand Up @@ -3175,16 +3170,6 @@ parameters:
count: 1
path: src/lib/Server/Output/ValueObjectVisitor/DeletedUserSession.php

-
message: "#^Method Ibexa\\\\Rest\\\\Server\\\\Output\\\\ValueObjectVisitor\\\\Exception\\:\\:visit\\(\\) has no return type specified\\.$#"
count: 1
path: src/lib/Server/Output/ValueObjectVisitor/Exception.php

-
message: "#^Property Ibexa\\\\Rest\\\\Server\\\\Output\\\\ValueObjectVisitor\\\\Exception\\:\\:\\$httpStatusCodes type has no value type specified in iterable type array\\.$#"
count: 1
path: src/lib/Server/Output/ValueObjectVisitor/Exception.php

-
message: "#^Property Ibexa\\\\Rest\\\\Server\\\\Output\\\\ValueObjectVisitor\\\\Exception\\:\\:\\$translator \\(Symfony\\\\Contracts\\\\Translation\\\\TranslatorInterface\\) does not accept Symfony\\\\Contracts\\\\Translation\\\\TranslatorInterface\\|null\\.$#"
count: 1
Expand Down
160 changes: 160 additions & 0 deletions src/contracts/Output/Exceptions/AbstractExceptionVisitor.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
<?php

/**
* @copyright Copyright (C) Ibexa AS. All rights reserved.
* @license For full copyright and license information view LICENSE file distributed with this source code.
*/
namespace Ibexa\Contracts\Rest\Output\Exceptions;

use Ibexa\Contracts\Rest\Output\Generator;
use Ibexa\Contracts\Rest\Output\ValueObjectVisitor;
use Ibexa\Contracts\Rest\Output\Visitor;
use Ibexa\Core\Base\Translatable;
use JMS\TranslationBundle\Annotation\Desc;
use JMS\TranslationBundle\Annotation\Ignore;
use Symfony\Contracts\Translation\TranslatorInterface;

abstract class AbstractExceptionVisitor extends ValueObjectVisitor
{
/**
* Mapping of HTTP status codes to their respective error messages.
*
* @var array<int, string>
*/
protected static $httpStatusCodes = [
400 => 'Bad Request',
401 => 'Unauthorized',
402 => 'Payment Required',
403 => 'Forbidden',
404 => 'Not Found',
405 => 'Method Not Allowed',
406 => 'Not Acceptable',
407 => 'Proxy Authentication Required',
408 => 'Request Time-out',
409 => 'Conflict',
410 => 'Gone',
411 => 'Length Required',
412 => 'Precondition Failed',
413 => 'Request Entity Too Large',
414 => 'Request-URI Too Long',
415 => 'Unsupported Media Type',
416 => 'Requested range not satisfiable',
417 => 'Expectation Failed',
418 => "I'm a teapot",
421 => 'There are too many connections from your internet address',
422 => 'Unprocessable Entity',
423 => 'Locked',
424 => 'Failed Dependency',
425 => 'Unordered Collection',
426 => 'Upgrade Required',
500 => 'Internal Server Error',
501 => 'Not Implemented',
502 => 'Bad Gateway',
503 => 'Service Unavailable',
504 => 'Gateway Time-out',
505 => 'HTTP Version not supported',
506 => 'Variant Also Negotiates',
507 => 'Insufficient Storage',
509 => 'Bandwidth Limit Exceeded',
510 => 'Not Extended',
];

/**
* Returns HTTP status code.
*
* @return int
*/
protected function getStatus()
{
return 500;
}

/**
* @param \Exception $data
*
* @return void
*/
public function visit(Visitor $visitor, Generator $generator, $data)
{
$generator->startObjectElement('ErrorMessage');

$visitor->setHeader('Content-Type', $generator->getMediaType('ErrorMessage'));

$statusCode = $this->generateErrorCode($generator, $visitor, $data);

$errorMessage = $this->getErrorMessage($data, $statusCode);
$generator->valueElement('errorMessage', $errorMessage);

$errorDescription = $this->getErrorDescription($data, $statusCode);
$generator->valueElement('errorDescription', $errorDescription);

if ($this->canDisplayExceptionTrace()) {
$generator->valueElement('trace', $data->getTraceAsString());
$generator->valueElement('file', $data->getFile());
$generator->valueElement('line', $data->getLine());
}

$previous = $data->getPrevious();
if ($previous !== null && $this->canDisplayPreviousException()) {
$generator->startObjectElement('Previous', 'ErrorMessage');
$visitor->visitValueObject($previous);
$generator->endObjectElement('Previous');
}

$generator->endObjectElement('ErrorMessage');
}

protected function generateErrorCode(Generator $generator, Visitor $visitor, \Exception $e): int
{
$statusCode = $this->getStatus();
$visitor->setStatus($statusCode);

$generator->valueElement('errorCode', $statusCode);

return $statusCode;
}

protected function getErrorMessage(\Exception $data, int $statusCode): string
{
return static::$httpStatusCodes[$statusCode] ?? static::$httpStatusCodes[500];
}

protected function getErrorDescription(\Exception $data, int $statusCode): string
{
$translator = $this->getTranslator();
if ($statusCode < 500 || $this->canDisplayExceptionMessage()) {
$errorDescription = $data instanceof Translatable && $translator
? /** @Ignore */
$translator->trans($data->getMessageTemplate(), $data->getParameters(), 'ibexa_repository_exceptions')
: $data->getMessage();
} else {
// Do not leak any file paths and sensitive data on production environments
$errorDescription = $translator
? /** @Desc("An error has occurred. Please try again later or contact your Administrator.") */
$translator->trans('non_verbose_error', [], 'ibexa_repository_exceptions')
: 'An error has occurred. Please try again later or contact your Administrator.';
}

return $errorDescription;
}

protected function getTranslator(): ?TranslatorInterface
{
return null;
}

protected function canDisplayExceptionTrace(): bool
{
return false;
}

protected function canDisplayPreviousException(): bool
{
return false;
}

protected function canDisplayExceptionMessage(): bool
{
return false;
}
}
120 changes: 12 additions & 108 deletions src/lib/Server/Output/ValueObjectVisitor/Exception.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,13 @@
*/
namespace Ibexa\Rest\Server\Output\ValueObjectVisitor;

use Ibexa\Contracts\Rest\Output\Generator;
use Ibexa\Contracts\Rest\Output\ValueObjectVisitor;
use Ibexa\Contracts\Rest\Output\Visitor;
use Ibexa\Core\Base\Translatable;
use JMS\TranslationBundle\Annotation\Desc;
use JMS\TranslationBundle\Annotation\Ignore;
use Ibexa\Contracts\Rest\Output\Exceptions\AbstractExceptionVisitor;
use Symfony\Contracts\Translation\TranslatorInterface;

/**
* Exception value object visitor.
*/
class Exception extends ValueObjectVisitor
class Exception extends AbstractExceptionVisitor
{
/**
* Is debug mode enabled?
Expand All @@ -26,49 +21,6 @@ class Exception extends ValueObjectVisitor
*/
protected $debug = false;

/**
* Mapping of HTTP status codes to their respective error messages.
*
* @var array
*/
protected static $httpStatusCodes = [
400 => 'Bad Request',
401 => 'Unauthorized',
402 => 'Payment Required',
403 => 'Forbidden',
404 => 'Not Found',
405 => 'Method Not Allowed',
406 => 'Not Acceptable',
407 => 'Proxy Authentication Required',
408 => 'Request Time-out',
409 => 'Conflict',
410 => 'Gone',
411 => 'Length Required',
412 => 'Precondition Failed',
413 => 'Request Entity Too Large',
414 => 'Request-URI Too Long',
415 => 'Unsupported Media Type',
416 => 'Requested range not satisfiable',
417 => 'Expectation Failed',
418 => "I'm a teapot",
421 => 'There are too many connections from your internet address',
422 => 'Unprocessable Entity',
423 => 'Locked',
424 => 'Failed Dependency',
425 => 'Unordered Collection',
426 => 'Upgrade Required',
500 => 'Internal Server Error',
501 => 'Not Implemented',
502 => 'Bad Gateway',
503 => 'Service Unavailable',
504 => 'Gateway Time-out',
505 => 'HTTP Version not supported',
506 => 'Variant Also Negotiates',
507 => 'Insufficient Storage',
509 => 'Bandwidth Limit Exceeded',
510 => 'Not Extended',
];

/** @var \Symfony\Contracts\Translation\TranslatorInterface */
protected $translator;

Expand All @@ -84,72 +36,24 @@ public function __construct($debug = false, ?TranslatorInterface $translator = n
$this->translator = $translator;
}

/**
* Returns HTTP status code.
*
* @return int
*/
protected function getStatus()
protected function getTranslator(): ?TranslatorInterface
{
return 500;
return $this->translator;
}

/**
* Visit struct returned by controllers.
*
* @param \Ibexa\Contracts\Rest\Output\Visitor $visitor
* @param \Ibexa\Contracts\Rest\Output\Generator $generator
* @param \Exception $data
*/
public function visit(Visitor $visitor, Generator $generator, $data)
protected function canDisplayExceptionMessage(): bool
{
$generator->startObjectElement('ErrorMessage');

$visitor->setHeader('Content-Type', $generator->getMediaType('ErrorMessage'));

$statusCode = $this->generateErrorCode($generator, $visitor, $data);

$generator->valueElement(
'errorMessage',
static::$httpStatusCodes[$statusCode] ?? static::$httpStatusCodes[500]
);

if ($this->debug || $statusCode < 500) {
$errorDescription = $data instanceof Translatable && $this->translator
? /** @Ignore */ $this->translator->trans($data->getMessageTemplate(), $data->getParameters(), 'ibexa_repository_exceptions')
: $data->getMessage();
} else {
// Do not leak any file paths and sensitive data on production environments
$errorDescription = $this->translator
? /** @Desc("An error has occurred. Please try again later or contact your Administrator.") */ $this->translator->trans('non_verbose_error', [], 'ibexa_repository_exceptions')
: 'An error has occurred. Please try again later or contact your Administrator.';
}

$generator->valueElement('errorDescription', $errorDescription);

if ($this->debug) {
$generator->valueElement('trace', $data->getTraceAsString());
$generator->valueElement('file', $data->getFile());
$generator->valueElement('line', $data->getLine());
}

if ($previous = $data->getPrevious()) {
$generator->startObjectElement('Previous', 'ErrorMessage');
$visitor->visitValueObject($previous);
$generator->endObjectElement('Previous');
}

$generator->endObjectElement('ErrorMessage');
return $this->debug;
}

protected function generateErrorCode(Generator $generator, Visitor $visitor, \Exception $e): int
protected function canDisplayExceptionTrace(): bool
{
$statusCode = $this->getStatus();
$visitor->setStatus($statusCode);

$generator->valueElement('errorCode', $statusCode);
return $this->debug;
}

return $statusCode;
protected function canDisplayPreviousException(): bool
{
return true;
}
}

Expand Down

0 comments on commit fd2cbf5

Please sign in to comment.