1<?php 2/** 3 * This program is free software; you can redistribute it and/or modify 4 * it under the terms of the GNU General Public License as published by 5 * the Free Software Foundation; either version 2 of the License, or 6 * (at your option) any later version. 7 * 8 * This program is distributed in the hope that it will be useful, 9 * but WITHOUT ANY WARRANTY; without even the implied warranty of 10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 * GNU General Public License for more details. 12 * 13 * You should have received a copy of the GNU General Public License along 14 * with this program; if not, write to the Free Software Foundation, Inc., 15 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 16 * http://www.gnu.org/copyleft/gpl.html 17 * 18 * @file 19 */ 20 21/** 22 * MWHttpRequest implemented using internal curl compiled into PHP 23 */ 24class CurlHttpRequest extends MWHttpRequest { 25 public const SUPPORTS_FILE_POSTS = true; 26 27 protected $curlOptions = []; 28 protected $headerText = ""; 29 30 /** 31 * @internal Use HttpRequestFactory 32 * @throws RuntimeException 33 */ 34 public function __construct() { 35 if ( !function_exists( 'curl_init' ) ) { 36 throw new RuntimeException( 37 __METHOD__ . ': curl (https://www.php.net/curl) is not installed' ); 38 } 39 40 parent::__construct( ...func_get_args() ); 41 } 42 43 /** 44 * @param resource $fh 45 * @param string $content 46 * @return int 47 */ 48 protected function readHeader( $fh, $content ) { 49 $this->headerText .= $content; 50 return strlen( $content ); 51 } 52 53 /** 54 * @see MWHttpRequest::execute 55 * 56 * @throws MWException 57 * @return Status 58 */ 59 public function execute() { 60 $this->prepare(); 61 62 if ( !$this->status->isOK() ) { 63 return Status::wrap( $this->status ); // TODO B/C; move this to callers 64 } 65 66 $this->curlOptions[CURLOPT_PROXY] = $this->proxy; 67 $this->curlOptions[CURLOPT_TIMEOUT] = $this->timeout; 68 $this->curlOptions[CURLOPT_CONNECTTIMEOUT_MS] = $this->connectTimeout * 1000; 69 $this->curlOptions[CURLOPT_HTTP_VERSION] = CURL_HTTP_VERSION_1_0; 70 $this->curlOptions[CURLOPT_WRITEFUNCTION] = $this->callback; 71 $this->curlOptions[CURLOPT_HEADERFUNCTION] = [ $this, "readHeader" ]; 72 $this->curlOptions[CURLOPT_MAXREDIRS] = $this->maxRedirects; 73 $this->curlOptions[CURLOPT_ENCODING] = ""; # Enable compression 74 75 $this->curlOptions[CURLOPT_USERAGENT] = $this->reqHeaders['User-Agent']; 76 77 $this->curlOptions[CURLOPT_SSL_VERIFYHOST] = $this->sslVerifyHost ? 2 : 0; 78 $this->curlOptions[CURLOPT_SSL_VERIFYPEER] = $this->sslVerifyCert; 79 80 if ( $this->caInfo ) { 81 $this->curlOptions[CURLOPT_CAINFO] = $this->caInfo; 82 } 83 84 if ( $this->headersOnly ) { 85 $this->curlOptions[CURLOPT_NOBODY] = true; 86 $this->curlOptions[CURLOPT_HEADER] = true; 87 } elseif ( $this->method == 'POST' ) { 88 $this->curlOptions[CURLOPT_POST] = true; 89 $postData = $this->postData; 90 // Don't interpret POST parameters starting with '@' as file uploads, because this 91 // makes it impossible to POST plain values starting with '@' (and causes security 92 // issues potentially exposing the contents of local files). 93 $this->curlOptions[CURLOPT_SAFE_UPLOAD] = true; 94 $this->curlOptions[CURLOPT_POSTFIELDS] = $postData; 95 96 // Suppress 'Expect: 100-continue' header, as some servers 97 // will reject it with a 417 and Curl won't auto retry 98 // with HTTP 1.0 fallback 99 $this->reqHeaders['Expect'] = ''; 100 } else { 101 $this->curlOptions[CURLOPT_CUSTOMREQUEST] = $this->method; 102 } 103 104 $this->curlOptions[CURLOPT_HTTPHEADER] = $this->getHeaderList(); 105 106 $curlHandle = curl_init( $this->url ); 107 108 if ( !curl_setopt_array( $curlHandle, $this->curlOptions ) ) { 109 $this->status->fatal( 'http-internal-error' ); 110 throw new InvalidArgumentException( "Error setting curl options." ); 111 } 112 113 if ( $this->followRedirects && $this->canFollowRedirects() ) { 114 Wikimedia\suppressWarnings(); 115 if ( !curl_setopt( $curlHandle, CURLOPT_FOLLOWLOCATION, true ) ) { 116 $this->logger->debug( __METHOD__ . ": Couldn't set CURLOPT_FOLLOWLOCATION. " . 117 "Probably open_basedir is set." ); 118 // Continue the processing. If it were in curl_setopt_array, 119 // processing would have halted on its entry 120 } 121 Wikimedia\restoreWarnings(); 122 } 123 124 if ( $this->profiler ) { 125 $profileSection = $this->profiler->scopedProfileIn( 126 __METHOD__ . '-' . $this->profileName 127 ); 128 } 129 130 $curlRes = curl_exec( $curlHandle ); 131 if ( curl_errno( $curlHandle ) == CURLE_OPERATION_TIMEOUTED ) { 132 $this->status->fatal( 'http-timed-out', $this->url ); 133 } elseif ( $curlRes === false ) { 134 $this->status->fatal( 'http-curl-error', curl_error( $curlHandle ) ); 135 } else { 136 $this->headerList = explode( "\r\n", $this->headerText ); 137 } 138 139 curl_close( $curlHandle ); 140 141 if ( $this->profiler ) { 142 $this->profiler->scopedProfileOut( $profileSection ); 143 } 144 145 $this->parseHeader(); 146 $this->setStatus(); 147 148 return Status::wrap( $this->status ); // TODO B/C; move this to callers 149 } 150 151 /** 152 * @return bool 153 */ 154 public function canFollowRedirects() { 155 $curlVersionInfo = curl_version(); 156 if ( $curlVersionInfo['version_number'] < 0x071304 ) { 157 $this->logger->debug( "Cannot follow redirects with libcurl < 7.19.4 due to CVE-2009-0037" ); 158 return false; 159 } 160 161 return true; 162 } 163} 164