From 8ed46ccdf3feb5be13c2914279c7988564fe121c Mon Sep 17 00:00:00 2001 From: Eric GELOEN Date: Sat, 7 Mar 2015 21:52:45 +0100 Subject: [PATCH 1/2] Fix/Rationalize transports --- library/Requests.php | 35 +++++++++- library/Requests/Transport/cURL.php | 50 ++++++++------ library/Requests/Transport/fsockopen.php | 83 +++++++++++------------- 3 files changed, 102 insertions(+), 66 deletions(-) diff --git a/library/Requests.php b/library/Requests.php index 31eee40e6..16da02abb 100755 --- a/library/Requests.php +++ b/library/Requests.php @@ -54,6 +54,20 @@ class Requests { */ const DELETE = 'DELETE'; + /** + * OPTIONS method + * + * @var string + */ + const OPTIONS = 'OPTIONS'; + + /** + * TRACE method + * + * @var string + */ + const TRACE = 'TRACE'; + /** * PATCH method * @@ -181,7 +195,7 @@ protected static function get_transport($capabilities = array()) { if (self::$transport[$cap_string] === null) { throw new Requests_Exception('No working transports found', 'notransport', self::$transports); } - + return new self::$transport[$cap_string](); } @@ -235,6 +249,20 @@ public static function put($url, $headers = array(), $data = array(), $options = return self::request($url, $headers, $data, self::PUT, $options); } + /** + * Send an OPTIONS request + */ + public static function options($url, $headers = array(), $data = array(), $options = array()) { + return self::request($url, $headers, $data, self::OPTIONS, $options); + } + + /** + * Send a TRACE request + */ + public static function trace($url, $headers = array(), $data = array(), $options = array()) { + return self::request($url, $headers, $data, self::TRACE, $options); + } + /** * Send a PATCH request * @@ -450,6 +478,7 @@ protected static function get_default_options($multirequest = false) { 'timeout' => 10, 'connect_timeout' => 10, 'useragent' => 'php-requests/' . self::VERSION, + 'protocol_version' => 1.1, 'redirected' => 0, 'redirects' => 10, 'follow_redirects' => true, @@ -519,6 +548,10 @@ protected static function set_defaults(&$url, &$headers, &$data, &$type, &$optio $iri->host = Requests_IDNAEncoder::encode($iri->ihost); $url = $iri->uri; } + + if (!isset($options['data_as_query'])) { + $options['data_as_query'] = in_array($options['type'], array(Requests::HEAD, Requests::GET, Requests::DELETE)); + } } /** diff --git a/library/Requests/Transport/cURL.php b/library/Requests/Transport/cURL.php index 678ab7363..e46796d1b 100755 --- a/library/Requests/Transport/cURL.php +++ b/library/Requests/Transport/cURL.php @@ -64,19 +64,6 @@ class Requests_Transport_cURL implements Requests_Transport { public function __construct() { $curl = curl_version(); $this->version = $curl['version_number']; - $this->fp = curl_init(); - - curl_setopt($this->fp, CURLOPT_HEADER, false); - curl_setopt($this->fp, CURLOPT_RETURNTRANSFER, 1); - if ($this->version >= self::CURL_7_10_5) { - curl_setopt($this->fp, CURLOPT_ENCODING, ''); - } - if (defined('CURLOPT_PROTOCOLS')) { - curl_setopt($this->fp, CURLOPT_PROTOCOLS, CURLPROTO_HTTP | CURLPROTO_HTTPS); - } - if (defined('CURLOPT_REDIR_PROTOCOLS')) { - curl_setopt($this->fp, CURLOPT_REDIR_PROTOCOLS, CURLPROTO_HTTP | CURLPROTO_HTTPS); - } } /** @@ -225,14 +212,31 @@ public function &get_subrequest_handle($url, $headers, $data, $options) { * @param array $options Request options, see {@see Requests::response()} for documentation */ protected function setup_handle($url, $headers, $data, $options) { + $this->fp = curl_init(); + + curl_setopt($this->fp, CURLOPT_HEADER, false); + curl_setopt($this->fp, CURLOPT_RETURNTRANSFER, 1); + if ($this->version >= self::CURL_7_10_5) { + curl_setopt($this->fp, CURLOPT_ENCODING, ''); + } + if (defined('CURLOPT_PROTOCOLS')) { + curl_setopt($this->fp, CURLOPT_PROTOCOLS, CURLPROTO_HTTP | CURLPROTO_HTTPS); + } + if (defined('CURLOPT_REDIR_PROTOCOLS')) { + curl_setopt($this->fp, CURLOPT_REDIR_PROTOCOLS, CURLPROTO_HTTP | CURLPROTO_HTTPS); + } + $options['hooks']->dispatch('curl.before_request', array(&$this->fp)); $headers = Requests::flatten($headers); - if (in_array($options['type'], array(Requests::HEAD, Requests::GET, Requests::DELETE)) & !empty($data)) { - $url = self::format_get($url, $data); - } - elseif (!empty($data) && !is_string($data)) { - $data = http_build_query($data, null, '&'); + + if (!empty($data)) { + if ($options['data_as_query']) { + $url = self::format_get($url, $data); + $data = ''; + } elseif (!is_string($data)) { + $data = http_build_query($data, null, '&'); + } } switch ($options['type']) { @@ -242,15 +246,18 @@ protected function setup_handle($url, $headers, $data, $options) { break; case Requests::PATCH: case Requests::PUT: + case Requests::DELETE: + case Requests::OPTIONS: curl_setopt($this->fp, CURLOPT_CUSTOMREQUEST, $options['type']); curl_setopt($this->fp, CURLOPT_POSTFIELDS, $data); break; - case Requests::DELETE: - curl_setopt($this->fp, CURLOPT_CUSTOMREQUEST, 'DELETE'); - break; case Requests::HEAD: + curl_setopt($this->fp, CURLOPT_CUSTOMREQUEST, $options['type']); curl_setopt($this->fp, CURLOPT_NOBODY, true); break; + case Requests::TRACE: + curl_setopt($this->fp, CURLOPT_CUSTOMREQUEST, $options['type']); + break; } if( is_int($options['timeout']) or $this->version < self::CURL_7_16_2 ) { @@ -267,6 +274,7 @@ protected function setup_handle($url, $headers, $data, $options) { curl_setopt($this->fp, CURLOPT_REFERER, $url); curl_setopt($this->fp, CURLOPT_USERAGENT, $options['useragent']); curl_setopt($this->fp, CURLOPT_HTTPHEADER, $headers); + curl_setopt($this->fp, CURLOPT_HTTP_VERSION, $options['protocol_version'] == 1.1 ? CURL_HTTP_VERSION_1_1 : CURL_HTTP_VERSION_1_0); if (true === $options['blocking']) { curl_setopt($this->fp, CURLOPT_HEADERFUNCTION, array(&$this, 'stream_headers')); diff --git a/library/Requests/Transport/fsockopen.php b/library/Requests/Transport/fsockopen.php index 8975e2207..79b7b5531 100755 --- a/library/Requests/Transport/fsockopen.php +++ b/library/Requests/Transport/fsockopen.php @@ -48,7 +48,8 @@ class Requests_Transport_fsockopen implements Requests_Transport { * @param array $options Request options, see {@see Requests::response()} for documentation * @return string Raw HTTP result */ - public function request($url, $headers = array(), $data = array(), $options = array()) { + public function request($url, $headers = array(), $data = array(), $options = array()) + { $options['hooks']->dispatch('fsockopen.before_request'); $url_parts = parse_url($url); @@ -89,13 +90,12 @@ public function request($url, $headers = array(), $data = array(), $options = ar } stream_context_set_option($context, array('ssl' => $context_options)); - } - else { + } else { $remote_socket = 'tcp://' . $host; } - $proxy = isset( $options['proxy'] ); - $proxy_auth = $proxy && isset( $options['proxy_username'] ) && isset( $options['proxy_password'] ); + $proxy = isset($options['proxy']); + $proxy_auth = $proxy && isset($options['proxy_username']) && isset($options['proxy_password']); if (!isset($url_parts['port'])) { $url_parts['port'] = 80; @@ -110,10 +110,8 @@ public function request($url, $headers = array(), $data = array(), $options = ar restore_error_handler(); - if ($verifyname) { - if (!$this->verify_certificate_from_context($host, $context)) { - throw new Requests_Exception('SSL certificate did not match the requested domain name', 'ssl.no_match'); - } + if ($verifyname && !$this->verify_certificate_from_context($host, $context)) { + throw new Requests_Exception('SSL certificate did not match the requested domain name', 'ssl.no_match'); } if (!$fp) { @@ -121,51 +119,39 @@ public function request($url, $headers = array(), $data = array(), $options = ar // Connection issue throw new Requests_Exception(rtrim($this->connect_error), 'fsockopen.connect_error'); } - else { - throw new Requests_Exception($errstr, 'fsockopenerror'); - return; - } + + throw new Requests_Exception($errstr, 'fsockopenerror'); } $request_body = ''; $out = ''; - switch ($options['type']) { - case Requests::POST: - case Requests::PUT: - case Requests::PATCH: - if (isset($url_parts['path'])) { - $path = $url_parts['path']; - if (isset($url_parts['query'])) { - $path .= '?' . $url_parts['query']; - } - } - else { - $path = '/'; - } - $options['hooks']->dispatch( 'fsockopen.remote_host_path', array( &$path, $url ) ); - $out = $options['type'] . " $path HTTP/1.0\r\n"; + if ($options['data_as_query']) { + $path = self::format_get($url_parts, $data); + $data = ''; + } else { + $path = self::format_get($url_parts, array()); + } - if (is_array($data)) { - $request_body = http_build_query($data, null, '&'); - } - else { - $request_body = $data; - } + $options['hooks']->dispatch('fsockopen.remote_host_path', array(&$path, $url)); + $out = $options['type'] . " $path HTTP/" . $options['protocol_version'] . "\r\n"; + + if ($options['type'] !== Requests::TRACE) { + if (is_array($data)) { + $request_body = http_build_query($data, null, '&'); + } else { + $request_body = $data; + } + + if (!empty($data)) { if (empty($headers['Content-Length'])) { $headers['Content-Length'] = strlen($request_body); } + if (empty($headers['Content-Type'])) { $headers['Content-Type'] = 'application/x-www-form-urlencoded; charset=UTF-8'; } - break; - case Requests::HEAD: - case Requests::GET: - case Requests::DELETE: - $path = self::format_get($url_parts, $data); - $options['hooks']->dispatch('fsockopen.remote_host_path', array(&$path, $url)); - $out = $options['type'] . " $path HTTP/1.0\r\n"; - break; + } } $out .= "Host: {$url_parts['host']}"; @@ -174,7 +160,12 @@ public function request($url, $headers = array(), $data = array(), $options = ar } $out .= "\r\n"; - $out .= "User-Agent: {$options['useragent']}\r\n"; + $lowerHeaders = array_change_key_case($headers); + + if (!isset($lowerHeaders['user-agent'])) { + $out .= "User-Agent: {$options['useragent']}\r\n"; + } + $accept_encoding = $this->accept_encoding(); if (!empty($accept_encoding)) { $out .= "Accept-Encoding: $accept_encoding\r\n"; @@ -192,7 +183,11 @@ public function request($url, $headers = array(), $data = array(), $options = ar $out .= "\r\n"; } - $out .= "Connection: Close\r\n\r\n" . $request_body; + if (!isset($lowerHeaders['connection'])) { + $out .= "Connection: Close\r\n"; + } + + $out .= "\r\n" . $request_body; $options['hooks']->dispatch('fsockopen.before_send', array(&$out)); From 9e7791a942542fc23a472e00cc0911fa988345d4 Mon Sep 17 00:00:00 2001 From: Eric GELOEN Date: Sat, 18 Apr 2015 15:15:23 +0200 Subject: [PATCH 2/2] Add tests --- .gitignore | 3 +++ tests/Transport/Base.php | 39 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 42 insertions(+) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..b0febf77d --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +/composer.lock +/vendor +/tests/coverage diff --git a/tests/Transport/Base.php b/tests/Transport/Base.php index b9bd437e0..bf58b1919 100755 --- a/tests/Transport/Base.php +++ b/tests/Transport/Base.php @@ -110,6 +110,11 @@ public function testHEAD() { $this->assertEquals('', $request->body); } + public function testTRACE() { + $request = Requests::trace(httpbin('/get'), array(), $this->getOptions()); + $this->assertEquals(200, $request->status_code); + } + public function testRawPOST() { $data = 'test'; $request = Requests::post(httpbin('/post'), array(), $data, $this->getOptions()); @@ -215,6 +220,11 @@ public function testPATCHWithArray() { $this->assertEquals(array('test' => 'true', 'test2' => 'test'), $result['form']); } + public function testOPTIONS() { + $request = Requests::options(httpbin('/post'), array(), array(), $this->getOptions()); + $this->assertEquals(200, $request->status_code); + } + public function testDELETE() { $request = Requests::delete(httpbin('/delete'), array(), $this->getOptions()); $this->assertEquals(200, $request->status_code); @@ -680,4 +690,33 @@ public function testHostHeader() { $portXpathMatches = $portXpath->query('//p/b'); $this->assertEquals(8080, $portXpathMatches->item(0)->nodeValue); } + + public function testReusableTransport() { + $options = $this->getOptions(array('transport' => new $this->transport())); + + $request1 = Requests::get(httpbin('/get'), array(), $options); + $request2 = Requests::get(httpbin('/get'), array(), $options); + + $this->assertEquals(200, $request1->status_code); + $this->assertEquals(200, $request2->status_code); + + $result1 = json_decode($request1->body, true); + $result2 = json_decode($request2->body, true); + + $this->assertEquals(httpbin('/get'), $result1['url']); + $this->assertEquals(httpbin('/get'), $result2['url']); + + $this->assertEmpty($result1['args']); + $this->assertEmpty($result2['args']); + } + + public function testDataAsQuery() { + $data = array('test' => 'true', 'test2' => 'test'); + $request = Requests::post(httpbin('/post'), array(), $data, $this->getOptions(array('data_as_query' => true))); + $this->assertEquals(200, $request->status_code); + + $result = json_decode($request->body, true); + $this->assertEquals(httpbin('/post').'?test=true&test2=test', $result['url']); + $this->assertEquals('', $result['data']); + } }