ErrorProcessor.php 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325
  1. <?php
  2. /**
  3. * Copyright © Magento, Inc. All rights reserved.
  4. * See COPYING.txt for license details.
  5. */
  6. namespace Magento\Framework\Webapi;
  7. use Magento\Framework\App\Filesystem\DirectoryList;
  8. use Magento\Framework\App\ObjectManager;
  9. use Magento\Framework\App\State;
  10. use Magento\Framework\Exception\AggregateExceptionInterface;
  11. use Magento\Framework\Exception\AuthenticationException;
  12. use Magento\Framework\Exception\AuthorizationException;
  13. use Magento\Framework\Exception\LocalizedException;
  14. use Magento\Framework\Exception\NoSuchEntityException;
  15. use Magento\Framework\Phrase;
  16. use Magento\Framework\Serialize\Serializer\Json;
  17. use Magento\Framework\Webapi\Exception as WebapiException;
  18. /**
  19. * Helper for errors processing.
  20. *
  21. * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
  22. * @api
  23. * @since 100.0.2
  24. */
  25. class ErrorProcessor
  26. {
  27. const DEFAULT_SHUTDOWN_FUNCTION = 'apiShutdownFunction';
  28. const DEFAULT_ERROR_HTTP_CODE = 500;
  29. const DEFAULT_RESPONSE_CHARSET = 'UTF-8';
  30. const INTERNAL_SERVER_ERROR_MSG = 'Internal Error. Details are available in Magento log file. Report ID: %s';
  31. /**#@+
  32. * Error data representation formats.
  33. */
  34. const DATA_FORMAT_JSON = 'json';
  35. const DATA_FORMAT_XML = 'xml';
  36. /**#@-*/
  37. /**#@-*/
  38. protected $encoder;
  39. /**
  40. * @var \Magento\Framework\App\State
  41. */
  42. protected $_appState;
  43. /**
  44. * @var \Psr\Log\LoggerInterface
  45. */
  46. protected $_logger;
  47. /**
  48. * Filesystem instance
  49. *
  50. * @var \Magento\Framework\Filesystem
  51. */
  52. protected $_filesystem;
  53. /**
  54. * @var \Magento\Framework\Filesystem\Directory\Write
  55. */
  56. protected $directoryWrite;
  57. /**
  58. * Instance of serializer.
  59. *
  60. * @var Json
  61. */
  62. private $serializer;
  63. /**
  64. * @param \Magento\Framework\Json\Encoder $encoder
  65. * @param \Magento\Framework\App\State $appState
  66. * @param \Psr\Log\LoggerInterface $logger
  67. * @param \Magento\Framework\Filesystem $filesystem
  68. * @param Json|null $serializer
  69. */
  70. public function __construct(
  71. \Magento\Framework\Json\Encoder $encoder,
  72. \Magento\Framework\App\State $appState,
  73. \Psr\Log\LoggerInterface $logger,
  74. \Magento\Framework\Filesystem $filesystem,
  75. Json $serializer = null
  76. ) {
  77. $this->encoder = $encoder;
  78. $this->_appState = $appState;
  79. $this->_logger = $logger;
  80. $this->_filesystem = $filesystem;
  81. $this->directoryWrite = $this->_filesystem->getDirectoryWrite(DirectoryList::VAR_DIR);
  82. $this->serializer = $serializer ?: ObjectManager::getInstance()->get(Json::class);
  83. $this->registerShutdownFunction();
  84. }
  85. /**
  86. * Mask actual exception for security reasons in case when it should not be exposed to API clients.
  87. *
  88. * Convert any exception into \Magento\Framework\Webapi\Exception.
  89. *
  90. * @param \Exception $exception Exception to convert to a WebAPI exception
  91. *
  92. * @return WebapiException
  93. */
  94. public function maskException(\Exception $exception)
  95. {
  96. $isDevMode = $this->_appState->getMode() === State::MODE_DEVELOPER;
  97. $stackTrace = $isDevMode ? $exception->getTraceAsString() : null;
  98. if ($exception instanceof WebapiException) {
  99. $maskedException = $exception;
  100. } elseif ($exception instanceof LocalizedException) {
  101. // Map HTTP codes for LocalizedExceptions according to exception type
  102. if ($exception instanceof NoSuchEntityException) {
  103. $httpCode = WebapiException::HTTP_NOT_FOUND;
  104. } elseif (($exception instanceof AuthorizationException)
  105. || ($exception instanceof AuthenticationException)
  106. ) {
  107. $httpCode = WebapiException::HTTP_UNAUTHORIZED;
  108. } else {
  109. // Input, Expired, InvalidState exceptions will fall to here
  110. $httpCode = WebapiException::HTTP_BAD_REQUEST;
  111. }
  112. if ($exception instanceof AggregateExceptionInterface) {
  113. $errors = $exception->getErrors();
  114. } else {
  115. $errors = null;
  116. }
  117. $maskedException = new WebapiException(
  118. new Phrase($exception->getRawMessage()),
  119. $exception->getCode(),
  120. $httpCode,
  121. $exception->getParameters(),
  122. get_class($exception),
  123. $errors,
  124. $stackTrace
  125. );
  126. } else {
  127. $message = $exception->getMessage();
  128. $code = $exception->getCode();
  129. //if not in Dev mode, make sure the message and code is masked for unanticipated exceptions
  130. if (!$isDevMode) {
  131. /** Log information about actual exception */
  132. $reportId = $this->_critical($exception);
  133. $message = sprintf(self::INTERNAL_SERVER_ERROR_MSG, $reportId);
  134. $code = 0;
  135. }
  136. $maskedException = new WebapiException(
  137. new Phrase($message),
  138. $code,
  139. WebapiException::HTTP_INTERNAL_ERROR,
  140. [],
  141. '',
  142. null,
  143. $stackTrace
  144. );
  145. }
  146. return $maskedException;
  147. }
  148. /**
  149. * Process API exception.
  150. *
  151. * Create report if not in developer mode and render error to send correct API response.
  152. *
  153. * @param \Exception $exception
  154. * @param int $httpCode
  155. * @return void
  156. * @SuppressWarnings(PHPMD.ExitExpression)
  157. */
  158. public function renderException(\Exception $exception, $httpCode = self::DEFAULT_ERROR_HTTP_CODE)
  159. {
  160. if ($this->_appState->getMode() == State::MODE_DEVELOPER ||
  161. $exception instanceof \Magento\Framework\Webapi\Exception
  162. ) {
  163. $this->renderErrorMessage($exception->getMessage(), $exception->getTraceAsString(), $httpCode);
  164. } else {
  165. $reportId = $this->_critical($exception);
  166. $this->renderErrorMessage(
  167. new Phrase('Internal Error. Details are available in Magento log file. Report ID: %1', $reportId),
  168. 'Trace is not available.',
  169. $httpCode
  170. );
  171. }
  172. exit;
  173. }
  174. /**
  175. * Log information about exception to exception log.
  176. *
  177. * @param \Exception $exception
  178. * @return string
  179. */
  180. protected function _critical(\Exception $exception)
  181. {
  182. $reportId = uniqid("webapi-");
  183. $message = "Report ID: {$reportId}; Message: {$exception->getMessage()}";
  184. $code = $exception->getCode();
  185. $exception = new \Exception($message, $code, $exception);
  186. $this->_logger->critical($exception);
  187. return $reportId;
  188. }
  189. /**
  190. * Render error according to mime type.
  191. *
  192. * @param string $errorMessage
  193. * @param string $trace
  194. * @param int $httpCode
  195. * @return void
  196. */
  197. public function renderErrorMessage(
  198. $errorMessage,
  199. $trace = 'Trace is not available.',
  200. $httpCode = self::DEFAULT_ERROR_HTTP_CODE
  201. ) {
  202. if (isset($_SERVER['HTTP_ACCEPT']) && strstr($_SERVER['HTTP_ACCEPT'], 'xml')) {
  203. $output = $this->_formatError($errorMessage, $trace, $httpCode, self::DATA_FORMAT_XML);
  204. $mimeType = 'application/xml';
  205. } else {
  206. /** Default format is JSON */
  207. $output = $this->_formatError($errorMessage, $trace, $httpCode, self::DATA_FORMAT_JSON);
  208. $mimeType = 'application/json';
  209. }
  210. if (!headers_sent()) {
  211. header('HTTP/1.1 ' . ($httpCode ? $httpCode : self::DEFAULT_ERROR_HTTP_CODE));
  212. header('Content-Type: ' . $mimeType . '; charset=' . self::DEFAULT_RESPONSE_CHARSET);
  213. }
  214. echo $output;
  215. }
  216. /**
  217. * Format error data according to required format.
  218. *
  219. * @param string $errorMessage
  220. * @param string $trace
  221. * @param int $httpCode
  222. * @param string $format
  223. * @return array|string
  224. */
  225. protected function _formatError($errorMessage, $trace, $httpCode, $format)
  226. {
  227. $errorData = [];
  228. $message = ['code' => $httpCode, 'message' => $errorMessage];
  229. $isDeveloperMode = $this->_appState->getMode() == State::MODE_DEVELOPER;
  230. if ($isDeveloperMode) {
  231. $message['trace'] = $trace;
  232. }
  233. $errorData['messages']['error'][] = $message;
  234. switch ($format) {
  235. case self::DATA_FORMAT_JSON:
  236. $errorData = $this->encoder->encode($errorData);
  237. break;
  238. case self::DATA_FORMAT_XML:
  239. $errorData = '<?xml version="1.0"?>'
  240. . '<error>'
  241. . '<messages>'
  242. . '<error>'
  243. . '<data_item>'
  244. . '<code>' . $httpCode . '</code>'
  245. . '<message><![CDATA[' . $errorMessage . ']]></message>'
  246. . ($isDeveloperMode ? '<trace><![CDATA[' . $trace . ']]></trace>' : '')
  247. . '</data_item>'
  248. . '</error>'
  249. . '</messages>'
  250. . '</error>';
  251. break;
  252. }
  253. return $errorData;
  254. }
  255. /**
  256. * Declare web API-specific shutdown function.
  257. *
  258. * @return $this
  259. */
  260. public function registerShutdownFunction()
  261. {
  262. register_shutdown_function([$this, self::DEFAULT_SHUTDOWN_FUNCTION]);
  263. return $this;
  264. }
  265. /**
  266. * Function to catch errors, that has not been caught by the user error dispatcher function.
  267. *
  268. * @return void
  269. */
  270. public function apiShutdownFunction()
  271. {
  272. $fatalErrorFlag = E_ERROR | E_USER_ERROR | E_PARSE | E_CORE_ERROR | E_COMPILE_ERROR | E_RECOVERABLE_ERROR;
  273. $error = error_get_last();
  274. if ($error && $error['type'] & $fatalErrorFlag) {
  275. $errorMessage = "Fatal Error: '{$error['message']}' in '{$error['file']}' on line {$error['line']}";
  276. $reportId = $this->_saveFatalErrorReport($errorMessage);
  277. if ($this->_appState->getMode() == State::MODE_DEVELOPER) {
  278. $this->renderErrorMessage($errorMessage);
  279. } else {
  280. $this->renderErrorMessage(
  281. new Phrase('Server internal error. See details in report api/%1', [$reportId])
  282. );
  283. }
  284. }
  285. }
  286. /**
  287. * Log information about fatal error.
  288. *
  289. * @param string $reportData
  290. * @return string
  291. */
  292. protected function _saveFatalErrorReport($reportData)
  293. {
  294. $this->directoryWrite->create('report/api');
  295. $reportId = abs((int)(microtime(true) * random_int(100, 1000)));
  296. $this->directoryWrite->writeFile('report/api/' . $reportId, $this->serializer->serialize($reportData));
  297. return $reportId;
  298. }
  299. }