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