diff --git a/.php-cs-fixer.php b/.php-cs-fixer.php index 4ba28536a..6b793736c 100644 --- a/.php-cs-fixer.php +++ b/.php-cs-fixer.php @@ -11,6 +11,7 @@ $configFactory = new InternalConfigFactory(); $configFactory->withRules([ 'declare_strict_types' => false, + 'phpdoc_no_empty_return' => false, ]); return $configFactory diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index cf9f00da2..0fcc5a72c 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -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 @@ -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 diff --git a/src/contracts/Output/Exceptions/AbstractExceptionVisitor.php b/src/contracts/Output/Exceptions/AbstractExceptionVisitor.php new file mode 100644 index 000000000..47ebac409 --- /dev/null +++ b/src/contracts/Output/Exceptions/AbstractExceptionVisitor.php @@ -0,0 +1,160 @@ + + */ + 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; + } +} diff --git a/src/lib/Server/Output/ValueObjectVisitor/Exception.php b/src/lib/Server/Output/ValueObjectVisitor/Exception.php index 129abe7b4..6069f3392 100644 --- a/src/lib/Server/Output/ValueObjectVisitor/Exception.php +++ b/src/lib/Server/Output/ValueObjectVisitor/Exception.php @@ -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? @@ -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; @@ -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; } }