MDL-33955 make downalod_file_content() use curl class and cleanup

The options in constructor were renamed to setttings in order to prevent confusion.
This removes ignored CURLOPT_BINARYTRANSFER, add newline header cleaning,
reworks proxy settings and some other minor issues.
This commit is contained in:
Petr Škoda
2013-06-14 08:03:49 +02:00
parent 9936c2a54c
commit 220eef0e4c
+342 -232
View File
@@ -1126,7 +1126,6 @@ function format_postdata_for_curlcall($postdata) {
* Fetches content of file from Internet (using proxy if defined). Uses cURL extension if present.
* Due to security concerns only downloads from http(s) sources are supported.
*
* @todo MDL-31073 add version test for '7.10.5'
* @category files
* @param string $url file url starting with http(s)://
* @param array $headers http headers, null if none. If set, should be an
@@ -1149,14 +1148,7 @@ function format_postdata_for_curlcall($postdata) {
function download_file_content($url, $headers=null, $postdata=null, $fullresponse=false, $timeout=300, $connecttimeout=20, $skipcertverify=false, $tofile=NULL, $calctimeout=false) {
global $CFG;
// some extra security
$newlines = array("\r", "\n");
if (is_array($headers) ) {
foreach ($headers as $key => $value) {
$headers[$key] = str_replace($newlines, '', $value);
}
}
$url = str_replace($newlines, '', $url);
// Only http and https links supported.
if (!preg_match('|^https?://|i', $url)) {
if ($fullresponse) {
$response = new stdClass();
@@ -1171,227 +1163,168 @@ function download_file_content($url, $headers=null, $postdata=null, $fullrespons
}
}
// check if proxy (if used) should be bypassed for this url
$proxybypass = is_proxybypass($url);
$options = array();
if (!$ch = curl_init($url)) {
debugging('Can not init curl.');
return false;
}
// set extra headers
if (is_array($headers) ) {
$headers2 = array();
$headers2 = array();
if (is_array($headers)) {
foreach ($headers as $key => $value) {
$headers2[] = "$key: $value";
if (is_numeric($key)) {
$headers2[] = $value;
} else {
$headers2[] = "$key: $value";
}
}
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers2);
}
if ($skipcertverify) {
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
$options['CURLOPT_SSL_VERIFYPEER'] = false;
} else {
$options['CURLOPT_SSL_VERIFYPEER'] = true;
}
// use POST if requested
$options['CURLOPT_CONNECTTIMEOUT'] = $connecttimeout;
$options['CURLOPT_FOLLOWLOCATION'] = 1;
$options['CURLOPT_MAXREDIRS'] = 5;
// Use POST if requested.
if (is_array($postdata)) {
$postdata = format_postdata_for_curlcall($postdata);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $postdata);
}
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HEADER, false);
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, $connecttimeout);
if ($cacert = curl::get_cacert()) {
curl_setopt($ch, CURLOPT_CAINFO, $cacert);
}
if (!ini_get('open_basedir') and !ini_get('safe_mode')) {
// TODO: add version test for '7.10.5'
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
curl_setopt($ch, CURLOPT_MAXREDIRS, 5);
}
if (!empty($CFG->proxyhost) and !$proxybypass) {
// SOCKS supported in PHP5 only
if (!empty($CFG->proxytype) and ($CFG->proxytype == 'SOCKS5')) {
if (defined('CURLPROXY_SOCKS5')) {
curl_setopt($ch, CURLOPT_PROXYTYPE, CURLPROXY_SOCKS5);
} else {
curl_close($ch);
if ($fullresponse) {
$response = new stdClass();
$response->status = '0';
$response->headers = array();
$response->response_code = 'SOCKS5 proxy is not supported in PHP4';
$response->results = '';
$response->error = 'SOCKS5 proxy is not supported in PHP4';
return $response;
} else {
debugging("SOCKS5 proxy is not supported in PHP4.", DEBUG_ALL);
return false;
}
}
}
curl_setopt($ch, CURLOPT_HTTPPROXYTUNNEL, false);
if (empty($CFG->proxyport)) {
curl_setopt($ch, CURLOPT_PROXY, $CFG->proxyhost);
} else {
curl_setopt($ch, CURLOPT_PROXY, $CFG->proxyhost.':'.$CFG->proxyport);
}
if (!empty($CFG->proxyuser) and !empty($CFG->proxypassword)) {
curl_setopt($ch, CURLOPT_PROXYUSERPWD, $CFG->proxyuser.':'.$CFG->proxypassword);
if (defined('CURLOPT_PROXYAUTH')) {
// any proxy authentication if PHP 5.1
curl_setopt($ch, CURLOPT_PROXYAUTH, CURLAUTH_BASIC | CURLAUTH_NTLM);
}
}
}
// set up header and content handlers
$received = new stdClass();
$received->headers = array(); // received headers array
$received->tofile = $tofile;
$received->fh = null;
curl_setopt($ch, CURLOPT_HEADERFUNCTION, partial('download_file_content_header_handler', $received));
if ($tofile) {
curl_setopt($ch, CURLOPT_WRITEFUNCTION, partial('download_file_content_write_handler', $received));
} else if (empty($postdata)) {
$postdata = null;
}
// Optionally attempt to get more correct timeout by fetching the file size.
if (!isset($CFG->curltimeoutkbitrate)) {
//use very slow rate of 56kbps as a timeout speed when not set
// Use very slow rate of 56kbps as a timeout speed when not set.
$bitrate = 56;
} else {
$bitrate = $CFG->curltimeoutkbitrate;
}
if ($calctimeout and !isset($postdata)) {
$curl = new curl();
$curl->setHeader($headers2);
// try to calculate the proper amount for timeout from remote file size.
// if disabled or zero, we won't do any checks nor head requests.
if ($calctimeout && $bitrate > 0) {
//setup header request only options
curl_setopt_array ($ch, array(
CURLOPT_RETURNTRANSFER => false,
CURLOPT_NOBODY => true)
);
$curl->head($url, $postdata, $options);
curl_exec($ch);
$info = curl_getinfo($ch);
$err = curl_error($ch);
if ($err === '' && $info['download_content_length'] > 0) { //no curl errors
$timeout = max($timeout, ceil($info['download_content_length'] * 8 / ($bitrate * 1024))); //adjust for large files only - take max timeout.
$info = $curl->get_info();
$error_no = $curl->get_errno();
if (!$error_no && $info['download_content_length'] > 0) {
// No curl errors - adjust for large files only - take max timeout.
$timeout = max($timeout, ceil($info['download_content_length'] * 8 / ($bitrate * 1024)));
}
//reinstate affected curl options
curl_setopt_array ($ch, array(
CURLOPT_RETURNTRANSFER => true,
CURLOPT_NOBODY => false,
CURLOPT_HTTPGET => true)
);
}
curl_setopt($ch, CURLOPT_TIMEOUT, $timeout);
$result = curl_exec($ch);
$curl = new curl();
$curl->setHeader($headers2);
// try to detect encoding problems
$options['CURLOPT_RETURNTRANSFER'] = true;
$options['CURLOPT_NOBODY'] = false;
$options['CURLOPT_TIMEOUT'] = $timeout;
if ($tofile) {
$fh = fopen($tofile, 'w');
if (!$fh) {
if ($fullresponse) {
$response = new stdClass();
$response->status = 0;
$response->headers = array();
$response->response_code = 'Can not write to file';
$response->results = false;
$response->error = 'Can not write to file';
return $response;
} else {
return false;
}
}
$options['CURLOPT_FILE'] = $fh;
}
if (isset($postdata)) {
$content = $curl->post($url, $postdata, $options);
} else {
$content = $curl->get($url, null, $options);
}
if ($tofile) {
fclose($fh);
@chmod($tofile, $CFG->filepermissions);
}
/*
// Try to detect encoding problems.
if ((curl_errno($ch) == 23 or curl_errno($ch) == 61) and defined('CURLOPT_ENCODING')) {
curl_setopt($ch, CURLOPT_ENCODING, 'none');
$result = curl_exec($ch);
}
*/
if ($received->fh) {
fclose($received->fh);
}
$info = $curl->get_info();
$error_no = $curl->get_errno();
$rawheaders = $curl->get_raw_response();
if (curl_errno($ch)) {
$error = curl_error($ch);
$error_no = curl_errno($ch);
curl_close($ch);
if ($fullresponse) {
$response = new stdClass();
if ($error_no == 28) {
$response->status = '-100'; // mimic snoopy
} else {
$response->status = '0';
}
$response->headers = array();
$response->response_code = $error;
$response->results = false;
$response->error = $error;
return $response;
} else {
if ($error_no) {
$error = $content;
if (!$fullresponse) {
debugging("cURL request for \"$url\" failed with: $error ($error_no)", DEBUG_ALL);
return false;
}
$response = new stdClass();
if ($error_no == 28) {
$response->status = '-100'; // Mimic snoopy.
} else {
$response->status = '0';
}
$response->headers = array();
$response->response_code = $error;
$response->results = false;
$response->error = $error;
return $response;
}
if ($tofile) {
$content = true;
}
if (empty($info['http_code'])) {
// For security reasons we support only true http connections (Location: file:// exploit prevention).
$response = new stdClass();
$response->status = '0';
$response->headers = array();
$response->response_code = 'Unknown cURL error';
$response->results = false; // do NOT change this, we really want to ignore the result!
$response->error = 'Unknown cURL error';
} else {
$info = curl_getinfo($ch);
curl_close($ch);
$response = new stdClass();
$response->status = (string)$info['http_code'];
$response->headers = $rawheaders;
$response->results = $content;
$response->error = '';
if (empty($info['http_code'])) {
// for security reasons we support only true http connections (Location: file:// exploit prevention)
$response = new stdClass();
$response->status = '0';
$response->headers = array();
$response->response_code = 'Unknown cURL error';
$response->results = false; // do NOT change this, we really want to ignore the result!
$response->error = 'Unknown cURL error';
} else {
$response = new stdClass();
$response->status = (string)$info['http_code'];
$response->headers = $received->headers;
$response->response_code = $received->headers[0];
$response->results = $result;
$response->error = '';
}
if ($fullresponse) {
return $response;
} else if ($info['http_code'] != 200) {
debugging("cURL request for \"$url\" failed, HTTP response code: ".$response->response_code, DEBUG_ALL);
return false;
} else {
return $response->results;
// There might be multiple headers on redirect, find the status of the last one.
$firstline = true;
foreach ($rawheaders as $line) {
if ($firstline) {
$response->response_code = $line;
$firstline = false;
}
if (trim($line, "\r\n") === '') {
$firstline = true;
}
}
}
}
/**
* internal implementation
* @param stdClass $received
* @param resource $ch
* @param mixed $header
* @return int header length
*/
function download_file_content_header_handler($received, $ch, $header) {
$received->headers[] = $header;
return strlen($header);
}
if ($fullresponse) {
return $response;
}
/**
* internal implementation
* @param stdClass $received
* @param resource $ch
* @param mixed $data
*/
function download_file_content_write_handler($received, $ch, $data) {
if (!$received->fh) {
$received->fh = fopen($received->tofile, 'w');
if ($received->fh === false) {
// bad luck, file creation or overriding failed
return 0;
}
if ($info['http_code'] != 200) {
debugging("cURL request for \"$url\" failed, HTTP response code: ".$response->response_code, DEBUG_ALL);
return false;
}
if (fwrite($received->fh, $data) === false) {
// bad luck, write failed, let's abort completely
return 0;
}
return strlen($data);
return $response->results;
}
/**
@@ -2824,12 +2757,14 @@ function file_modify_html_header($text) {
class curl {
/** @var bool Caches http request contents */
public $cache = false;
/** @var bool Uses proxy */
public $proxy = false;
/** @var bool Uses proxy, null means automatic based on URL */
public $proxy = null;
/** @var string library version */
public $version = '0.4 dev';
/** @var array http's response */
public $response = array();
/** @var array Raw response headers, needed for BC in download_file_content(). */
public $rawresponse = array();
/** @var array http header */
public $header = array();
/** @var string cURL information */
@@ -2838,6 +2773,8 @@ class curl {
public $error;
/** @var int error code */
public $errno;
/** @var bool use workaround for open_basedir restrictions, to be changed from unit tests only! */
public $emulateredirects = null;
/** @var array cURL options */
private $options;
@@ -2851,36 +2788,45 @@ class curl {
private $debug = false;
/** @var bool|string Path to cookie file */
private $cookie = false;
/** @var bool tracks multiple headers in response - redirect detection */
private $responsefinished = false;
/**
* Constructor
* Curl constructor.
*
* @global stdClass $CFG
* @param array $options
* Allowed settings are:
* proxy: (bool) use proxy server, null means autodetect non-local from url
* debug: (bool) use debug output
* cookie: (string) path to cookie file, false if none
* cache: (bool) use cache
* module_cache: (string) type of cache
*
* @param array $settings
*/
public function __construct($options = array()){
public function __construct($settings = array()) {
global $CFG;
if (!function_exists('curl_init')) {
$this->error = 'cURL module must be enabled!';
trigger_error($this->error, E_USER_ERROR);
return false;
}
// the options of curl should be init here.
// All settings of this class should be init here.
$this->resetopt();
if (!empty($options['debug'])) {
if (!empty($settings['debug'])) {
$this->debug = true;
}
if(!empty($options['cookie'])) {
if($options['cookie'] === true) {
if (!empty($settings['cookie'])) {
if($settings['cookie'] === true) {
$this->cookie = $CFG->dataroot.'/curl_cookie.txt';
} else {
$this->cookie = $options['cookie'];
$this->cookie = $settings['cookie'];
}
}
if (!empty($options['cache'])) {
if (!empty($settings['cache'])) {
if (class_exists('curl_cache')) {
if (!empty($options['module_cache'])) {
$this->cache = new curl_cache($options['module_cache']);
if (!empty($settings['module_cache'])) {
$this->cache = new curl_cache($settings['module_cache']);
} else {
$this->cache = new curl_cache('misc');
}
@@ -2907,11 +2853,19 @@ class curl {
}
$this->setopt(array('proxytype'=>$this->proxy_type));
}
if (isset($settings['proxy'])) {
$this->proxy = $settings['proxy'];
}
} else {
$this->proxy = false;
}
if (!empty($this->proxy_host)) {
$this->proxy = array('proxy'=>$this->proxy_host);
if (!isset($this->emulateredirects)) {
$this->emulateredirects = (ini_get('open_basedir') or ini_get('safe_mode'));
}
}
/**
* Resets the CURL options that have already been set
*/
@@ -2922,17 +2876,13 @@ class curl {
$this->options['CURLOPT_HEADER'] = 0;
// True to Exclude the body from the output
$this->options['CURLOPT_NOBODY'] = 0;
// TRUE to follow any "Location: " header that the server
// sends as part of the HTTP header (note this is recursive,
// PHP will follow as many "Location: " headers that it is sent,
// unless CURLOPT_MAXREDIRS is set).
//$this->options['CURLOPT_FOLLOWLOCATION'] = 1;
// Redirect ny default.
$this->options['CURLOPT_FOLLOWLOCATION'] = 1;
$this->options['CURLOPT_MAXREDIRS'] = 10;
$this->options['CURLOPT_ENCODING'] = '';
// TRUE to return the transfer as a string of the return
// value of curl_exec() instead of outputting it out directly.
$this->options['CURLOPT_RETURNTRANSFER'] = 1;
$this->options['CURLOPT_BINARYTRANSFER'] = 0;
$this->options['CURLOPT_SSL_VERIFYPEER'] = 0;
$this->options['CURLOPT_SSL_VERIFYHOST'] = 2;
$this->options['CURLOPT_CONNECTTIMEOUT'] = 30;
@@ -3005,6 +2955,8 @@ class curl {
}
if (stripos($name, 'CURLOPT_') === false) {
$name = strtoupper('CURLOPT_'.$name);
} else {
$name = strtoupper($name);
}
$this->options[$name] = $val;
}
@@ -3043,18 +2995,27 @@ class curl {
$this->setHeader($v);
}
} else {
$this->header[] = $header;
// Remove newlines, they are not allowed in headers.
$this->header[] = preg_replace('/[\r\n]/', '', $header);
}
}
/**
* Set HTTP Response Header
*
* Get HTTP Response Headers
* @return array of arrays
*/
public function getResponse() {
return $this->response;
}
/**
* Get raw HTTP Response Headers
* @return array of strings
*/
public function get_raw_response() {
return $this->rawresponse;
}
/**
* private callback function
* Formatting HTTP Response Header
@@ -3064,6 +3025,17 @@ class curl {
* @return int The strlen of the header
*/
private function formatHeader($ch, $header) {
$this->rawresponse[] = $header;
if (trim($header, "\r\n") === '') {
if ($this->responsefinished) {
// Multiple headers means redirect, keep just the latest one.
$this->response = array();
return strlen($header);
}
$this->responsefinished = true;
}
if (strlen($header) > 2) {
list($key, $value) = explode(" ", rtrim($header, "\r\n"), 2);
$key = rtrim($key, ':');
@@ -3092,6 +3064,14 @@ class curl {
* @return resource The curl handle
*/
private function apply_opt($curl, $options) {
// Some more security first.
if (defined('CURLOPT_PROTOCOLS')) {
$this->options['CURLOPT_PROTOCOLS'] = (CURLPROTO_HTTP | CURLPROTO_HTTPS);
}
if (defined('CURLOPT_REDIR_PROTOCOLS')) {
$this->options['CURLOPT_REDIR_PROTOCOLS'] = (CURLPROTO_HTTP | CURLPROTO_HTTPS);
}
// Clean up
$this->cleanopt();
// set cookie
@@ -3101,10 +3081,24 @@ class curl {
));
}
// set proxy
if (!empty($this->proxy) || !empty($options['proxy'])) {
$this->setopt($this->proxy);
// Bypass proxy if required.
if ($this->proxy === null) {
if (!empty($this->options['CURLOPT_URL']) and is_proxybypass($this->options['CURLOPT_URL'])) {
$proxy = false;
} else {
$proxy = true;
}
} else {
$proxy = (bool)$this->proxy;
}
// Set proxy.
if ($proxy) {
$options['CURLOPT_PROXY'] = $this->proxy_host;
} else {
unset($this->options['CURLOPT_PROXY']);
}
$this->setopt($options);
// reset before set options
curl_setopt($curl, CURLOPT_HEADERFUNCTION, array(&$this,'formatHeader'));
@@ -3118,12 +3112,6 @@ class curl {
}
curl_setopt($curl, CURLOPT_HTTPHEADER, $this->header);
// Bypass proxy (for this request only) if required.
if (!empty($this->options['CURLOPT_URL']) &&
is_proxybypass($this->options['CURLOPT_URL'])) {
unset($this->options['CURLOPT_PROXY']);
}
if ($this->debug) {
echo '<h1>Options</h1>';
var_dump($this->options);
@@ -3131,11 +3119,35 @@ class curl {
var_dump($this->header);
}
// Do not allow infinite redirects.
if (!isset($this->options['CURLOPT_MAXREDIRS'])) {
$this->options['CURLOPT_MAXREDIRS'] = 0;
} else if ($this->options['CURLOPT_MAXREDIRS'] > 100) {
$this->options['CURLOPT_MAXREDIRS'] = 100;
} else {
$this->options['CURLOPT_MAXREDIRS'] = (int)$this->options['CURLOPT_MAXREDIRS'];
}
// Make sure we always know if redirects expected.
if (!isset($this->options['CURLOPT_FOLLOWLOCATION'])) {
$this->options['CURLOPT_FOLLOWLOCATION'] = 0;
}
// Set options.
foreach($this->options as $name => $val) {
$name = constant(strtoupper($name));
if ($name === 'CURLOPT_PROTOCOLS' or $name === 'CURLOPT_REDIR_PROTOCOLS') {
// These can not be changed, sorry.
continue;
}
if ($name === 'CURLOPT_FOLLOWLOCATION' and $this->emulateredirects) {
// The redirects are emulated elsewhere.
curl_setopt($curl, CURLOPT_FOLLOWLOCATION, 0);
continue;
}
$name = constant($name);
curl_setopt($curl, $name, $val);
}
return $curl;
}
@@ -3179,7 +3191,6 @@ class curl {
* @return array An array of results
*/
public function download($requests, $options = array()) {
$options['CURLOPT_BINARYTRANSFER'] = 1;
$options['RETURNTRANSFER'] = false;
return $this->multi($requests, $options);
}
@@ -3244,19 +3255,119 @@ class curl {
// create curl instance
$curl = curl_init($url);
$options['url'] = $url;
// Reset here so that the data is valid when result returned from cache.
$this->info = array();
$this->error = '';
$this->errno = 0;
$this->response = array();
$this->rawresponse = array();
$this->responsefinished = false;
$this->apply_opt($curl, $options);
if ($this->cache && $ret = $this->cache->get($this->options)) {
return $ret;
} else {
$ret = curl_exec($curl);
if ($this->cache) {
$this->cache->set($this->options, $ret);
}
}
$ret = curl_exec($curl);
$this->info = curl_getinfo($curl);
$this->error = curl_error($curl);
$this->errno = curl_errno($curl);
// Note: $this->response and $this->rawresponse are filled by $hits->formatHeader callback.
if ($this->emulateredirects and $this->options['CURLOPT_FOLLOWLOCATION'] and $this->info['http_code'] != 200) {
$redirects = 0;
while($redirects <= $this->options['CURLOPT_MAXREDIRS']) {
if ($this->info['http_code'] == 301) {
// Moved Permanently - repeat the same request on new URL.
} else if ($this->info['http_code'] == 302) {
// Found - the standard redirect - repeat the same request on new URL.
} else if ($this->info['http_code'] == 303) {
// 303 See Other - repeat only if GET, do not bother with POSTs.
if (empty($this->options['CURLOPT_HTTPGET'])) {
break;
}
} else if ($this->info['http_code'] == 307) {
// Temporary Redirect - must repeat using the same request type.
} else if ($this->info['http_code'] == 308) {
// Permanent Redirect - must repeat using the same request type.
} else {
// Some other http code means do not retry!
break;
}
$redirects++;
$redirecturl = null;
if (isset($this->info['redirect_url'])) {
if (preg_match('|^https?://|i', $this->info['redirect_url'])) {
$redirecturl = $this->info['redirect_url'];
}
}
if (!$redirecturl) {
foreach ($this->response as $k => $v) {
if (strtolower($k) === 'location') {
$redirecturl = $v;
break;
}
}
if (preg_match('|^https?://|i', $redirecturl)) {
// Great, this is the correct location format!
} else if ($redirecturl) {
$current = curl_getinfo($curl, CURLINFO_EFFECTIVE_URL);
if (strpos($redirecturl, '/') === 0) {
// Relative to server root - just guess.
$pos = strpos('/', $current, 8);
if ($pos === false) {
$redirecturl = $current.$redirecturl;
} else {
$redirecturl = substr($current, 0, $pos).$redirecturl;
}
} else {
// Relative to current script.
$redirecturl = dirname($current).'/'.$redirecturl;
}
}
}
$this->responsefinished = false;
$this->response = array();
curl_setopt($curl, CURLOPT_URL, $redirecturl);
$ret = curl_exec($curl);
$this->info = curl_getinfo($curl);
$this->error = curl_error($curl);
$this->errno = curl_errno($curl);
$this->info['redirect_count'] = $redirects;
if ($this->info['http_code'] === 200) {
// Finally this is what we wanted.
break;
}
if ($this->errno != CURLE_OK) {
// Something wrong is going on.
break;
}
}
if ($redirects > $this->options['CURLOPT_MAXREDIRS']) {
$this->errno = CURLE_TOO_MANY_REDIRECTS;
$this->error = 'Maximum ('.$this->options['CURLOPT_MAXREDIRS'].') redirects followed';
}
}
if ($this->cache) {
$this->cache->set($this->options, $ret);
}
if ($this->debug) {
echo '<h1>Return Data</h1>';
@@ -3372,7 +3483,6 @@ class curl {
*/
public function download_one($url, $params, $options = array()) {
$options['CURLOPT_HTTPGET'] = 1;
$options['CURLOPT_BINARYTRANSFER'] = true;
if (!empty($params)) {
$url .= (stripos($url, '?') !== false) ? '&' : '?';
$url .= http_build_query($params, '', '&');