Response.php 12 KB

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