]> scripts.mit.edu Git - autoinstalls/mediawiki.git/blob - includes/WebResponse.php
MediaWiki 1.30.2-scripts
[autoinstalls/mediawiki.git] / includes / WebResponse.php
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 /**
24  * Allow programs to request this object from WebRequest::response()
25  * and handle all outputting (or lack of outputting) via it.
26  * @ingroup HTTP
27  */
28 class WebResponse {
29
30         /** @var array Used to record set cookies, because PHP's setcookie() will
31          * happily send an identical Set-Cookie to the client.
32          */
33         protected static $setCookies = [];
34
35         /**
36          * Output an HTTP header, wrapper for PHP's header()
37          * @param string $string Header to output
38          * @param bool $replace Replace current similar header
39          * @param null|int $http_response_code Forces the HTTP response code to the specified value.
40          */
41         public function header( $string, $replace = true, $http_response_code = null ) {
42                 \MediaWiki\HeaderCallback::warnIfHeadersSent();
43                 if ( $http_response_code ) {
44                         header( $string, $replace, $http_response_code );
45                 } else {
46                         header( $string, $replace );
47                 }
48         }
49
50         /**
51          * Get a response header
52          * @param string $key The name of the header to get (case insensitive).
53          * @return string|null The header value (if set); null otherwise.
54          * @since 1.25
55          */
56         public function getHeader( $key ) {
57                 foreach ( headers_list() as $header ) {
58                         list( $name, $val ) = explode( ':', $header, 2 );
59                         if ( !strcasecmp( $name, $key ) ) {
60                                 return trim( $val );
61                         }
62                 }
63                 return null;
64         }
65
66         /**
67          * Output an HTTP status code header
68          * @since 1.26
69          * @param int $code Status code
70          */
71         public function statusHeader( $code ) {
72                 HttpStatus::header( $code );
73         }
74
75         /**
76          * Test if headers have been sent
77          * @since 1.27
78          * @return bool
79          */
80         public function headersSent() {
81                 return headers_sent();
82         }
83
84         /**
85          * Set the browser cookie
86          * @param string $name The name of the cookie.
87          * @param string $value The value to be stored in the cookie.
88          * @param int|null $expire Unix timestamp (in seconds) when the cookie should expire.
89          *        0 (the default) causes it to expire $wgCookieExpiration seconds from now.
90          *        null causes it to be a session cookie.
91          * @param array $options Assoc of additional cookie options:
92          *     prefix: string, name prefix ($wgCookiePrefix)
93          *     domain: string, cookie domain ($wgCookieDomain)
94          *     path: string, cookie path ($wgCookiePath)
95          *     secure: bool, secure attribute ($wgCookieSecure)
96          *     httpOnly: bool, httpOnly attribute ($wgCookieHttpOnly)
97          * @since 1.22 Replaced $prefix, $domain, and $forceSecure with $options
98          */
99         public function setCookie( $name, $value, $expire = 0, $options = [] ) {
100                 global $wgCookiePath, $wgCookiePrefix, $wgCookieDomain;
101                 global $wgCookieSecure, $wgCookieExpiration, $wgCookieHttpOnly;
102
103                 $options = array_filter( $options, function ( $a ) {
104                         return $a !== null;
105                 } ) + [
106                         'prefix' => $wgCookiePrefix,
107                         'domain' => $wgCookieDomain,
108                         'path' => $wgCookiePath,
109                         'secure' => $wgCookieSecure,
110                         'httpOnly' => $wgCookieHttpOnly,
111                         'raw' => false,
112                 ];
113
114                 if ( $expire === null ) {
115                         $expire = 0; // Session cookie
116                 } elseif ( $expire == 0 && $wgCookieExpiration != 0 ) {
117                         $expire = time() + $wgCookieExpiration;
118                 }
119
120                 $func = $options['raw'] ? 'setrawcookie' : 'setcookie';
121
122                 if ( Hooks::run( 'WebResponseSetCookie', [ &$name, &$value, &$expire, &$options ] ) ) {
123                         $cookie = $options['prefix'] . $name;
124                         $data = [
125                                 'name' => (string)$cookie,
126                                 'value' => (string)$value,
127                                 'expire' => (int)$expire,
128                                 'path' => (string)$options['path'],
129                                 'domain' => (string)$options['domain'],
130                                 'secure' => (bool)$options['secure'],
131                                 'httpOnly' => (bool)$options['httpOnly'],
132                         ];
133
134                         // Per RFC 6265, key is name + domain + path
135                         $key = "{$data['name']}\n{$data['domain']}\n{$data['path']}";
136
137                         // If this cookie name was in the request, fake an entry in
138                         // self::$setCookies for it so the deleting check works right.
139                         if ( isset( $_COOKIE[$cookie] ) && !array_key_exists( $key, self::$setCookies ) ) {
140                                 self::$setCookies[$key] = [];
141                         }
142
143                         // PHP deletes if value is the empty string; also, a past expiry is deleting
144                         $deleting = ( $data['value'] === '' || $data['expire'] > 0 && $data['expire'] <= time() );
145
146                         if ( $deleting && !isset( self::$setCookies[$key] ) ) { // isset( null ) is false
147                                 wfDebugLog( 'cookie', 'already deleted ' . $func . ': "' . implode( '", "', $data ) . '"' );
148                         } elseif ( !$deleting && isset( self::$setCookies[$key] ) &&
149                                 self::$setCookies[$key] === [ $func, $data ]
150                         ) {
151                                 wfDebugLog( 'cookie', 'already set ' . $func . ': "' . implode( '", "', $data ) . '"' );
152                         } else {
153                                 wfDebugLog( 'cookie', $func . ': "' . implode( '", "', $data ) . '"' );
154                                 if ( call_user_func_array( $func, array_values( $data ) ) ) {
155                                         self::$setCookies[$key] = $deleting ? null : [ $func, $data ];
156                                 }
157                         }
158                 }
159         }
160
161         /**
162          * Unset a browser cookie.
163          * This sets the cookie with an empty value and an expiry set to a time in the past,
164          * which will cause the browser to remove any cookie with the given name, domain and
165          * path from its cookie store. Options other than these (and prefix) have no effect.
166          * @param string $name Cookie name
167          * @param array $options Cookie options, see {@link setCookie()}
168          * @since 1.27
169          */
170         public function clearCookie( $name, $options = [] ) {
171                 $this->setCookie( $name, '', time() - 31536000 /* 1 year */, $options );
172         }
173
174         /**
175          * Checks whether this request is performing cookie operations
176          *
177          * @return bool
178          * @since 1.27
179          */
180         public function hasCookies() {
181                 return (bool)self::$setCookies;
182         }
183 }
184
185 /**
186  * @ingroup HTTP
187  */
188 class FauxResponse extends WebResponse {
189         private $headers;
190         private $cookies = [];
191         private $code;
192
193         /**
194          * Stores a HTTP header
195          * @param string $string Header to output
196          * @param bool $replace Replace current similar header
197          * @param null|int $http_response_code Forces the HTTP response code to the specified value.
198          */
199         public function header( $string, $replace = true, $http_response_code = null ) {
200                 if ( substr( $string, 0, 5 ) == 'HTTP/' ) {
201                         $parts = explode( ' ', $string, 3 );
202                         $this->code = intval( $parts[1] );
203                 } else {
204                         list( $key, $val ) = array_map( 'trim', explode( ":", $string, 2 ) );
205
206                         $key = strtoupper( $key );
207
208                         if ( $replace || !isset( $this->headers[$key] ) ) {
209                                 $this->headers[$key] = $val;
210                         }
211                 }
212
213                 if ( $http_response_code !== null ) {
214                         $this->code = intval( $http_response_code );
215                 }
216         }
217
218         /**
219          * @since 1.26
220          * @param int $code Status code
221          */
222         public function statusHeader( $code ) {
223                 $this->code = intval( $code );
224         }
225
226         public function headersSent() {
227                 return false;
228         }
229
230         /**
231          * @param string $key The name of the header to get (case insensitive).
232          * @return string|null The header value (if set); null otherwise.
233          */
234         public function getHeader( $key ) {
235                 $key = strtoupper( $key );
236
237                 if ( isset( $this->headers[$key] ) ) {
238                         return $this->headers[$key];
239                 }
240                 return null;
241         }
242
243         /**
244          * Get the HTTP response code, null if not set
245          *
246          * @return int|null
247          */
248         public function getStatusCode() {
249                 return $this->code;
250         }
251
252         /**
253          * @param string $name The name of the cookie.
254          * @param string $value The value to be stored in the cookie.
255          * @param int|null $expire Ignored in this faux subclass.
256          * @param array $options Ignored in this faux subclass.
257          */
258         public function setCookie( $name, $value, $expire = 0, $options = [] ) {
259                 global $wgCookiePath, $wgCookiePrefix, $wgCookieDomain;
260                 global $wgCookieSecure, $wgCookieExpiration, $wgCookieHttpOnly;
261
262                 $options = array_filter( $options, function ( $a ) {
263                         return $a !== null;
264                 } ) + [
265                         'prefix' => $wgCookiePrefix,
266                         'domain' => $wgCookieDomain,
267                         'path' => $wgCookiePath,
268                         'secure' => $wgCookieSecure,
269                         'httpOnly' => $wgCookieHttpOnly,
270                         'raw' => false,
271                 ];
272
273                 if ( $expire === null ) {
274                         $expire = 0; // Session cookie
275                 } elseif ( $expire == 0 && $wgCookieExpiration != 0 ) {
276                         $expire = time() + $wgCookieExpiration;
277                 }
278
279                 $this->cookies[$options['prefix'] . $name] = [
280                         'value' => (string)$value,
281                         'expire' => (int)$expire,
282                         'path' => (string)$options['path'],
283                         'domain' => (string)$options['domain'],
284                         'secure' => (bool)$options['secure'],
285                         'httpOnly' => (bool)$options['httpOnly'],
286                         'raw' => (bool)$options['raw'],
287                 ];
288         }
289
290         /**
291          * @param string $name
292          * @return string|null
293          */
294         public function getCookie( $name ) {
295                 if ( isset( $this->cookies[$name] ) ) {
296                         return $this->cookies[$name]['value'];
297                 }
298                 return null;
299         }
300
301         /**
302          * @param string $name
303          * @return array|null
304          */
305         public function getCookieData( $name ) {
306                 if ( isset( $this->cookies[$name] ) ) {
307                         return $this->cookies[$name];
308                 }
309                 return null;
310         }
311
312         /**
313          * @return array
314          */
315         public function getCookies() {
316                 return $this->cookies;
317         }
318 }