MediaWiki fundraising/REL1_35
PhpHttpRequest.php
Go to the documentation of this file.
1<?php
22
23 private $fopenErrors = [];
24
28 public function __construct() {
29 if ( !wfIniGetBool( 'allow_url_fopen' ) ) {
30 throw new RuntimeException( __METHOD__ . ': allow_url_fopen needs to be enabled for ' .
31 'pure PHP http requests to work. If possible, curl should be used instead. See ' .
32 'https://www.php.net/curl.'
33 );
34 }
35
36 parent::__construct( ...func_get_args() );
37 }
38
43 protected function urlToTcp( $url ) {
44 $parsedUrl = parse_url( $url );
45
46 return 'tcp://' . $parsedUrl['host'] . ':' . $parsedUrl['port'];
47 }
48
58 protected function getCertOptions() {
59 $certOptions = [];
60 $certLocations = [];
61 if ( $this->caInfo ) {
62 $certLocations = [ 'manual' => $this->caInfo ];
63 }
64
65 foreach ( $certLocations as $key => $cert ) {
66 if ( is_dir( $cert ) ) {
67 $certOptions['capath'] = $cert;
68 break;
69 } elseif ( is_file( $cert ) ) {
70 $certOptions['cafile'] = $cert;
71 break;
72 } elseif ( $key === 'manual' ) {
73 // fail more loudly if a cert path was manually configured and it is not valid
74 throw new DomainException( "Invalid CA info passed: $cert" );
75 }
76 }
77
78 return $certOptions;
79 }
80
91 public function errorHandler( $errno, $errstr ) {
92 $n = count( $this->fopenErrors ) + 1;
93 $this->fopenErrors += [ "errno$n" => $errno, "errstr$n" => $errstr ];
94 }
95
101 public function execute() {
102 $this->prepare();
103
104 if ( is_array( $this->postData ) ) {
105 $this->postData = wfArrayToCgi( $this->postData );
106 }
107
108 if ( $this->parsedUrl['scheme'] != 'http'
109 && $this->parsedUrl['scheme'] != 'https' ) {
110 $this->status->fatal( 'http-invalid-scheme', $this->parsedUrl['scheme'] );
111 }
112
113 $this->reqHeaders['Accept'] = "*/*";
114 $this->reqHeaders['Connection'] = 'Close';
115 if ( $this->method == 'POST' ) {
116 // Required for HTTP 1.0 POSTs
117 $this->reqHeaders['Content-Length'] = strlen( $this->postData );
118 if ( !isset( $this->reqHeaders['Content-Type'] ) ) {
119 $this->reqHeaders['Content-Type'] = "application/x-www-form-urlencoded";
120 }
121 }
122
123 // Set up PHP stream context
124 $options = [
125 'http' => [
126 'method' => $this->method,
127 'header' => implode( "\r\n", $this->getHeaderList() ),
128 'protocol_version' => '1.1',
129 'max_redirects' => $this->followRedirects ? $this->maxRedirects : 0,
130 'ignore_errors' => true,
131 'timeout' => $this->timeout,
132 // Curl options in case curlwrappers are installed
133 'curl_verify_ssl_host' => $this->sslVerifyHost ? 2 : 0,
134 'curl_verify_ssl_peer' => $this->sslVerifyCert,
135 ],
136 'ssl' => [
137 'verify_peer' => $this->sslVerifyCert,
138 'SNI_enabled' => true,
139 'ciphers' => 'HIGH:!SSLv2:!SSLv3:-ADH:-kDH:-kECDH:-DSS',
140 'disable_compression' => true,
141 ],
142 ];
143
144 if ( $this->proxy ) {
145 $options['http']['proxy'] = $this->urlToTcp( $this->proxy );
146 $options['http']['request_fulluri'] = true;
147 }
148
149 if ( $this->postData ) {
150 $options['http']['content'] = $this->postData;
151 }
152
153 if ( $this->sslVerifyHost ) {
154 $options['ssl']['peer_name'] = $this->parsedUrl['host'];
155 }
156
157 $options['ssl'] += $this->getCertOptions();
158
159 $context = stream_context_create( $options );
160
161 $this->headerList = [];
162 $reqCount = 0;
163 $url = $this->url;
164
165 $result = [];
166
167 if ( $this->profiler ) {
168 $profileSection = $this->profiler->scopedProfileIn(
169 __METHOD__ . '-' . $this->profileName
170 );
171 }
172 do {
173 $reqCount++;
174 $this->fopenErrors = [];
175 set_error_handler( [ $this, 'errorHandler' ] );
176 $fh = fopen( $url, "r", false, $context );
177 restore_error_handler();
178
179 if ( !$fh ) {
180 break;
181 }
182
183 $result = stream_get_meta_data( $fh );
184 $this->headerList = $result['wrapper_data'];
185 $this->parseHeader();
186
187 if ( !$this->followRedirects ) {
188 break;
189 }
190
191 # Handle manual redirection
192 if ( !$this->isRedirect() || $reqCount > $this->maxRedirects ) {
193 break;
194 }
195 # Check security of URL
196 $url = $this->getResponseHeader( "Location" );
197
198 if ( !Http::isValidURI( $url ) ) {
199 $this->logger->debug( __METHOD__ . ": insecure redirection" );
200 break;
201 }
202 } while ( true );
203 if ( $this->profiler ) {
204 $this->profiler->scopedProfileOut( $profileSection );
205 }
206
207 $this->setStatus();
208
209 if ( $fh === false ) {
210 if ( $this->fopenErrors ) {
211 $this->logger->warning( __CLASS__
212 . ': error opening connection: {errstr1}', $this->fopenErrors );
213 }
214 $this->status->fatal( 'http-request-error' );
215 return Status::wrap( $this->status ); // TODO B/C; move this to callers
216 }
217
218 if ( $result['timed_out'] ) {
219 $this->status->fatal( 'http-timed-out', $this->url );
220 return Status::wrap( $this->status ); // TODO B/C; move this to callers
221 }
222
223 // If everything went OK, or we received some error code
224 // get the response body content.
225 if ( $this->status->isOK() || (int)$this->respStatus >= 300 ) {
226 while ( !feof( $fh ) ) {
227 $buf = fread( $fh, 8192 );
228
229 if ( $buf === false ) {
230 $this->status->fatal( 'http-read-error' );
231 break;
232 }
233
234 if ( $buf !== '' ) {
235 call_user_func( $this->callback, $fh, $buf );
236 }
237 }
238 }
239 fclose( $fh );
240
241 return Status::wrap( $this->status ); // TODO B/C; move this to callers
242 }
243}
wfIniGetBool( $setting)
Safety wrapper around ini_get() for boolean settings.
wfArrayToCgi( $array1, $array2=null, $prefix='')
This function takes one or two arrays as input, and returns a CGI-style string, e....
static isValidURI( $uri)
Check that the given URI is a valid one.
Definition Http.php:117
This wrapper class will call out to curl (if available) or fallback to regular PHP if necessary for h...
isRedirect()
Returns true if the last status code was a redirect.
setStatus()
Sets HTTPRequest status member to a fatal value with the error message if the returned integer value ...
parseHeader()
Parses the headers, including the HTTP status code and any Set-Cookie headers.
getResponseHeader( $header)
Returns the value of the given response header.
getHeaderList()
Get an array of the headers.
getCertOptions()
Returns an array with a 'capath' or 'cafile' key that is suitable to be merged into the 'ssl' sub-arr...
errorHandler( $errno, $errstr)
Custom error handler for dealing with fopen() errors.