[ Index ]

PHP Cross Reference of MediaWiki-1.24.0

title

Body

[close]

/includes/ -> HttpFunctions.php (source)

   1  <?php
   2  /**
   3   * Various HTTP related functions.
   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   * @ingroup HTTP
  22   */
  23  
  24  /**
  25   * @defgroup HTTP HTTP
  26   */
  27  
  28  /**
  29   * Various HTTP related functions
  30   * @ingroup HTTP
  31   */
  32  class Http {
  33      static public $httpEngine = false;
  34  
  35      /**
  36       * Perform an HTTP request
  37       *
  38       * @param string $method HTTP method. Usually GET/POST
  39       * @param string $url Full URL to act on. If protocol-relative, will be expanded to an http:// URL
  40       * @param array $options Options to pass to MWHttpRequest object.
  41       *    Possible keys for the array:
  42       *    - timeout             Timeout length in seconds
  43       *    - connectTimeout      Timeout for connection, in seconds (curl only)
  44       *    - postData            An array of key-value pairs or a url-encoded form data
  45       *    - proxy               The proxy to use.
  46       *                          Otherwise it will use $wgHTTPProxy (if set)
  47       *                          Otherwise it will use the environment variable "http_proxy" (if set)
  48       *    - noProxy             Don't use any proxy at all. Takes precedence over proxy value(s).
  49       *    - sslVerifyHost       Verify hostname against certificate
  50       *    - sslVerifyCert       Verify SSL certificate
  51       *    - caInfo              Provide CA information
  52       *    - maxRedirects        Maximum number of redirects to follow (defaults to 5)
  53       *    - followRedirects     Whether to follow redirects (defaults to false).
  54       *                            Note: this should only be used when the target URL is trusted,
  55       *                            to avoid attacks on intranet services accessible by HTTP.
  56       *    - userAgent           A user agent, if you want to override the default
  57       *                          MediaWiki/$wgVersion
  58       * @return string|bool (bool)false on failure or a string on success
  59       */
  60  	public static function request( $method, $url, $options = array() ) {
  61          wfDebug( "HTTP: $method: $url\n" );
  62          wfProfileIn( __METHOD__ . "-$method" );
  63  
  64          $options['method'] = strtoupper( $method );
  65  
  66          if ( !isset( $options['timeout'] ) ) {
  67              $options['timeout'] = 'default';
  68          }
  69          if ( !isset( $options['connectTimeout'] ) ) {
  70              $options['connectTimeout'] = 'default';
  71          }
  72  
  73          $req = MWHttpRequest::factory( $url, $options );
  74          $status = $req->execute();
  75  
  76          $content = false;
  77          if ( $status->isOK() ) {
  78              $content = $req->getContent();
  79          }
  80          wfProfileOut( __METHOD__ . "-$method" );
  81          return $content;
  82      }
  83  
  84      /**
  85       * Simple wrapper for Http::request( 'GET' )
  86       * @see Http::request()
  87       *
  88       * @param string $url
  89       * @param string $timeout
  90       * @param array $options
  91       * @return string
  92       */
  93  	public static function get( $url, $timeout = 'default', $options = array() ) {
  94          $options['timeout'] = $timeout;
  95          return Http::request( 'GET', $url, $options );
  96      }
  97  
  98      /**
  99       * Simple wrapper for Http::request( 'POST' )
 100       * @see Http::request()
 101       *
 102       * @param string $url
 103       * @param array $options
 104       * @return string
 105       */
 106  	public static function post( $url, $options = array() ) {
 107          return Http::request( 'POST', $url, $options );
 108      }
 109  
 110      /**
 111       * Check if the URL can be served by localhost
 112       *
 113       * @param string $url Full url to check
 114       * @return bool
 115       */
 116  	public static function isLocalURL( $url ) {
 117          global $wgCommandLineMode, $wgConf;
 118  
 119          if ( $wgCommandLineMode ) {
 120              return false;
 121          }
 122  
 123          // Extract host part
 124          $matches = array();
 125          if ( preg_match( '!^http://([\w.-]+)[/:].*$!', $url, $matches ) ) {
 126              $host = $matches[1];
 127              // Split up dotwise
 128              $domainParts = explode( '.', $host );
 129              // Check if this domain or any superdomain is listed in $wgConf as a local virtual host
 130              $domainParts = array_reverse( $domainParts );
 131  
 132              $domain = '';
 133              $countParts = count( $domainParts );
 134              for ( $i = 0; $i < $countParts; $i++ ) {
 135                  $domainPart = $domainParts[$i];
 136                  if ( $i == 0 ) {
 137                      $domain = $domainPart;
 138                  } else {
 139                      $domain = $domainPart . '.' . $domain;
 140                  }
 141  
 142                  if ( $wgConf->isLocalVHost( $domain ) ) {
 143                      return true;
 144                  }
 145              }
 146          }
 147  
 148          return false;
 149      }
 150  
 151      /**
 152       * A standard user-agent we can use for external requests.
 153       * @return string
 154       */
 155  	public static function userAgent() {
 156          global $wgVersion;
 157          return "MediaWiki/$wgVersion";
 158      }
 159  
 160      /**
 161       * Checks that the given URI is a valid one. Hardcoding the
 162       * protocols, because we only want protocols that both cURL
 163       * and php support.
 164       *
 165       * file:// should not be allowed here for security purpose (r67684)
 166       *
 167       * @todo FIXME this is wildly inaccurate and fails to actually check most stuff
 168       *
 169       * @param string $uri URI to check for validity
 170       * @return bool
 171       */
 172  	public static function isValidURI( $uri ) {
 173          return preg_match(
 174              '/^https?:\/\/[^\/\s]\S*$/D',
 175              $uri
 176          );
 177      }
 178  }
 179  
 180  /**
 181   * This wrapper class will call out to curl (if available) or fallback
 182   * to regular PHP if necessary for handling internal HTTP requests.
 183   *
 184   * Renamed from HttpRequest to MWHttpRequest to avoid conflict with
 185   * PHP's HTTP extension.
 186   */
 187  class MWHttpRequest {
 188      const SUPPORTS_FILE_POSTS = false;
 189  
 190      protected $content;
 191      protected $timeout = 'default';
 192      protected $headersOnly = null;
 193      protected $postData = null;
 194      protected $proxy = null;
 195      protected $noProxy = false;
 196      protected $sslVerifyHost = true;
 197      protected $sslVerifyCert = true;
 198      protected $caInfo = null;
 199      protected $method = "GET";
 200      protected $reqHeaders = array();
 201      protected $url;
 202      protected $parsedUrl;
 203      protected $callback;
 204      protected $maxRedirects = 5;
 205      protected $followRedirects = false;
 206  
 207      /**
 208       * @var CookieJar
 209       */
 210      protected $cookieJar;
 211  
 212      protected $headerList = array();
 213      protected $respVersion = "0.9";
 214      protected $respStatus = "200 Ok";
 215      protected $respHeaders = array();
 216  
 217      public $status;
 218  
 219      /**
 220       * @param string $url Url to use. If protocol-relative, will be expanded to an http:// URL
 221       * @param array $options (optional) extra params to pass (see Http::request())
 222       */
 223  	protected function __construct( $url, $options = array() ) {
 224          global $wgHTTPTimeout, $wgHTTPConnectTimeout;
 225  
 226          $this->url = wfExpandUrl( $url, PROTO_HTTP );
 227          $this->parsedUrl = wfParseUrl( $this->url );
 228  
 229          if ( !$this->parsedUrl || !Http::isValidURI( $this->url ) ) {
 230              $this->status = Status::newFatal( 'http-invalid-url' );
 231          } else {
 232              $this->status = Status::newGood( 100 ); // continue
 233          }
 234  
 235          if ( isset( $options['timeout'] ) && $options['timeout'] != 'default' ) {
 236              $this->timeout = $options['timeout'];
 237          } else {
 238              $this->timeout = $wgHTTPTimeout;
 239          }
 240          if ( isset( $options['connectTimeout'] ) && $options['connectTimeout'] != 'default' ) {
 241              $this->connectTimeout = $options['connectTimeout'];
 242          } else {
 243              $this->connectTimeout = $wgHTTPConnectTimeout;
 244          }
 245          if ( isset( $options['userAgent'] ) ) {
 246              $this->setUserAgent( $options['userAgent'] );
 247          }
 248  
 249          $members = array( "postData", "proxy", "noProxy", "sslVerifyHost", "caInfo",
 250                  "method", "followRedirects", "maxRedirects", "sslVerifyCert", "callback" );
 251  
 252          foreach ( $members as $o ) {
 253              if ( isset( $options[$o] ) ) {
 254                  // ensure that MWHttpRequest::method is always
 255                  // uppercased. Bug 36137
 256                  if ( $o == 'method' ) {
 257                      $options[$o] = strtoupper( $options[$o] );
 258                  }
 259                  $this->$o = $options[$o];
 260              }
 261          }
 262  
 263          if ( $this->noProxy ) {
 264              $this->proxy = ''; // noProxy takes precedence
 265          }
 266      }
 267  
 268      /**
 269       * Simple function to test if we can make any sort of requests at all, using
 270       * cURL or fopen()
 271       * @return bool
 272       */
 273  	public static function canMakeRequests() {
 274          return function_exists( 'curl_init' ) || wfIniGetBool( 'allow_url_fopen' );
 275      }
 276  
 277      /**
 278       * Generate a new request object
 279       * @param string $url Url to use
 280       * @param array $options (optional) extra params to pass (see Http::request())
 281       * @throws MWException
 282       * @return CurlHttpRequest|PhpHttpRequest
 283       * @see MWHttpRequest::__construct
 284       */
 285  	public static function factory( $url, $options = null ) {
 286          if ( !Http::$httpEngine ) {
 287              Http::$httpEngine = function_exists( 'curl_init' ) ? 'curl' : 'php';
 288          } elseif ( Http::$httpEngine == 'curl' && !function_exists( 'curl_init' ) ) {
 289              throw new MWException( __METHOD__ . ': curl (http://php.net/curl) is not installed, but' .
 290                  ' Http::$httpEngine is set to "curl"' );
 291          }
 292  
 293          switch ( Http::$httpEngine ) {
 294              case 'curl':
 295                  return new CurlHttpRequest( $url, $options );
 296              case 'php':
 297                  if ( !wfIniGetBool( 'allow_url_fopen' ) ) {
 298                      throw new MWException( __METHOD__ . ': allow_url_fopen ' .
 299                          'needs to be enabled for pure PHP http requests to ' .
 300                          'work. If possible, curl should be used instead. See ' .
 301                          'http://php.net/curl.'
 302                      );
 303                  }
 304                  return new PhpHttpRequest( $url, $options );
 305              default:
 306                  throw new MWException( __METHOD__ . ': The setting of Http::$httpEngine is not valid.' );
 307          }
 308      }
 309  
 310      /**
 311       * Get the body, or content, of the response to the request
 312       *
 313       * @return string
 314       */
 315  	public function getContent() {
 316          return $this->content;
 317      }
 318  
 319      /**
 320       * Set the parameters of the request
 321       *
 322       * @param array $args
 323       * @todo overload the args param
 324       */
 325  	public function setData( $args ) {
 326          $this->postData = $args;
 327      }
 328  
 329      /**
 330       * Take care of setting up the proxy (do nothing if "noProxy" is set)
 331       *
 332       * @return void
 333       */
 334  	public function proxySetup() {
 335          global $wgHTTPProxy;
 336  
 337          // If there is an explicit proxy set and proxies are not disabled, then use it
 338          if ( $this->proxy && !$this->noProxy ) {
 339              return;
 340          }
 341  
 342          // Otherwise, fallback to $wgHTTPProxy/http_proxy (when set) if this is not a machine
 343          // local URL and proxies are not disabled
 344          if ( Http::isLocalURL( $this->url ) || $this->noProxy ) {
 345              $this->proxy = '';
 346          } elseif ( $wgHTTPProxy ) {
 347              $this->proxy = $wgHTTPProxy;
 348          } elseif ( getenv( "http_proxy" ) ) {
 349              $this->proxy = getenv( "http_proxy" );
 350          }
 351      }
 352  
 353      /**
 354       * Set the user agent
 355       * @param string $UA
 356       */
 357  	public function setUserAgent( $UA ) {
 358          $this->setHeader( 'User-Agent', $UA );
 359      }
 360  
 361      /**
 362       * Set an arbitrary header
 363       * @param string $name
 364       * @param string $value
 365       */
 366  	public function setHeader( $name, $value ) {
 367          // I feel like I should normalize the case here...
 368          $this->reqHeaders[$name] = $value;
 369      }
 370  
 371      /**
 372       * Get an array of the headers
 373       * @return array
 374       */
 375  	public function getHeaderList() {
 376          $list = array();
 377  
 378          if ( $this->cookieJar ) {
 379              $this->reqHeaders['Cookie'] =
 380                  $this->cookieJar->serializeToHttpRequest(
 381                      $this->parsedUrl['path'],
 382                      $this->parsedUrl['host']
 383                  );
 384          }
 385  
 386          foreach ( $this->reqHeaders as $name => $value ) {
 387              $list[] = "$name: $value";
 388          }
 389  
 390          return $list;
 391      }
 392  
 393      /**
 394       * Set a read callback to accept data read from the HTTP request.
 395       * By default, data is appended to an internal buffer which can be
 396       * retrieved through $req->getContent().
 397       *
 398       * To handle data as it comes in -- especially for large files that
 399       * would not fit in memory -- you can instead set your own callback,
 400       * in the form function($resource, $buffer) where the first parameter
 401       * is the low-level resource being read (implementation specific),
 402       * and the second parameter is the data buffer.
 403       *
 404       * You MUST return the number of bytes handled in the buffer; if fewer
 405       * bytes are reported handled than were passed to you, the HTTP fetch
 406       * will be aborted.
 407       *
 408       * @param callable $callback
 409       * @throws MWException
 410       */
 411  	public function setCallback( $callback ) {
 412          if ( !is_callable( $callback ) ) {
 413              throw new MWException( 'Invalid MwHttpRequest callback' );
 414          }
 415          $this->callback = $callback;
 416      }
 417  
 418      /**
 419       * A generic callback to read the body of the response from a remote
 420       * server.
 421       *
 422       * @param resource $fh
 423       * @param string $content
 424       * @return int
 425       */
 426  	public function read( $fh, $content ) {
 427          $this->content .= $content;
 428          return strlen( $content );
 429      }
 430  
 431      /**
 432       * Take care of whatever is necessary to perform the URI request.
 433       *
 434       * @return Status
 435       */
 436  	public function execute() {
 437          wfProfileIn( __METHOD__ );
 438  
 439          $this->content = "";
 440  
 441          if ( strtoupper( $this->method ) == "HEAD" ) {
 442              $this->headersOnly = true;
 443          }
 444  
 445          $this->proxySetup(); // set up any proxy as needed
 446  
 447          if ( !$this->callback ) {
 448              $this->setCallback( array( $this, 'read' ) );
 449          }
 450  
 451          if ( !isset( $this->reqHeaders['User-Agent'] ) ) {
 452              $this->setUserAgent( Http::userAgent() );
 453          }
 454  
 455          wfProfileOut( __METHOD__ );
 456      }
 457  
 458      /**
 459       * Parses the headers, including the HTTP status code and any
 460       * Set-Cookie headers.  This function expects the headers to be
 461       * found in an array in the member variable headerList.
 462       */
 463  	protected function parseHeader() {
 464          wfProfileIn( __METHOD__ );
 465  
 466          $lastname = "";
 467  
 468          foreach ( $this->headerList as $header ) {
 469              if ( preg_match( "#^HTTP/([0-9.]+) (.*)#", $header, $match ) ) {
 470                  $this->respVersion = $match[1];
 471                  $this->respStatus = $match[2];
 472              } elseif ( preg_match( "#^[ \t]#", $header ) ) {
 473                  $last = count( $this->respHeaders[$lastname] ) - 1;
 474                  $this->respHeaders[$lastname][$last] .= "\r\n$header";
 475              } elseif ( preg_match( "#^([^:]*):[\t ]*(.*)#", $header, $match ) ) {
 476                  $this->respHeaders[strtolower( $match[1] )][] = $match[2];
 477                  $lastname = strtolower( $match[1] );
 478              }
 479          }
 480  
 481          $this->parseCookies();
 482  
 483          wfProfileOut( __METHOD__ );
 484      }
 485  
 486      /**
 487       * Sets HTTPRequest status member to a fatal value with the error
 488       * message if the returned integer value of the status code was
 489       * not successful (< 300) or a redirect (>=300 and < 400).  (see
 490       * RFC2616, section 10,
 491       * http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html for a
 492       * list of status codes.)
 493       */
 494  	protected function setStatus() {
 495          if ( !$this->respHeaders ) {
 496              $this->parseHeader();
 497          }
 498  
 499          if ( (int)$this->respStatus > 399 ) {
 500              list( $code, $message ) = explode( " ", $this->respStatus, 2 );
 501              $this->status->fatal( "http-bad-status", $code, $message );
 502          }
 503      }
 504  
 505      /**
 506       * Get the integer value of the HTTP status code (e.g. 200 for "200 Ok")
 507       * (see RFC2616, section 10, http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html
 508       * for a list of status codes.)
 509       *
 510       * @return int
 511       */
 512  	public function getStatus() {
 513          if ( !$this->respHeaders ) {
 514              $this->parseHeader();
 515          }
 516  
 517          return (int)$this->respStatus;
 518      }
 519  
 520      /**
 521       * Returns true if the last status code was a redirect.
 522       *
 523       * @return bool
 524       */
 525  	public function isRedirect() {
 526          if ( !$this->respHeaders ) {
 527              $this->parseHeader();
 528          }
 529  
 530          $status = (int)$this->respStatus;
 531  
 532          if ( $status >= 300 && $status <= 303 ) {
 533              return true;
 534          }
 535  
 536          return false;
 537      }
 538  
 539      /**
 540       * Returns an associative array of response headers after the
 541       * request has been executed.  Because some headers
 542       * (e.g. Set-Cookie) can appear more than once the, each value of
 543       * the associative array is an array of the values given.
 544       *
 545       * @return array
 546       */
 547  	public function getResponseHeaders() {
 548          if ( !$this->respHeaders ) {
 549              $this->parseHeader();
 550          }
 551  
 552          return $this->respHeaders;
 553      }
 554  
 555      /**
 556       * Returns the value of the given response header.
 557       *
 558       * @param string $header
 559       * @return string
 560       */
 561  	public function getResponseHeader( $header ) {
 562          if ( !$this->respHeaders ) {
 563              $this->parseHeader();
 564          }
 565  
 566          if ( isset( $this->respHeaders[strtolower( $header )] ) ) {
 567              $v = $this->respHeaders[strtolower( $header )];
 568              return $v[count( $v ) - 1];
 569          }
 570  
 571          return null;
 572      }
 573  
 574      /**
 575       * Tells the MWHttpRequest object to use this pre-loaded CookieJar.
 576       *
 577       * @param CookieJar $jar
 578       */
 579  	public function setCookieJar( $jar ) {
 580          $this->cookieJar = $jar;
 581      }
 582  
 583      /**
 584       * Returns the cookie jar in use.
 585       *
 586       * @return CookieJar
 587       */
 588  	public function getCookieJar() {
 589          if ( !$this->respHeaders ) {
 590              $this->parseHeader();
 591          }
 592  
 593          return $this->cookieJar;
 594      }
 595  
 596      /**
 597       * Sets a cookie. Used before a request to set up any individual
 598       * cookies. Used internally after a request to parse the
 599       * Set-Cookie headers.
 600       * @see Cookie::set
 601       * @param string $name
 602       * @param mixed $value
 603       * @param array $attr
 604       */
 605  	public function setCookie( $name, $value = null, $attr = null ) {
 606          if ( !$this->cookieJar ) {
 607              $this->cookieJar = new CookieJar;
 608          }
 609  
 610          $this->cookieJar->setCookie( $name, $value, $attr );
 611      }
 612  
 613      /**
 614       * Parse the cookies in the response headers and store them in the cookie jar.
 615       */
 616  	protected function parseCookies() {
 617          wfProfileIn( __METHOD__ );
 618  
 619          if ( !$this->cookieJar ) {
 620              $this->cookieJar = new CookieJar;
 621          }
 622  
 623          if ( isset( $this->respHeaders['set-cookie'] ) ) {
 624              $url = parse_url( $this->getFinalUrl() );
 625              foreach ( $this->respHeaders['set-cookie'] as $cookie ) {
 626                  $this->cookieJar->parseCookieResponseHeader( $cookie, $url['host'] );
 627              }
 628          }
 629  
 630          wfProfileOut( __METHOD__ );
 631      }
 632  
 633      /**
 634       * Returns the final URL after all redirections.
 635       *
 636       * Relative values of the "Location" header are incorrect as
 637       * stated in RFC, however they do happen and modern browsers
 638       * support them.  This function loops backwards through all
 639       * locations in order to build the proper absolute URI - Marooned
 640       * at wikia-inc.com
 641       *
 642       * Note that the multiple Location: headers are an artifact of
 643       * CURL -- they shouldn't actually get returned this way. Rewrite
 644       * this when bug 29232 is taken care of (high-level redirect
 645       * handling rewrite).
 646       *
 647       * @return string
 648       */
 649  	public function getFinalUrl() {
 650          $headers = $this->getResponseHeaders();
 651  
 652          //return full url (fix for incorrect but handled relative location)
 653          if ( isset( $headers['location'] ) ) {
 654              $locations = $headers['location'];
 655              $domain = '';
 656              $foundRelativeURI = false;
 657              $countLocations = count( $locations );
 658  
 659              for ( $i = $countLocations - 1; $i >= 0; $i-- ) {
 660                  $url = parse_url( $locations[$i] );
 661  
 662                  if ( isset( $url['host'] ) ) {
 663                      $domain = $url['scheme'] . '://' . $url['host'];
 664                      break; //found correct URI (with host)
 665                  } else {
 666                      $foundRelativeURI = true;
 667                  }
 668              }
 669  
 670              if ( $foundRelativeURI ) {
 671                  if ( $domain ) {
 672                      return $domain . $locations[$countLocations - 1];
 673                  } else {
 674                      $url = parse_url( $this->url );
 675                      if ( isset( $url['host'] ) ) {
 676                          return $url['scheme'] . '://' . $url['host'] .
 677                              $locations[$countLocations - 1];
 678                      }
 679                  }
 680              } else {
 681                  return $locations[$countLocations - 1];
 682              }
 683          }
 684  
 685          return $this->url;
 686      }
 687  
 688      /**
 689       * Returns true if the backend can follow redirects. Overridden by the
 690       * child classes.
 691       * @return bool
 692       */
 693  	public function canFollowRedirects() {
 694          return true;
 695      }
 696  }
 697  
 698  /**
 699   * MWHttpRequest implemented using internal curl compiled into PHP
 700   */
 701  class CurlHttpRequest extends MWHttpRequest {
 702      const SUPPORTS_FILE_POSTS = true;
 703  
 704      protected $curlOptions = array();
 705      protected $headerText = "";
 706  
 707      /**
 708       * @param resource $fh
 709       * @param string $content
 710       * @return int
 711       */
 712  	protected function readHeader( $fh, $content ) {
 713          $this->headerText .= $content;
 714          return strlen( $content );
 715      }
 716  
 717  	public function execute() {
 718          wfProfileIn( __METHOD__ );
 719  
 720          parent::execute();
 721  
 722          if ( !$this->status->isOK() ) {
 723              wfProfileOut( __METHOD__ );
 724              return $this->status;
 725          }
 726  
 727          $this->curlOptions[CURLOPT_PROXY] = $this->proxy;
 728          $this->curlOptions[CURLOPT_TIMEOUT] = $this->timeout;
 729  
 730          // Only supported in curl >= 7.16.2
 731          if ( defined( 'CURLOPT_CONNECTTIMEOUT_MS' ) ) {
 732              $this->curlOptions[CURLOPT_CONNECTTIMEOUT_MS] = $this->connectTimeout * 1000;
 733          }
 734  
 735          $this->curlOptions[CURLOPT_HTTP_VERSION] = CURL_HTTP_VERSION_1_0;
 736          $this->curlOptions[CURLOPT_WRITEFUNCTION] = $this->callback;
 737          $this->curlOptions[CURLOPT_HEADERFUNCTION] = array( $this, "readHeader" );
 738          $this->curlOptions[CURLOPT_MAXREDIRS] = $this->maxRedirects;
 739          $this->curlOptions[CURLOPT_ENCODING] = ""; # Enable compression
 740  
 741          $this->curlOptions[CURLOPT_USERAGENT] = $this->reqHeaders['User-Agent'];
 742  
 743          $this->curlOptions[CURLOPT_SSL_VERIFYHOST] = $this->sslVerifyHost ? 2 : 0;
 744          $this->curlOptions[CURLOPT_SSL_VERIFYPEER] = $this->sslVerifyCert;
 745  
 746          if ( $this->caInfo ) {
 747              $this->curlOptions[CURLOPT_CAINFO] = $this->caInfo;
 748          }
 749  
 750          if ( $this->headersOnly ) {
 751              $this->curlOptions[CURLOPT_NOBODY] = true;
 752              $this->curlOptions[CURLOPT_HEADER] = true;
 753          } elseif ( $this->method == 'POST' ) {
 754              $this->curlOptions[CURLOPT_POST] = true;
 755              $this->curlOptions[CURLOPT_POSTFIELDS] = $this->postData;
 756              // Suppress 'Expect: 100-continue' header, as some servers
 757              // will reject it with a 417 and Curl won't auto retry
 758              // with HTTP 1.0 fallback
 759              $this->reqHeaders['Expect'] = '';
 760          } else {
 761              $this->curlOptions[CURLOPT_CUSTOMREQUEST] = $this->method;
 762          }
 763  
 764          $this->curlOptions[CURLOPT_HTTPHEADER] = $this->getHeaderList();
 765  
 766          $curlHandle = curl_init( $this->url );
 767  
 768          if ( !curl_setopt_array( $curlHandle, $this->curlOptions ) ) {
 769              wfProfileOut( __METHOD__ );
 770              throw new MWException( "Error setting curl options." );
 771          }
 772  
 773          if ( $this->followRedirects && $this->canFollowRedirects() ) {
 774              wfSuppressWarnings();
 775              if ( !curl_setopt( $curlHandle, CURLOPT_FOLLOWLOCATION, true ) ) {
 776                  wfDebug( __METHOD__ . ": Couldn't set CURLOPT_FOLLOWLOCATION. " .
 777                      "Probably safe_mode or open_basedir is set.\n" );
 778                  // Continue the processing. If it were in curl_setopt_array,
 779                  // processing would have halted on its entry
 780              }
 781              wfRestoreWarnings();
 782          }
 783  
 784          $curlRes = curl_exec( $curlHandle );
 785          if ( curl_errno( $curlHandle ) == CURLE_OPERATION_TIMEOUTED ) {
 786              $this->status->fatal( 'http-timed-out', $this->url );
 787          } elseif ( $curlRes === false ) {
 788              $this->status->fatal( 'http-curl-error', curl_error( $curlHandle ) );
 789          } else {
 790              $this->headerList = explode( "\r\n", $this->headerText );
 791          }
 792  
 793          curl_close( $curlHandle );
 794  
 795          $this->parseHeader();
 796          $this->setStatus();
 797  
 798          wfProfileOut( __METHOD__ );
 799  
 800          return $this->status;
 801      }
 802  
 803      /**
 804       * @return bool
 805       */
 806  	public function canFollowRedirects() {
 807          if ( strval( ini_get( 'open_basedir' ) ) !== '' || wfIniGetBool( 'safe_mode' ) ) {
 808              wfDebug( "Cannot follow redirects in safe mode\n" );
 809              return false;
 810          }
 811  
 812          $curlVersionInfo = curl_version();
 813          if ( $curlVersionInfo['version_number'] < 0x071304 ) {
 814              wfDebug( "Cannot follow redirects with libcurl < 7.19.4 due to CVE-2009-0037\n" );
 815              return false;
 816          }
 817  
 818          return true;
 819      }
 820  }
 821  
 822  class PhpHttpRequest extends MWHttpRequest {
 823  
 824      /**
 825       * @param string $url
 826       * @return string
 827       */
 828  	protected function urlToTcp( $url ) {
 829          $parsedUrl = parse_url( $url );
 830  
 831          return 'tcp://' . $parsedUrl['host'] . ':' . $parsedUrl['port'];
 832      }
 833  
 834  	public function execute() {
 835          wfProfileIn( __METHOD__ );
 836  
 837          parent::execute();
 838  
 839          if ( is_array( $this->postData ) ) {
 840              $this->postData = wfArrayToCgi( $this->postData );
 841          }
 842  
 843          if ( $this->parsedUrl['scheme'] != 'http'
 844              && $this->parsedUrl['scheme'] != 'https' ) {
 845              $this->status->fatal( 'http-invalid-scheme', $this->parsedUrl['scheme'] );
 846          }
 847  
 848          $this->reqHeaders['Accept'] = "*/*";
 849          $this->reqHeaders['Connection'] = 'Close';
 850          if ( $this->method == 'POST' ) {
 851              // Required for HTTP 1.0 POSTs
 852              $this->reqHeaders['Content-Length'] = strlen( $this->postData );
 853              if ( !isset( $this->reqHeaders['Content-Type'] ) ) {
 854                  $this->reqHeaders['Content-Type'] = "application/x-www-form-urlencoded";
 855              }
 856          }
 857  
 858          // Set up PHP stream context
 859          $options = array(
 860              'http' => array(
 861                  'method' => $this->method,
 862                  'header' => implode( "\r\n", $this->getHeaderList() ),
 863                  'protocol_version' => '1.1',
 864                  'max_redirects' => $this->followRedirects ? $this->maxRedirects : 0,
 865                  'ignore_errors' => true,
 866                  'timeout' => $this->timeout,
 867                  // Curl options in case curlwrappers are installed
 868                  'curl_verify_ssl_host' => $this->sslVerifyHost ? 2 : 0,
 869                  'curl_verify_ssl_peer' => $this->sslVerifyCert,
 870              ),
 871              'ssl' => array(
 872                  'verify_peer' => $this->sslVerifyCert,
 873                  'SNI_enabled' => true,
 874              ),
 875          );
 876  
 877          if ( $this->proxy ) {
 878              $options['http']['proxy'] = $this->urlToTCP( $this->proxy );
 879              $options['http']['request_fulluri'] = true;
 880          }
 881  
 882          if ( $this->postData ) {
 883              $options['http']['content'] = $this->postData;
 884          }
 885  
 886          if ( $this->sslVerifyHost ) {
 887              $options['ssl']['CN_match'] = $this->parsedUrl['host'];
 888          }
 889  
 890          if ( is_dir( $this->caInfo ) ) {
 891              $options['ssl']['capath'] = $this->caInfo;
 892          } elseif ( is_file( $this->caInfo ) ) {
 893              $options['ssl']['cafile'] = $this->caInfo;
 894          } elseif ( $this->caInfo ) {
 895              throw new MWException( "Invalid CA info passed: {$this->caInfo}" );
 896          }
 897  
 898          $context = stream_context_create( $options );
 899  
 900          $this->headerList = array();
 901          $reqCount = 0;
 902          $url = $this->url;
 903  
 904          $result = array();
 905  
 906          do {
 907              $reqCount++;
 908              wfSuppressWarnings();
 909              $fh = fopen( $url, "r", false, $context );
 910              wfRestoreWarnings();
 911  
 912              if ( !$fh ) {
 913                  break;
 914              }
 915  
 916              $result = stream_get_meta_data( $fh );
 917              $this->headerList = $result['wrapper_data'];
 918              $this->parseHeader();
 919  
 920              if ( !$this->followRedirects ) {
 921                  break;
 922              }
 923  
 924              # Handle manual redirection
 925              if ( !$this->isRedirect() || $reqCount > $this->maxRedirects ) {
 926                  break;
 927              }
 928              # Check security of URL
 929              $url = $this->getResponseHeader( "Location" );
 930  
 931              if ( !Http::isValidURI( $url ) ) {
 932                  wfDebug( __METHOD__ . ": insecure redirection\n" );
 933                  break;
 934              }
 935          } while ( true );
 936  
 937          $this->setStatus();
 938  
 939          if ( $fh === false ) {
 940              $this->status->fatal( 'http-request-error' );
 941              wfProfileOut( __METHOD__ );
 942              return $this->status;
 943          }
 944  
 945          if ( $result['timed_out'] ) {
 946              $this->status->fatal( 'http-timed-out', $this->url );
 947              wfProfileOut( __METHOD__ );
 948              return $this->status;
 949          }
 950  
 951          // If everything went OK, or we received some error code
 952          // get the response body content.
 953          if ( $this->status->isOK() || (int)$this->respStatus >= 300 ) {
 954              while ( !feof( $fh ) ) {
 955                  $buf = fread( $fh, 8192 );
 956  
 957                  if ( $buf === false ) {
 958                      $this->status->fatal( 'http-read-error' );
 959                      break;
 960                  }
 961  
 962                  if ( strlen( $buf ) ) {
 963                      call_user_func( $this->callback, $fh, $buf );
 964                  }
 965              }
 966          }
 967          fclose( $fh );
 968  
 969          wfProfileOut( __METHOD__ );
 970  
 971          return $this->status;
 972      }
 973  }


Generated: Fri Nov 28 14:03:12 2014 Cross-referenced by PHPXref 0.7.1