1<?php 2 3namespace Drupal\jsonapi\Normalizer; 4 5use Drupal\Core\Cache\CacheableMetadata; 6use Drupal\Core\Session\AccountInterface; 7use Drupal\jsonapi\Normalizer\Value\HttpExceptionNormalizerValue; 8use Symfony\Component\HttpFoundation\Response; 9use Symfony\Component\HttpKernel\Exception\HttpException; 10 11/** 12 * Normalizes an HttpException in compliance with the JSON:API specification. 13 * 14 * @internal JSON:API maintains no PHP API since its API is the HTTP API. This 15 * class may change at any time and this will break any dependencies on it. 16 * 17 * @see https://www.drupal.org/project/drupal/issues/3032787 18 * @see jsonapi.api.php 19 * 20 * @see http://jsonapi.org/format/#error-objects 21 */ 22class HttpExceptionNormalizer extends NormalizerBase { 23 24 /** 25 * The interface or class that this Normalizer supports. 26 * 27 * @var string 28 */ 29 protected $supportedInterfaceOrClass = HttpException::class; 30 31 /** 32 * The current user making the request. 33 * 34 * @var \Drupal\Core\Session\AccountInterface 35 */ 36 protected $currentUser; 37 38 /** 39 * HttpExceptionNormalizer constructor. 40 * 41 * @param \Drupal\Core\Session\AccountInterface $current_user 42 * The current user. 43 */ 44 public function __construct(AccountInterface $current_user) { 45 $this->currentUser = $current_user; 46 } 47 48 /** 49 * {@inheritdoc} 50 */ 51 public function normalize($object, $format = NULL, array $context = []) { 52 $cacheability = new CacheableMetadata(); 53 $cacheability->addCacheableDependency($object); 54 return new HttpExceptionNormalizerValue($cacheability, static::rasterizeValueRecursive($this->buildErrorObjects($object))); 55 } 56 57 /** 58 * Builds the normalized JSON:API error objects for the response. 59 * 60 * @param \Symfony\Component\HttpKernel\Exception\HttpException $exception 61 * The Exception. 62 * 63 * @return array 64 * The error objects to include in the response. 65 */ 66 protected function buildErrorObjects(HttpException $exception) { 67 $error = []; 68 $status_code = $exception->getStatusCode(); 69 if (!empty(Response::$statusTexts[$status_code])) { 70 $error['title'] = Response::$statusTexts[$status_code]; 71 } 72 $error += [ 73 'status' => (string) $status_code, 74 'detail' => $exception->getMessage(), 75 ]; 76 $error['links']['via']['href'] = \Drupal::request()->getUri(); 77 // Provide an "info" link by default: if the exception carries a single 78 // "Link" header, use that, otherwise fall back to the HTTP spec section 79 // covering the exception's status code. 80 $headers = $exception->getHeaders(); 81 if (isset($headers['Link']) && !is_array($headers['Link'])) { 82 $error['links']['info']['href'] = $headers['Link']; 83 } 84 elseif ($info_url = $this->getInfoUrl($status_code)) { 85 $error['links']['info']['href'] = $info_url; 86 } 87 // Exceptions thrown without an explicitly defined code get assigned zero by 88 // default. Since this is no helpful information, omit it. 89 if ($exception->getCode() !== 0) { 90 $error['code'] = (string) $exception->getCode(); 91 } 92 if ($this->currentUser->hasPermission('access site reports')) { 93 // The following information may contain sensitive information. Only show 94 // it to authorized users. 95 $error['source'] = [ 96 'file' => $exception->getFile(), 97 'line' => $exception->getLine(), 98 ]; 99 $error['meta'] = [ 100 'exception' => (string) $exception, 101 'trace' => $exception->getTrace(), 102 ]; 103 } 104 105 return [$error]; 106 } 107 108 /** 109 * Return a string to the common problem type. 110 * 111 * @return string|null 112 * URL pointing to the specific RFC-2616 section. Or NULL if it is an HTTP 113 * status code that is defined in another RFC. 114 * 115 * @see https://www.drupal.org/project/drupal/issues/2832211#comment-11826234 116 * 117 * @internal 118 */ 119 public static function getInfoUrl($status_code) { 120 // Depending on the error code we'll return a different URL. 121 $url = 'http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html'; 122 $sections = [ 123 '100' => '#sec10.1.1', 124 '101' => '#sec10.1.2', 125 '200' => '#sec10.2.1', 126 '201' => '#sec10.2.2', 127 '202' => '#sec10.2.3', 128 '203' => '#sec10.2.4', 129 '204' => '#sec10.2.5', 130 '205' => '#sec10.2.6', 131 '206' => '#sec10.2.7', 132 '300' => '#sec10.3.1', 133 '301' => '#sec10.3.2', 134 '302' => '#sec10.3.3', 135 '303' => '#sec10.3.4', 136 '304' => '#sec10.3.5', 137 '305' => '#sec10.3.6', 138 '307' => '#sec10.3.8', 139 '400' => '#sec10.4.1', 140 '401' => '#sec10.4.2', 141 '402' => '#sec10.4.3', 142 '403' => '#sec10.4.4', 143 '404' => '#sec10.4.5', 144 '405' => '#sec10.4.6', 145 '406' => '#sec10.4.7', 146 '407' => '#sec10.4.8', 147 '408' => '#sec10.4.9', 148 '409' => '#sec10.4.10', 149 '410' => '#sec10.4.11', 150 '411' => '#sec10.4.12', 151 '412' => '#sec10.4.13', 152 '413' => '#sec10.4.14', 153 '414' => '#sec10.4.15', 154 '415' => '#sec10.4.16', 155 '416' => '#sec10.4.17', 156 '417' => '#sec10.4.18', 157 '500' => '#sec10.5.1', 158 '501' => '#sec10.5.2', 159 '502' => '#sec10.5.3', 160 '503' => '#sec10.5.4', 161 '504' => '#sec10.5.5', 162 '505' => '#sec10.5.6', 163 ]; 164 return empty($sections[$status_code]) ? NULL : $url . $sections[$status_code]; 165 } 166 167} 168