mirror of
https://github.com/processwire/processwire.git
synced 2025-08-13 18:24:57 +02:00
Improve the WireHttp class by adding a CURL-based get(), post(), head() or send() fallback that is used (if CURL available) when the fopen() method fails. It can also be f orced from the new $options argument added to all the sending methods.
This commit is contained in:
@@ -22,10 +22,11 @@
|
||||
*
|
||||
* Thanks to @horst for his assistance with several methods in this class.
|
||||
*
|
||||
* ProcessWire 3.x, Copyright 2018 by Ryan Cramer
|
||||
* ProcessWire 3.x, Copyright 2019 by Ryan Cramer
|
||||
* https://processwire.com
|
||||
*
|
||||
* @method bool|string send($url, $data = array(), $method = 'POST')
|
||||
* @method bool|string send($url, $data = array(), $method = 'POST', array $options = array())
|
||||
*
|
||||
*
|
||||
*/
|
||||
|
||||
@@ -243,6 +244,14 @@ class WireHttp extends Wire {
|
||||
*/
|
||||
protected $hasFopen = false;
|
||||
|
||||
/**
|
||||
* Last type used for send (fopen, socket, curl)
|
||||
*
|
||||
* @var string
|
||||
*
|
||||
*/
|
||||
protected $lastSendType = '';
|
||||
|
||||
/**
|
||||
* Options to pass to $sanitizer->url('url', $options) in WireHttp::validateURL() method
|
||||
*
|
||||
@@ -287,12 +296,14 @@ class WireHttp extends Wire {
|
||||
*
|
||||
* @param string $url URL to post to (including http:// or https://)
|
||||
* @param mixed $data Associative array of data to send (if not already set before), or raw data to send.
|
||||
* @param array $options Optional options to modify default behavior, see the send() method for details.
|
||||
* @return bool|string False on failure or string of contents received on success.
|
||||
* @see WireHttp::send(), WireHttp::get(), WireHttp::head()
|
||||
*
|
||||
*/
|
||||
public function post($url, $data = array()) {
|
||||
public function post($url, $data = array(), array $options = array()) {
|
||||
if(!isset($this->headers['content-type'])) $this->setHeader('content-type', 'application/x-www-form-urlencoded; charset=utf-8');
|
||||
return $this->send($url, $data, 'POST');
|
||||
return $this->send($url, $data, 'POST', $options);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -312,11 +323,13 @@ class WireHttp extends Wire {
|
||||
*
|
||||
* @param string $url URL to send request to (including http:// or https://)
|
||||
* @param mixed $data Array of data to send (if not already set before) or raw data to send.
|
||||
* @param array $options Optional options to modify default behavior, see the send() method for details.
|
||||
* @return bool|string False on failure or string of contents received on success.
|
||||
* @see WireHttp::send(), WireHttp::post(), WireHttp::head(), WireHttp::getJSON()
|
||||
*
|
||||
*/
|
||||
public function get($url, $data = array()) {
|
||||
return $this->send($url, $data, 'GET');
|
||||
public function get($url, $data = array(), array $options = array()) {
|
||||
return $this->send($url, $data, 'GET', $options);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -325,11 +338,13 @@ class WireHttp extends Wire {
|
||||
* @param string $url URL to send request to (including http:// or https://)
|
||||
* @param bool $assoc Default is to return an array (specified by TRUE). If you want an object instead, specify FALSE.
|
||||
* @param mixed $data Array of data to send (if not already set before) or raw data to send
|
||||
* @return bool|array|object False on failure or an array or object on success.
|
||||
* @param array $options Optional options to modify default behavior, see the send() method for details.
|
||||
* @return bool|array|object False on failure or an array or object on success.
|
||||
* @see WireHttp::send(), WireHttp::get()
|
||||
*
|
||||
*/
|
||||
public function getJSON($url, $assoc = true, $data = array()) {
|
||||
return json_decode($this->get($url, $data), $assoc);
|
||||
public function getJSON($url, $assoc = true, $data = array(), array $options = array()) {
|
||||
return json_decode($this->get($url, $data, $options), $assoc);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -337,11 +352,13 @@ class WireHttp extends Wire {
|
||||
*
|
||||
* @param string $url URL to request (including http:// or https://)
|
||||
* @param mixed $data Array of data to send (if not already set before) or raw data to send
|
||||
* @param array $options Optional options to modify default behavior, see the send() method for details.
|
||||
* @return bool|array False on failure or Arrray with ResponseHeaders on success.
|
||||
* @see WireHttp::send(), WireHttp::post(), WireHttp::get()
|
||||
*
|
||||
*/
|
||||
public function head($url, $data = array()) {
|
||||
$this->send($url, $data, 'HEAD');
|
||||
public function head($url, $data = array(), array $options = array()) {
|
||||
$this->send($url, $data, 'HEAD', $options);
|
||||
$responseHeaders = $this->getResponseHeaders();
|
||||
return is_array($responseHeaders) ? $responseHeaders : false;
|
||||
}
|
||||
@@ -352,11 +369,13 @@ class WireHttp extends Wire {
|
||||
* @param string $url URL to request (including http:// or https://)
|
||||
* @param mixed $data Array of data to send (if not already set before) or raw data
|
||||
* @param bool $textMode When true function will return a string rather than integer, see the statusText() method.
|
||||
* @param array $options Optional options to modify default behavior, see the send() method for details.
|
||||
* @return bool|integer|string False on failure or integer or string of status code (200|404|etc) on success.
|
||||
* @see WireHttp::send(), WireHttp::statusText()
|
||||
*
|
||||
*/
|
||||
public function status($url, $data = array(), $textMode = false) {
|
||||
$this->send($url, $data, 'HEAD');
|
||||
public function status($url, $data = array(), $textMode = false, array $options = array()) {
|
||||
$this->send($url, $data, 'HEAD', $options);
|
||||
return $this->getHttpCode($textMode);
|
||||
}
|
||||
|
||||
@@ -365,12 +384,14 @@ class WireHttp extends Wire {
|
||||
*
|
||||
* @param string $url URL to request (including http:// or https://)
|
||||
* @param mixed $data Array of data to send (if not already set before) or raw data
|
||||
* @param array $options Optional options to modify default behavior, see the send() method for details.
|
||||
* @return bool|string False on failure or string of status code + text on success.
|
||||
* Example: "200 OK', "302 Found", "404 Not Found"
|
||||
* @see WireHttp::send(), WireHttp::status()
|
||||
*
|
||||
*/
|
||||
public function statusText($url, $data = array()) {
|
||||
return $this->status($url, $data, true);
|
||||
public function statusText($url, $data = array(), array $options = array()) {
|
||||
return $this->status($url, $data, true, $options);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -455,68 +476,128 @@ class WireHttp extends Wire {
|
||||
* @param string $url URL to send to (including http:// or https://).
|
||||
* @param array $data Array of data to send (if not already set before).
|
||||
* @param string $method Method to use (either POST, GET, PUT, DELETE or others as needed).
|
||||
* @param array|string $options Options to modify behavior (this argument added in 3.0.124):
|
||||
* - `use` (string): What handler to use, one of 'auto', 'fopen', 'curl' or 'socket' (default='auto')
|
||||
* If the 'auto' option is used, the method will first try fopen and then fallback to curl and sockets unless 'fallback' is disabled.
|
||||
* - `fallback` (bool|string): Allow fallback to other methods? Applies only if 'use' option is 'auto'. (default=true)
|
||||
* For a specific fallback method specify 'socket' or 'curl'
|
||||
* @return bool|string False on failure or string of contents received on success.
|
||||
*
|
||||
*/
|
||||
public function ___send($url, $data = array(), $method = 'POST') {
|
||||
public function ___send($url, $data = array(), $method = 'POST', array $options = array()) {
|
||||
|
||||
$defaults = array(
|
||||
'use' => 'auto',
|
||||
'fallback' => 'auto', // false, 'auto', 'socket' or 'curl'
|
||||
'_url' => $url, // original unmodified URL
|
||||
);
|
||||
|
||||
$url = $this->validateURL($url, false);
|
||||
$options = array_merge($defaults, $options);
|
||||
$url = $this->validateURL($url, false);
|
||||
$allowFopen = $this->hasFopen;
|
||||
$result = false;
|
||||
|
||||
if(empty($url)) return false;
|
||||
$this->resetResponse();
|
||||
$unmodifiedURL = $url;
|
||||
|
||||
|
||||
if(!empty($data)) $this->setData($data);
|
||||
|
||||
if(!isset($this->headers['user-agent'])) {
|
||||
// some web servers deliver a 400 error if no user-agent set in request header, so make sure one is set
|
||||
$this->setHeader('user-agent',
|
||||
'ProcessWire/' . ProcessWire::versionMajor . '.' . ProcessWire::versionMinor . ' (' . $this->className() . ')'
|
||||
);
|
||||
}
|
||||
|
||||
if(!isset($this->headers['user-agent'])) $this->setHeader('user-agent', $this->getUserAgent());
|
||||
if(!in_array(strtoupper($method), $this->allowHttpMethods)) $method = 'POST';
|
||||
if($allowFopen && strpos($url, 'https://') === 0 && !extension_loaded('openssl')) $allowFopen = false;
|
||||
|
||||
if(!$this->hasFopen || strpos($url, 'https://') === 0 && !extension_loaded('openssl')) {
|
||||
if($options['use'] === 'socket') {
|
||||
// force socket
|
||||
return $this->sendSocket($url, $method);
|
||||
|
||||
} else if($options['use'] === 'curl') {
|
||||
// force curl
|
||||
if(!$this->hasCURL) {
|
||||
$this->error[] = 'CURL is not available';
|
||||
return false;
|
||||
}
|
||||
return $this->sendCURL($url, $method, $options);
|
||||
|
||||
} else if($options['use'] === 'fopen' && !$allowFopen) {
|
||||
$this->error[] = 'fopen is not available';
|
||||
return false;
|
||||
}
|
||||
|
||||
if($allowFopen) {
|
||||
$result = $this->sendFopen($url, $method, $options);
|
||||
} else if($options['fallback'] === false) {
|
||||
$this->error[] = 'fopen not available and fallback option is disabled';
|
||||
}
|
||||
|
||||
if($result === false && $options['fallback'] !== false) {
|
||||
// on fopen fail fallback to CURL then sockets
|
||||
if($this->hasCURL && $options['fallback'] !== 'socket') {
|
||||
$result = $this->sendCURL($url, $method, $options);
|
||||
}
|
||||
if($result === false && $options['fallback'] !== 'curl') {
|
||||
$result = $this->sendSocket($options['_url'], $method);
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send using fopen
|
||||
*
|
||||
* @param string $url
|
||||
* @param string $method
|
||||
* @param array $options
|
||||
*
|
||||
* @return bool|string
|
||||
*
|
||||
*/
|
||||
protected function sendFopen($url, $method = 'POST', array $options = array()) {
|
||||
|
||||
if($options) { /* ignore */ }
|
||||
|
||||
$this->resetResponse();
|
||||
$this->lastSendType = 'fopen';
|
||||
|
||||
if(!empty($this->data)) {
|
||||
$content = http_build_query($this->data);
|
||||
if($method === 'GET' && strlen($content)) {
|
||||
$url .= (strpos($url, '?') === false ? '?' : '&') . $content;
|
||||
$content = http_build_query($this->data);
|
||||
if(($method === 'GET' || $method === 'HEAD') && strlen($content)) {
|
||||
$url .= (strpos($url, '?') === false ? '?' : '&') . $content;
|
||||
$content = '';
|
||||
}
|
||||
} else if(!empty($this->rawData)) {
|
||||
$content = $this->rawData;
|
||||
$content = $this->rawData;
|
||||
} else {
|
||||
$content = '';
|
||||
}
|
||||
|
||||
$this->setHeader('content-length', strlen($content));
|
||||
$this->setHeader('content-length', strlen($content));
|
||||
|
||||
$header = '';
|
||||
foreach($this->headers as $key => $value) $header .= "$key: $value\r\n";
|
||||
foreach($this->headers as $key => $value) {
|
||||
$header .= "$key: $value\r\n";
|
||||
}
|
||||
|
||||
$header .= "Connection: close\r\n";
|
||||
|
||||
$options = array(
|
||||
'http' => array(
|
||||
'http' => array(
|
||||
'method' => $method,
|
||||
'timeout' => $this->getTimeout(),
|
||||
'timeout' => $this->getTimeout(),
|
||||
'content' => $content,
|
||||
'header' => $header,
|
||||
)
|
||||
);
|
||||
)
|
||||
);
|
||||
|
||||
set_error_handler(array($this, '_errorHandler'));
|
||||
$context = stream_context_create($options);
|
||||
$context = stream_context_create($options);
|
||||
$fp = fopen($url, 'rb', false, $context);
|
||||
restore_error_handler();
|
||||
|
||||
if(isset($http_response_header)) $this->setResponseHeader($http_response_header);
|
||||
|
||||
if(isset($http_response_header)) $this->setResponseHeader($http_response_header);
|
||||
|
||||
if($fp) {
|
||||
$result = @stream_get_contents($fp);
|
||||
|
||||
$result = @stream_get_contents($fp);
|
||||
|
||||
} else {
|
||||
$code = $this->getHttpCode();
|
||||
if($code && $code >= 400 && isset($this->httpCodes[$code])) {
|
||||
@@ -526,8 +607,7 @@ class WireHttp extends Wire {
|
||||
// PR #1281: known http success status code, no need to fallback to sockets
|
||||
$result = true;
|
||||
} else {
|
||||
// fallback to sockets
|
||||
$result = $this->sendSocket($unmodifiedURL, $method);
|
||||
$result = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -535,42 +615,94 @@ class WireHttp extends Wire {
|
||||
}
|
||||
|
||||
/**
|
||||
* Send using CURL (coming soon)
|
||||
* Send using CURL
|
||||
*
|
||||
* @param string $url
|
||||
* @param string $method
|
||||
* @param array $options
|
||||
* @return bool|string
|
||||
*
|
||||
*/
|
||||
protected function sendCURL($url, $method = 'POST', $options = array()) {
|
||||
|
||||
$this->resetResponse();
|
||||
$this->lastSendType = 'curl';
|
||||
$timeout = isset($options['timeout']) ? (float) $options['timeout'] : $this->getTimeout();
|
||||
if(!in_array(strtoupper($method), $this->allowHttpMethods)) $method = 'POST';
|
||||
|
||||
$curl = curl_init($url);
|
||||
$curl = curl_init();
|
||||
|
||||
curl_setopt($curl, CURLOPT_CONNECTTIMEOUT, $timeout);
|
||||
curl_setopt($curl, CURLOPT_TIMEOUT, $timeout);
|
||||
curl_setopt($curl, CURLOPT_FOLLOWLOCATION, true);
|
||||
if($method == 'POST') curl_setopt($curl, CURLOPT_POST, true);
|
||||
else if($method == 'PUT') curl_setopt($curl, CURLOPT_PUT, true);
|
||||
else curl_setopt($curl, CURLOPT_GET, true);
|
||||
curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
|
||||
|
||||
curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($curl, CURLOPT_SAFE_UPLOAD, true);
|
||||
curl_setopt($curl, CURLOPT_USERAGENT, $this->getUserAgent());
|
||||
|
||||
if(count($this->headers)) {
|
||||
curl_setopt($curl, CURLOPT_HTTPHEADER, $this->headers);
|
||||
}
|
||||
|
||||
if($method == 'POST') {
|
||||
curl_setopt($curl, CURLOPT_POST, true);
|
||||
} else if($method == 'PUT') {
|
||||
curl_setopt($curl, CURLOPT_PUT, true);
|
||||
} else if($method == 'HEAD') {
|
||||
curl_setopt($curl, CURLOPT_NOBODY, true);
|
||||
} else {
|
||||
curl_setopt($curl, CURLOPT_HTTPGET, true);
|
||||
}
|
||||
|
||||
// @felixwahner #1027
|
||||
if(isset($options['http']) && isset($options['http']['proxy']) && !is_null($options['http']['proxy'])) {
|
||||
curl_setopt($curl, CURLOPT_PROXY, $options['http']['proxy']);
|
||||
}
|
||||
|
||||
if(!empty($this->data)) {
|
||||
if($method === 'POST') {
|
||||
curl_setopt($curl, CURLOPT_POSTFIELDS, $this->data);
|
||||
} else {
|
||||
$content = http_build_query($this->data);
|
||||
if(strlen($content)) $url .= (strpos($url, '?') === false ? '?' : '&') . $content;
|
||||
}
|
||||
} else if(!empty($this->rawData)) {
|
||||
if($method === 'POST') {
|
||||
curl_setopt($curl, CURLOPT_POSTFIELDS, $this->rawData);
|
||||
} else {
|
||||
throw new WireException("Raw data option with CURL not supported for $method");
|
||||
}
|
||||
}
|
||||
|
||||
// called by CURL for each header and populates the $responseHeaders var
|
||||
$responseHeaders = array();
|
||||
curl_setopt($curl, CURLOPT_HEADERFUNCTION, function($curl, $header) use(&$responseHeaders) {
|
||||
if($curl) { /* ignore */ }
|
||||
$length = strlen($header);
|
||||
$header = explode(':', $header, 2);
|
||||
if(count($header) < 2) return $length; // ignore invalid headers
|
||||
$name = strtolower(trim($header[0]));
|
||||
$value = trim($header[1]);
|
||||
if(!array_key_exists($name, $responseHeaders)) {
|
||||
$responseHeaders[$name] = array($value);
|
||||
} else {
|
||||
$responseHeaders[$name][] = $value;
|
||||
}
|
||||
return $length;
|
||||
});
|
||||
|
||||
curl_setopt($curl, CURLOPT_URL, $url);
|
||||
$result = curl_exec($curl);
|
||||
if($result) $this->httpCode = curl_getinfo($curl, CURLINFO_HTTP_CODE);
|
||||
if($result === false) $this->error[] = curl_error($curl);
|
||||
|
||||
if($result === false) {
|
||||
$this->error[] = curl_error($curl);
|
||||
$this->httpCode = 0;
|
||||
} else {
|
||||
$this->setResponseHeaderValues($responseHeaders);
|
||||
$this->httpCode = curl_getinfo($curl, CURLINFO_HTTP_CODE);
|
||||
}
|
||||
|
||||
curl_close($curl);
|
||||
|
||||
return $result;
|
||||
}
|
||||
*/
|
||||
|
||||
/**
|
||||
* Alternate method of sending when allow_url_fopen isn't allowed
|
||||
@@ -587,6 +719,7 @@ class WireHttp extends Wire {
|
||||
static $level = 0; // recursion level
|
||||
|
||||
$this->resetResponse();
|
||||
$this->lastSendType = 'socket';
|
||||
$timeout = isset($options['timeout']) ? (float) $options['timeout'] : $this->getTimeout();
|
||||
if(!in_array(strtoupper($method), $this->allowHttpMethods)) $method = 'POST';
|
||||
|
||||
@@ -671,7 +804,7 @@ class WireHttp extends Wire {
|
||||
* @param string $fromURL URL of file you want to download.
|
||||
* @param string $toFile Filename you want to save it to (including full path).
|
||||
* @param array $options Optional aptions array for PHP's stream_context_create(), plus these optional options:
|
||||
* - `useMethod` (string): Specify "curl", "fopen" or "socket" to force a specific method (default=auto-detect).
|
||||
* - `use` or `useMethod` (string): Specify "curl", "fopen" or "socket" to force a specific method (default=auto-detect).
|
||||
* - `timeout` (float): Number of seconds till timeout.
|
||||
* @return string Filename that was downloaded (including full path).
|
||||
* @throws WireException All error conditions throw exceptions.
|
||||
@@ -696,10 +829,15 @@ class WireHttp extends Wire {
|
||||
$options['timeout'] = $this->timeout;
|
||||
}
|
||||
}
|
||||
|
||||
// the 'use' option can also be specified as a 'useMethod' option
|
||||
if(isset($options['useMethod']) && !isset($options['use'])) {
|
||||
$options['use'] = $options['useMethod'];
|
||||
}
|
||||
|
||||
if(isset($options['useMethod'])) {
|
||||
$useMethod = $options['useMethod'];
|
||||
unset($options['useMethod']);
|
||||
if(isset($options['use'])) {
|
||||
$useMethod = $options['use'];
|
||||
unset($options['use']);
|
||||
if(!in_array($useMethod, $allowMethods)) throw new WireException("Unrecognized useMethod: $useMethod");
|
||||
if($useMethod == 'curl' && !$this->hasCURL) throw new WireException("System does not support CURL");
|
||||
if($useMethod == 'fopen' && !$this->hasFopen) throw new WireException("System does not support fopen");
|
||||
@@ -981,6 +1119,40 @@ class WireHttp extends Wire {
|
||||
*/
|
||||
}
|
||||
|
||||
/**
|
||||
* Set response headers where they are provided as an associative array and values can be strings or arrays
|
||||
*
|
||||
* @param array $responseHeader headers in an associative array
|
||||
*
|
||||
*/
|
||||
protected function setResponseHeaderValues(array $responseHeader) {
|
||||
$this->responseHeaders = array();
|
||||
$this->responseHeaderArrays = array();
|
||||
|
||||
foreach($responseHeader as $key => $value) {
|
||||
$key = strtolower($key);
|
||||
if(!isset($this->responseHeaders[$key])) {
|
||||
if(is_array($value)) {
|
||||
$valueArray = $value;
|
||||
$valueStr = count($value) ? reset($value) : '';
|
||||
} else {
|
||||
$valueArray = strlen($value) ? array($value) : array();
|
||||
$valueStr = $value;
|
||||
}
|
||||
$this->responseHeaders[$key] = $valueStr;
|
||||
$this->responseHeaderArrays[$key] = $valueArray;
|
||||
} else {
|
||||
if(is_array($value)) {
|
||||
foreach($value as $k => $v) {
|
||||
$this->responseHeaderArrays[$key][] = $v;
|
||||
}
|
||||
} else {
|
||||
$this->responseHeaderArrays[$key][] = $value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send the contents of the given filename to the current http connection.
|
||||
*
|
||||
@@ -1227,6 +1399,24 @@ class WireHttp extends Wire {
|
||||
return $this->allowSchemes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current user-agent header
|
||||
*
|
||||
* To set the user agent header, use `$http->setHeader('user-agent', '...');`
|
||||
*
|
||||
* @return string
|
||||
*
|
||||
*/
|
||||
public function getUserAgent() {
|
||||
if(isset($this->headers['user-agent'])) {
|
||||
$userAgent = $this->headers['user-agent'];
|
||||
} else {
|
||||
// some web servers deliver a 400 error if no user-agent set in request header, so make sure one is set
|
||||
$userAgent = 'ProcessWire/' . ProcessWire::versionMajor . '.' . ProcessWire::versionMinor . ' (' . $this->className() . ')';
|
||||
}
|
||||
return $userAgent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the number of seconds till connection times out
|
||||
*
|
||||
@@ -1251,6 +1441,18 @@ class WireHttp extends Wire {
|
||||
return $this->timeout === null ? self::defaultTimeout : (float) $this->timeout;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the last used internal sending type: fopen, curl or socket
|
||||
*
|
||||
* #pw-internal
|
||||
*
|
||||
* @return string
|
||||
*
|
||||
*/
|
||||
public function getLastSendType() {
|
||||
return $this->lastSendType;
|
||||
}
|
||||
|
||||
/**
|
||||
* #pw-internal
|
||||
*
|
||||
|
Reference in New Issue
Block a user