Completed
Branch master (4b8315)
by
unknown
17:52
created

includes/WebResponse.php (1 issue)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

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
		if ( $http_response_code ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $http_response_code of type null|integer is loosely compared to true; this is ambiguous if the integer can be zero. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

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