WebResponse.php 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238
  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. /** @var bool Used to disable setters before running jobs post-request (T191537) */
  33. protected static $disableForPostSend = false;
  34. /**
  35. * Disable setters for post-send processing
  36. *
  37. * After this call, self::setCookie(), self::header(), and
  38. * self::statusHeader() will log a warning and return without
  39. * setting cookies or headers.
  40. *
  41. * @since 1.32
  42. */
  43. public static function disableForPostSend() {
  44. self::$disableForPostSend = true;
  45. }
  46. /**
  47. * Output an HTTP header, wrapper for PHP's header()
  48. * @param string $string Header to output
  49. * @param bool $replace Replace current similar header
  50. * @param null|int $http_response_code Forces the HTTP response code to the specified value.
  51. */
  52. public function header( $string, $replace = true, $http_response_code = null ) {
  53. if ( self::$disableForPostSend ) {
  54. wfDebugLog( 'header', 'ignored post-send header {header}', 'all', [
  55. 'header' => $string,
  56. 'replace' => $replace,
  57. 'http_response_code' => $http_response_code,
  58. 'exception' => new RuntimeException( 'Ignored post-send header' ),
  59. ] );
  60. return;
  61. }
  62. \MediaWiki\HeaderCallback::warnIfHeadersSent();
  63. if ( $http_response_code ) {
  64. header( $string, $replace, $http_response_code );
  65. } else {
  66. header( $string, $replace );
  67. }
  68. }
  69. /**
  70. * Get a response header
  71. * @param string $key The name of the header to get (case insensitive).
  72. * @return string|null The header value (if set); null otherwise.
  73. * @since 1.25
  74. */
  75. public function getHeader( $key ) {
  76. foreach ( headers_list() as $header ) {
  77. list( $name, $val ) = explode( ':', $header, 2 );
  78. if ( !strcasecmp( $name, $key ) ) {
  79. return trim( $val );
  80. }
  81. }
  82. return null;
  83. }
  84. /**
  85. * Output an HTTP status code header
  86. * @since 1.26
  87. * @param int $code Status code
  88. */
  89. public function statusHeader( $code ) {
  90. if ( self::$disableForPostSend ) {
  91. wfDebugLog( 'header', 'ignored post-send status header {code}', 'all', [
  92. 'code' => $code,
  93. 'exception' => new RuntimeException( 'Ignored post-send status header' ),
  94. ] );
  95. return;
  96. }
  97. HttpStatus::header( $code );
  98. }
  99. /**
  100. * Test if headers have been sent
  101. * @since 1.27
  102. * @return bool
  103. */
  104. public function headersSent() {
  105. return headers_sent();
  106. }
  107. /**
  108. * Set the browser cookie
  109. * @param string $name The name of the cookie.
  110. * @param string $value The value to be stored in the cookie.
  111. * @param int|null $expire Unix timestamp (in seconds) when the cookie should expire.
  112. * 0 (the default) causes it to expire $wgCookieExpiration seconds from now.
  113. * null causes it to be a session cookie.
  114. * @param array $options Assoc of additional cookie options:
  115. * prefix: string, name prefix ($wgCookiePrefix)
  116. * domain: string, cookie domain ($wgCookieDomain)
  117. * path: string, cookie path ($wgCookiePath)
  118. * secure: bool, secure attribute ($wgCookieSecure)
  119. * httpOnly: bool, httpOnly attribute ($wgCookieHttpOnly)
  120. * @since 1.22 Replaced $prefix, $domain, and $forceSecure with $options
  121. */
  122. public function setCookie( $name, $value, $expire = 0, $options = [] ) {
  123. global $wgCookiePath, $wgCookiePrefix, $wgCookieDomain;
  124. global $wgCookieSecure, $wgCookieExpiration, $wgCookieHttpOnly;
  125. $options = array_filter( $options, function ( $a ) {
  126. return $a !== null;
  127. } ) + [
  128. 'prefix' => $wgCookiePrefix,
  129. 'domain' => $wgCookieDomain,
  130. 'path' => $wgCookiePath,
  131. 'secure' => $wgCookieSecure,
  132. 'httpOnly' => $wgCookieHttpOnly,
  133. 'raw' => false,
  134. ];
  135. if ( $expire === null ) {
  136. $expire = 0; // Session cookie
  137. } elseif ( $expire == 0 && $wgCookieExpiration != 0 ) {
  138. $expire = time() + $wgCookieExpiration;
  139. }
  140. if ( self::$disableForPostSend ) {
  141. $cookie = $options['prefix'] . $name;
  142. wfDebugLog( 'cookie', 'ignored post-send cookie {cookie}', 'all', [
  143. 'cookie' => $cookie,
  144. 'data' => [
  145. 'name' => (string)$cookie,
  146. 'value' => (string)$value,
  147. 'expire' => (int)$expire,
  148. 'path' => (string)$options['path'],
  149. 'domain' => (string)$options['domain'],
  150. 'secure' => (bool)$options['secure'],
  151. 'httpOnly' => (bool)$options['httpOnly'],
  152. ],
  153. 'exception' => new RuntimeException( 'Ignored post-send cookie' ),
  154. ] );
  155. return;
  156. }
  157. $func = $options['raw'] ? 'setrawcookie' : 'setcookie';
  158. if ( Hooks::run( 'WebResponseSetCookie', [ &$name, &$value, &$expire, &$options ] ) ) {
  159. // Note: Don't try to move this earlier to reuse it for self::$disableForPostSend,
  160. // we need to use the altered values from the hook here. (T198525)
  161. $cookie = $options['prefix'] . $name;
  162. $data = [
  163. 'name' => (string)$cookie,
  164. 'value' => (string)$value,
  165. 'expire' => (int)$expire,
  166. 'path' => (string)$options['path'],
  167. 'domain' => (string)$options['domain'],
  168. 'secure' => (bool)$options['secure'],
  169. 'httpOnly' => (bool)$options['httpOnly'],
  170. ];
  171. // Per RFC 6265, key is name + domain + path
  172. $key = "{$data['name']}\n{$data['domain']}\n{$data['path']}";
  173. // If this cookie name was in the request, fake an entry in
  174. // self::$setCookies for it so the deleting check works right.
  175. if ( isset( $_COOKIE[$cookie] ) && !array_key_exists( $key, self::$setCookies ) ) {
  176. self::$setCookies[$key] = [];
  177. }
  178. // PHP deletes if value is the empty string; also, a past expiry is deleting
  179. $deleting = ( $data['value'] === '' || $data['expire'] > 0 && $data['expire'] <= time() );
  180. if ( $deleting && !isset( self::$setCookies[$key] ) ) { // isset( null ) is false
  181. wfDebugLog( 'cookie', 'already deleted ' . $func . ': "' . implode( '", "', $data ) . '"' );
  182. } elseif ( !$deleting && isset( self::$setCookies[$key] ) &&
  183. self::$setCookies[$key] === [ $func, $data ]
  184. ) {
  185. wfDebugLog( 'cookie', 'already set ' . $func . ': "' . implode( '", "', $data ) . '"' );
  186. } else {
  187. wfDebugLog( 'cookie', $func . ': "' . implode( '", "', $data ) . '"' );
  188. if ( $func( ...array_values( $data ) ) ) {
  189. self::$setCookies[$key] = $deleting ? null : [ $func, $data ];
  190. }
  191. }
  192. }
  193. }
  194. /**
  195. * Unset a browser cookie.
  196. * This sets the cookie with an empty value and an expiry set to a time in the past,
  197. * which will cause the browser to remove any cookie with the given name, domain and
  198. * path from its cookie store. Options other than these (and prefix) have no effect.
  199. * @param string $name Cookie name
  200. * @param array $options Cookie options, see {@link setCookie()}
  201. * @since 1.27
  202. */
  203. public function clearCookie( $name, $options = [] ) {
  204. $this->setCookie( $name, '', time() - 31536000 /* 1 year */, $options );
  205. }
  206. /**
  207. * Checks whether this request is performing cookie operations
  208. *
  209. * @return bool
  210. * @since 1.27
  211. */
  212. public function hasCookies() {
  213. return (bool)self::$setCookies;
  214. }
  215. }