WebResponse.php 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319
  1. <?php
  2. /**
  3. * Classes used to send headers and cookies back to the user
  4. *
  5. * This program is free software; you can redistribute it and/or modify
  6. * it under the terms of the GNU General Public License as published by
  7. * the Free Software Foundation; either version 2 of the License, or
  8. * (at your option) any later version.
  9. *
  10. * This program is distributed in the hope that it will be useful,
  11. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  12. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  13. * GNU General Public License for more details.
  14. *
  15. * You should have received a copy of the GNU General Public License along
  16. * with this program; if not, write to the Free Software Foundation, Inc.,
  17. * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
  18. * http://www.gnu.org/copyleft/gpl.html
  19. *
  20. * @file
  21. */
  22. /**
  23. * Allow programs to request this object from WebRequest::response()
  24. * and handle all outputting (or lack of outputting) via it.
  25. * @ingroup HTTP
  26. */
  27. class WebResponse {
  28. /** @var array Used to record set cookies, because PHP's setcookie() will
  29. * happily send an identical Set-Cookie to the client.
  30. */
  31. protected static $setCookies = [];
  32. /**
  33. * Output an HTTP header, wrapper for PHP's header()
  34. * @param string $string Header to output
  35. * @param bool $replace Replace current similar header
  36. * @param null|int $http_response_code Forces the HTTP response code to the specified value.
  37. */
  38. public function header( $string, $replace = true, $http_response_code = null ) {
  39. \MediaWiki\HeaderCallback::warnIfHeadersSent();
  40. if ( $http_response_code ) {
  41. header( $string, $replace, $http_response_code );
  42. } else {
  43. header( $string, $replace );
  44. }
  45. }
  46. /**
  47. * Get a response header
  48. * @param string $key The name of the header to get (case insensitive).
  49. * @return string|null The header value (if set); null otherwise.
  50. * @since 1.25
  51. */
  52. public function getHeader( $key ) {
  53. foreach ( headers_list() as $header ) {
  54. list( $name, $val ) = explode( ':', $header, 2 );
  55. if ( !strcasecmp( $name, $key ) ) {
  56. return trim( $val );
  57. }
  58. }
  59. return null;
  60. }
  61. /**
  62. * Output an HTTP status code header
  63. * @since 1.26
  64. * @param int $code Status code
  65. */
  66. public function statusHeader( $code ) {
  67. HttpStatus::header( $code );
  68. }
  69. /**
  70. * Test if headers have been sent
  71. * @since 1.27
  72. * @return bool
  73. */
  74. public function headersSent() {
  75. return headers_sent();
  76. }
  77. /**
  78. * Set the browser cookie
  79. * @param string $name The name of the cookie.
  80. * @param string $value The value to be stored in the cookie.
  81. * @param int|null $expire Unix timestamp (in seconds) when the cookie should expire.
  82. * 0 (the default) causes it to expire $wgCookieExpiration seconds from now.
  83. * null causes it to be a session cookie.
  84. * @param array $options Assoc of additional cookie options:
  85. * prefix: string, name prefix ($wgCookiePrefix)
  86. * domain: string, cookie domain ($wgCookieDomain)
  87. * path: string, cookie path ($wgCookiePath)
  88. * secure: bool, secure attribute ($wgCookieSecure)
  89. * httpOnly: bool, httpOnly attribute ($wgCookieHttpOnly)
  90. * @since 1.22 Replaced $prefix, $domain, and $forceSecure with $options
  91. */
  92. public function setCookie( $name, $value, $expire = 0, $options = [] ) {
  93. global $wgCookiePath, $wgCookiePrefix, $wgCookieDomain;
  94. global $wgCookieSecure, $wgCookieExpiration, $wgCookieHttpOnly;
  95. $options = array_filter( $options, function ( $a ) {
  96. return $a !== null;
  97. } ) + [
  98. 'prefix' => $wgCookiePrefix,
  99. 'domain' => $wgCookieDomain,
  100. 'path' => $wgCookiePath,
  101. 'secure' => $wgCookieSecure,
  102. 'httpOnly' => $wgCookieHttpOnly,
  103. 'raw' => false,
  104. ];
  105. if ( $expire === null ) {
  106. $expire = 0; // Session cookie
  107. } elseif ( $expire == 0 && $wgCookieExpiration != 0 ) {
  108. $expire = time() + $wgCookieExpiration;
  109. }
  110. $func = $options['raw'] ? 'setrawcookie' : 'setcookie';
  111. if ( Hooks::run( 'WebResponseSetCookie', [ &$name, &$value, &$expire, &$options ] ) ) {
  112. $cookie = $options['prefix'] . $name;
  113. $data = [
  114. 'name' => (string)$cookie,
  115. 'value' => (string)$value,
  116. 'expire' => (int)$expire,
  117. 'path' => (string)$options['path'],
  118. 'domain' => (string)$options['domain'],
  119. 'secure' => (bool)$options['secure'],
  120. 'httpOnly' => (bool)$options['httpOnly'],
  121. ];
  122. // Per RFC 6265, key is name + domain + path
  123. $key = "{$data['name']}\n{$data['domain']}\n{$data['path']}";
  124. // If this cookie name was in the request, fake an entry in
  125. // self::$setCookies for it so the deleting check works right.
  126. if ( isset( $_COOKIE[$cookie] ) && !array_key_exists( $key, self::$setCookies ) ) {
  127. self::$setCookies[$key] = [];
  128. }
  129. // PHP deletes if value is the empty string; also, a past expiry is deleting
  130. $deleting = ( $data['value'] === '' || $data['expire'] > 0 && $data['expire'] <= time() );
  131. if ( $deleting && !isset( self::$setCookies[$key] ) ) { // isset( null ) is false
  132. wfDebugLog( 'cookie', 'already deleted ' . $func . ': "' . implode( '", "', $data ) . '"' );
  133. } elseif ( !$deleting && isset( self::$setCookies[$key] ) &&
  134. self::$setCookies[$key] === [ $func, $data ]
  135. ) {
  136. wfDebugLog( 'cookie', 'already set ' . $func . ': "' . implode( '", "', $data ) . '"' );
  137. } else {
  138. wfDebugLog( 'cookie', $func . ': "' . implode( '", "', $data ) . '"' );
  139. if ( call_user_func_array( $func, array_values( $data ) ) ) {
  140. self::$setCookies[$key] = $deleting ? null : [ $func, $data ];
  141. }
  142. }
  143. }
  144. }
  145. /**
  146. * Unset a browser cookie.
  147. * This sets the cookie with an empty value and an expiry set to a time in the past,
  148. * which will cause the browser to remove any cookie with the given name, domain and
  149. * path from its cookie store. Options other than these (and prefix) have no effect.
  150. * @param string $name Cookie name
  151. * @param array $options Cookie options, see {@link setCookie()}
  152. * @since 1.27
  153. */
  154. public function clearCookie( $name, $options = [] ) {
  155. $this->setCookie( $name, '', time() - 31536000 /* 1 year */, $options );
  156. }
  157. /**
  158. * Checks whether this request is performing cookie operations
  159. *
  160. * @return bool
  161. * @since 1.27
  162. */
  163. public function hasCookies() {
  164. return (bool)self::$setCookies;
  165. }
  166. }
  167. /**
  168. * @ingroup HTTP
  169. */
  170. class FauxResponse extends WebResponse {
  171. private $headers;
  172. private $cookies = [];
  173. private $code;
  174. /**
  175. * Stores a HTTP header
  176. * @param string $string Header to output
  177. * @param bool $replace Replace current similar header
  178. * @param null|int $http_response_code Forces the HTTP response code to the specified value.
  179. */
  180. public function header( $string, $replace = true, $http_response_code = null ) {
  181. if ( substr( $string, 0, 5 ) == 'HTTP/' ) {
  182. $parts = explode( ' ', $string, 3 );
  183. $this->code = intval( $parts[1] );
  184. } else {
  185. list( $key, $val ) = array_map( 'trim', explode( ":", $string, 2 ) );
  186. $key = strtoupper( $key );
  187. if ( $replace || !isset( $this->headers[$key] ) ) {
  188. $this->headers[$key] = $val;
  189. }
  190. }
  191. if ( $http_response_code !== null ) {
  192. $this->code = intval( $http_response_code );
  193. }
  194. }
  195. /**
  196. * @since 1.26
  197. * @param int $code Status code
  198. */
  199. public function statusHeader( $code ) {
  200. $this->code = intval( $code );
  201. }
  202. public function headersSent() {
  203. return false;
  204. }
  205. /**
  206. * @param string $key The name of the header to get (case insensitive).
  207. * @return string|null The header value (if set); null otherwise.
  208. */
  209. public function getHeader( $key ) {
  210. $key = strtoupper( $key );
  211. if ( isset( $this->headers[$key] ) ) {
  212. return $this->headers[$key];
  213. }
  214. return null;
  215. }
  216. /**
  217. * Get the HTTP response code, null if not set
  218. *
  219. * @return int|null
  220. */
  221. public function getStatusCode() {
  222. return $this->code;
  223. }
  224. /**
  225. * @param string $name The name of the cookie.
  226. * @param string $value The value to be stored in the cookie.
  227. * @param int|null $expire Ignored in this faux subclass.
  228. * @param array $options Ignored in this faux subclass.
  229. */
  230. public function setCookie( $name, $value, $expire = 0, $options = [] ) {
  231. global $wgCookiePath, $wgCookiePrefix, $wgCookieDomain;
  232. global $wgCookieSecure, $wgCookieExpiration, $wgCookieHttpOnly;
  233. $options = array_filter( $options, function ( $a ) {
  234. return $a !== null;
  235. } ) + [
  236. 'prefix' => $wgCookiePrefix,
  237. 'domain' => $wgCookieDomain,
  238. 'path' => $wgCookiePath,
  239. 'secure' => $wgCookieSecure,
  240. 'httpOnly' => $wgCookieHttpOnly,
  241. 'raw' => false,
  242. ];
  243. if ( $expire === null ) {
  244. $expire = 0; // Session cookie
  245. } elseif ( $expire == 0 && $wgCookieExpiration != 0 ) {
  246. $expire = time() + $wgCookieExpiration;
  247. }
  248. $this->cookies[$options['prefix'] . $name] = [
  249. 'value' => (string)$value,
  250. 'expire' => (int)$expire,
  251. 'path' => (string)$options['path'],
  252. 'domain' => (string)$options['domain'],
  253. 'secure' => (bool)$options['secure'],
  254. 'httpOnly' => (bool)$options['httpOnly'],
  255. 'raw' => (bool)$options['raw'],
  256. ];
  257. }
  258. /**
  259. * @param string $name
  260. * @return string|null
  261. */
  262. public function getCookie( $name ) {
  263. if ( isset( $this->cookies[$name] ) ) {
  264. return $this->cookies[$name]['value'];
  265. }
  266. return null;
  267. }
  268. /**
  269. * @param string $name
  270. * @return array|null
  271. */
  272. public function getCookieData( $name ) {
  273. if ( isset( $this->cookies[$name] ) ) {
  274. return $this->cookies[$name];
  275. }
  276. return null;
  277. }
  278. /**
  279. * @return array
  280. */
  281. public function getCookies() {
  282. return $this->cookies;
  283. }
  284. }