MediaWiki  master
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 }
PhpHttpRequest\$fopenErrors
$fopenErrors
Definition: PhpHttpRequest.php:23
MWHttpRequest\setStatus
setStatus()
Sets HTTPRequest status member to a fatal value with the error message if the returned integer value ...
Definition: MWHttpRequest.php:442
MWHttpRequest\$sslVerifyCert
$sslVerifyCert
Definition: MWHttpRequest.php:47
PhpHttpRequest\errorHandler
errorHandler( $errno, $errstr)
Custom error handler for dealing with fopen() errors.
Definition: PhpHttpRequest.php:91
PhpHttpRequest\urlToTcp
urlToTcp( $url)
Definition: PhpHttpRequest.php:43
MWHttpRequest\parseHeader
parseHeader()
Parses the headers, including the HTTP status code and any Set-Cookie headers.
Definition: MWHttpRequest.php:411
MWHttpRequest\$postData
$postData
Definition: MWHttpRequest.php:43
Status\wrap
static wrap( $sv)
Succinct helper method to wrap a StatusValue.
Definition: Status.php:62
MWHttpRequest\isRedirect
isRedirect()
Returns true if the last status code was a redirect.
Definition: MWHttpRequest.php:476
PhpHttpRequest\__construct
__construct()
Definition: PhpHttpRequest.php:28
MWHttpRequest\$timeout
int string $timeout
Definition: MWHttpRequest.php:39
PhpHttpRequest\execute
execute()
Definition: PhpHttpRequest.php:101
MWHttpRequest\$method
$method
Definition: MWHttpRequest.php:49
MWHttpRequest
This wrapper class will call out to curl (if available) or fallback to regular PHP if necessary for h...
Definition: MWHttpRequest.php:33
MWHttpRequest\getHeaderList
getHeaderList()
Get an array of the headers.
Definition: MWHttpRequest.php:306
MWHttpRequest\$parsedUrl
$parsedUrl
Definition: MWHttpRequest.php:53
wfIniGetBool
wfIniGetBool( $setting)
Safety wrapper around ini_get() for boolean settings.
Definition: GlobalFunctions.php:1987
PhpHttpRequest\getCertOptions
getCertOptions()
Returns an array with a 'capath' or 'cafile' key that is suitable to be merged into the 'ssl' sub-arr...
Definition: PhpHttpRequest.php:58
MWHttpRequest\$caInfo
$caInfo
Definition: MWHttpRequest.php:48
MWHttpRequest\getResponseHeader
getResponseHeader( $header)
Returns the value of the given response header.
Definition: MWHttpRequest.php:513
Http\isValidURI
static isValidURI( $uri)
Check that the given URI is a valid one.
Definition: Http.php:117
PhpHttpRequest
Definition: PhpHttpRequest.php:21
MWHttpRequest\$url
$url
Definition: MWHttpRequest.php:52
wfArrayToCgi
wfArrayToCgi( $array1, $array2=null, $prefix='')
This function takes one or two arrays as input, and returns a CGI-style string, e....
Definition: GlobalFunctions.php:346
MWHttpRequest\prepare
prepare()
Definition: MWHttpRequest.php:387