Response.php 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369
  1. <?php
  2. namespace OAuth2;
  3. /**
  4. * Class to handle OAuth2 Responses in a graceful way. Use this interface
  5. * to output the proper OAuth2 responses.
  6. *
  7. * @see OAuth2\ResponseInterface
  8. *
  9. * This class borrows heavily from the Symfony2 Framework and is part of the symfony package
  10. * @see Symfony\Component\HttpFoundation\Request (https://github.com/symfony/symfony)
  11. */
  12. class Response implements ResponseInterface
  13. {
  14. public $version;
  15. protected $statusCode = 200;
  16. protected $statusText;
  17. protected $parameters = array();
  18. protected $httpHeaders = array();
  19. public static $statusTexts = array(
  20. 100 => 'Continue',
  21. 101 => 'Switching Protocols',
  22. 200 => 'OK',
  23. 201 => 'Created',
  24. 202 => 'Accepted',
  25. 203 => 'Non-Authoritative Information',
  26. 204 => 'No Content',
  27. 205 => 'Reset Content',
  28. 206 => 'Partial Content',
  29. 300 => 'Multiple Choices',
  30. 301 => 'Moved Permanently',
  31. 302 => 'Found',
  32. 303 => 'See Other',
  33. 304 => 'Not Modified',
  34. 305 => 'Use Proxy',
  35. 307 => 'Temporary Redirect',
  36. 400 => 'Bad Request',
  37. 401 => 'Unauthorized',
  38. 402 => 'Payment Required',
  39. 403 => 'Forbidden',
  40. 404 => 'Not Found',
  41. 405 => 'Method Not Allowed',
  42. 406 => 'Not Acceptable',
  43. 407 => 'Proxy Authentication Required',
  44. 408 => 'Request Timeout',
  45. 409 => 'Conflict',
  46. 410 => 'Gone',
  47. 411 => 'Length Required',
  48. 412 => 'Precondition Failed',
  49. 413 => 'Request Entity Too Large',
  50. 414 => 'Request-URI Too Long',
  51. 415 => 'Unsupported Media Type',
  52. 416 => 'Requested Range Not Satisfiable',
  53. 417 => 'Expectation Failed',
  54. 418 => 'I\'m a teapot',
  55. 500 => 'Internal Server Error',
  56. 501 => 'Not Implemented',
  57. 502 => 'Bad Gateway',
  58. 503 => 'Service Unavailable',
  59. 504 => 'Gateway Timeout',
  60. 505 => 'HTTP Version Not Supported',
  61. );
  62. public function __construct($parameters = array(), $statusCode = 200, $headers = array())
  63. {
  64. $this->setParameters($parameters);
  65. $this->setStatusCode($statusCode);
  66. $this->setHttpHeaders($headers);
  67. $this->version = '1.1';
  68. }
  69. /**
  70. * Converts the response object to string containing all headers and the response content.
  71. *
  72. * @return string The response with headers and content
  73. */
  74. public function __toString()
  75. {
  76. $headers = array();
  77. foreach ($this->httpHeaders as $name => $value) {
  78. $headers[$name] = (array) $value;
  79. }
  80. return
  81. sprintf('HTTP/%s %s %s', $this->version, $this->statusCode, $this->statusText)."\r\n".
  82. $this->getHttpHeadersAsString($headers)."\r\n".
  83. $this->getResponseBody();
  84. }
  85. /**
  86. * Returns the build header line.
  87. *
  88. * @param string $name The header name
  89. * @param string $value The header value
  90. *
  91. * @return string The built header line
  92. */
  93. protected function buildHeader($name, $value)
  94. {
  95. return sprintf("%s: %s\n", $name, $value);
  96. }
  97. public function getStatusCode()
  98. {
  99. return $this->statusCode;
  100. }
  101. public function setStatusCode($statusCode, $text = null)
  102. {
  103. $this->statusCode = (int) $statusCode;
  104. if ($this->isInvalid()) {
  105. throw new \InvalidArgumentException(sprintf('The HTTP status code "%s" is not valid.', $statusCode));
  106. }
  107. $this->statusText = false === $text ? '' : (null === $text ? self::$statusTexts[$this->statusCode] : $text);
  108. }
  109. public function getStatusText()
  110. {
  111. return $this->statusText;
  112. }
  113. public function getParameters()
  114. {
  115. return $this->parameters;
  116. }
  117. public function setParameters(array $parameters)
  118. {
  119. $this->parameters = $parameters;
  120. }
  121. public function addParameters(array $parameters)
  122. {
  123. $this->parameters = array_merge($this->parameters, $parameters);
  124. }
  125. public function getParameter($name, $default = null)
  126. {
  127. return isset($this->parameters[$name]) ? $this->parameters[$name] : $default;
  128. }
  129. public function setParameter($name, $value)
  130. {
  131. $this->parameters[$name] = $value;
  132. }
  133. public function setHttpHeaders(array $httpHeaders)
  134. {
  135. $this->httpHeaders = $httpHeaders;
  136. }
  137. public function setHttpHeader($name, $value)
  138. {
  139. $this->httpHeaders[$name] = $value;
  140. }
  141. public function addHttpHeaders(array $httpHeaders)
  142. {
  143. $this->httpHeaders = array_merge($this->httpHeaders, $httpHeaders);
  144. }
  145. public function getHttpHeaders()
  146. {
  147. return $this->httpHeaders;
  148. }
  149. public function getHttpHeader($name, $default = null)
  150. {
  151. return isset($this->httpHeaders[$name]) ? $this->httpHeaders[$name] : $default;
  152. }
  153. public function getResponseBody($format = 'json')
  154. {
  155. switch ($format) {
  156. case 'json':
  157. return json_encode($this->parameters);
  158. case 'xml':
  159. // this only works for single-level arrays
  160. $xml = new \SimpleXMLElement('<response/>');
  161. foreach ($this->parameters as $key => $param) {
  162. $xml->addChild($key, $param);
  163. }
  164. return $xml->asXML();
  165. }
  166. throw new \InvalidArgumentException(sprintf('The format %s is not supported', $format));
  167. }
  168. public function send($format = 'json')
  169. {
  170. // headers have already been sent by the developer
  171. if (headers_sent()) {
  172. return;
  173. }
  174. switch ($format) {
  175. case 'json':
  176. $this->setHttpHeader('Content-Type', 'application/json');
  177. break;
  178. case 'xml':
  179. $this->setHttpHeader('Content-Type', 'text/xml');
  180. break;
  181. }
  182. // status
  183. header(sprintf('HTTP/%s %s %s', $this->version, $this->statusCode, $this->statusText));
  184. foreach ($this->getHttpHeaders() as $name => $header) {
  185. header(sprintf('%s: %s', $name, $header));
  186. }
  187. echo $this->getResponseBody($format);
  188. }
  189. public function setError($statusCode, $error, $errorDescription = null, $errorUri = null)
  190. {
  191. $parameters = array(
  192. 'error' => $error,
  193. 'error_description' => $errorDescription,
  194. );
  195. if (!is_null($errorUri)) {
  196. if (strlen($errorUri) > 0 && $errorUri[0] == '#') {
  197. // we are referencing an oauth bookmark (for brevity)
  198. $errorUri = 'http://tools.ietf.org/html/rfc6749' . $errorUri;
  199. }
  200. $parameters['error_uri'] = $errorUri;
  201. }
  202. $httpHeaders = array(
  203. 'Cache-Control' => 'no-store'
  204. );
  205. $this->setStatusCode($statusCode);
  206. $this->addParameters($parameters);
  207. $this->addHttpHeaders($httpHeaders);
  208. if (!$this->isClientError() && !$this->isServerError()) {
  209. throw new \InvalidArgumentException(sprintf('The HTTP status code is not an error ("%s" given).', $statusCode));
  210. }
  211. }
  212. public function setRedirect($statusCode, $url, $state = null, $error = null, $errorDescription = null, $errorUri = null)
  213. {
  214. if (empty($url)) {
  215. throw new \InvalidArgumentException('Cannot redirect to an empty URL.');
  216. }
  217. $parameters = array();
  218. if (!is_null($state)) {
  219. $parameters['state'] = $state;
  220. }
  221. if (!is_null($error)) {
  222. $this->setError(400, $error, $errorDescription, $errorUri);
  223. }
  224. $this->setStatusCode($statusCode);
  225. $this->addParameters($parameters);
  226. if (count($this->parameters) > 0) {
  227. // add parameters to URL redirection
  228. $parts = parse_url($url);
  229. $sep = isset($parts['query']) && count($parts['query']) > 0 ? '&' : '?';
  230. $url .= $sep . http_build_query($this->parameters);
  231. }
  232. $this->addHttpHeaders(array('Location' => $url));
  233. if (!$this->isRedirection()) {
  234. throw new \InvalidArgumentException(sprintf('The HTTP status code is not a redirect ("%s" given).', $statusCode));
  235. }
  236. }
  237. // http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html
  238. /**
  239. * @return Boolean
  240. *
  241. * @api
  242. */
  243. public function isInvalid()
  244. {
  245. return $this->statusCode < 100 || $this->statusCode >= 600;
  246. }
  247. /**
  248. * @return Boolean
  249. *
  250. * @api
  251. */
  252. public function isInformational()
  253. {
  254. return $this->statusCode >= 100 && $this->statusCode < 200;
  255. }
  256. /**
  257. * @return Boolean
  258. *
  259. * @api
  260. */
  261. public function isSuccessful()
  262. {
  263. return $this->statusCode >= 200 && $this->statusCode < 300;
  264. }
  265. /**
  266. * @return Boolean
  267. *
  268. * @api
  269. */
  270. public function isRedirection()
  271. {
  272. return $this->statusCode >= 300 && $this->statusCode < 400;
  273. }
  274. /**
  275. * @return Boolean
  276. *
  277. * @api
  278. */
  279. public function isClientError()
  280. {
  281. return $this->statusCode >= 400 && $this->statusCode < 500;
  282. }
  283. /**
  284. * @return Boolean
  285. *
  286. * @api
  287. */
  288. public function isServerError()
  289. {
  290. return $this->statusCode >= 500 && $this->statusCode < 600;
  291. }
  292. /*
  293. * Functions from Symfony2 HttpFoundation - output pretty header
  294. */
  295. private function getHttpHeadersAsString($headers)
  296. {
  297. if (count($headers) == 0) {
  298. return '';
  299. }
  300. $max = max(array_map('strlen', array_keys($headers))) + 1;
  301. $content = '';
  302. ksort($headers);
  303. foreach ($headers as $name => $values) {
  304. foreach ($values as $value) {
  305. $content .= sprintf("%-{$max}s %s\r\n", $this->beautifyHeaderName($name).':', $value);
  306. }
  307. }
  308. return $content;
  309. }
  310. private function beautifyHeaderName($name)
  311. {
  312. return preg_replace_callback('/\-(.)/', array($this, 'beautifyCallback'), ucfirst($name));
  313. }
  314. private function beautifyCallback($match)
  315. {
  316. return '-'.strtoupper($match[1]);
  317. }
  318. }