chr.module in cURL HTTP Request 7
Same filename and directory in other branches
cURL HTTP Request methods
File
chr.moduleView source
<?php
/**
* @file
* cURL HTTP Request methods
*/
/**
* Implements hook_menu().
*/
function chr_menu() {
$items['admin/config/services/chr'] = array(
'title' => 'cURL HTTP Request',
'description' => 'Configure cURL HTTP Request settings',
'page callback' => 'drupal_get_form',
'page arguments' => array(
'chr_admin_settings_form',
),
'access arguments' => array(
'administer chr',
),
'file' => 'chr.admin.inc',
);
return $items;
}
/**
* Implements hook_permission().
*/
function chr_permission() {
return array(
'administer chr' => array(
'title' => t('Administer cURL HTTP Request'),
'description' => t('Perform administration tasks for cURL HTTP Request.'),
),
);
}
/**
* Legacy wrapper callback.
*/
function curl_http_request($url, array $options = array()) {
watchdog('curl_http_request', 'Call to legacy function curl_http_request. Please use chr_curl_http_request instead.', array(), WATCHDOG_WARNING);
return chr_curl_http_request($url, $options);
}
/**
* Performs an HTTP request.
*
* This is a flexible and powerful HTTP client implementation. Correctly
* handles GET, POST, PUT or any other HTTP requests. Handles redirects.
*
* @param $url
* A string containing a fully qualified URI.
* @param array $options
* (optional) An array that can have one or more of the following elements:
* - headers: An array containing request headers to send as name/value pairs.
* - method: A string containing the request method. Defaults to 'GET'.
* - data: A string containing the request body, formatted as
* 'param=value¶m=value&...'. Defaults to NULL.
* - max_redirects: An integer representing how many times a redirect
* may be followed. Defaults to 3.
* - timeout: A float representing the maximum number of seconds the function
* call may take. The default is 30 seconds. If a timeout occurs, the error
* code is set to the HTTP_REQUEST_TIMEOUT constant.
* - context: A context resource created with stream_context_create().
* - verify_ssl: A boolean, to decide whether (TRUE) or not (FALSE) verify the SSL certificate and host.
* - verbose: A boolean, to switch on (TRUE) and off (FALSE) the cURL verbose mode.
* - cookiefile: A string containing a local path to the cookie file.
* - http_proxy: An array that will override the system-wide HTTP proxy settings. Array's elements:
* - https_proxy: An array that will override the system-wide HTTPS proxy settings.
* - curl_opts: An array of generic cURL options.
* @return object
* An object that can have one or more of the following components:
* - request: A string containing the request body that was sent.
* - code: An integer containing the response status code, or the error code
* if an error occurred.
* - protocol: The response protocol (e.g. HTTP/1.1 or HTTP/1.0).
* - status_message: The status message from the response, if a response was
* received.
* - redirect_code: If redirected, an integer containing the initial response
* status code.
* - redirect_url: If redirected, a string containing the URL of the redirect
* target.
* - error: If an error occurred, the error message. Otherwise not set.
* - errno: If an error occurred, a cURL error number greater than 0. Otherwise set to 0.
* - headers: An array containing the response headers as name/value pairs.
* HTTP header names are case-insensitive (RFC 2616, section 4.2), so for
* easy access the array keys are returned in lower case.
* - data: A string containing the response body that was received.
* - curl_opts: An array of curl options used
*/
function chr_curl_http_request($url, array $options = array()) {
$result = new stdClass();
// Parse the URL and make sure we can handle the schema.
$uri = @parse_url($url);
if ($uri == FALSE) {
$result->error = 'unable to parse URL';
$result->code = -1001;
return $result;
}
if (!isset($uri['scheme'])) {
$result->error = 'missing schema';
$result->code = -1002;
return $result;
}
timer_start(__FUNCTION__);
// Merge the default options.
$options += array(
'headers' => array(
'User-Agent' => 'Drupal (+http://drupal.org/)',
),
'method' => 'GET',
'data' => NULL,
'max_redirects' => 3,
'timeout' => 30.0,
'context' => NULL,
'verify_ssl' => FALSE,
'verbose' => FALSE,
'cookiefile' => NULL,
'http_proxy' => _chr_get_proxy_variable('http_proxy'),
'https_proxy' => _chr_get_proxy_variable('https_proxy'),
'curl_opts' => array(),
);
// Initialize cURL object.
$ch = curl_init($url);
// Set the proxy settings.
_chr_curl_set_proxy_settings($options, $ch, $uri);
// If the database prefix is being used by SimpleTest to run the tests in a copied
// database then set the user-agent header to the database prefix so that any
// calls to other Drupal pages will run the SimpleTest prefixed database. The
// user-agent is used to ensure that multiple testing sessions running at the
// same time won't interfere with each other as they would if the database
// prefix were stored statically in a file or database variable.
$test_info =& $GLOBALS['drupal_test_info'];
if (!empty($test_info['test_run_id'])) {
$options['headers']['User-Agent'] = drupal_generate_test_ua($test_info['test_run_id']);
}
elseif (empty($options['headers']['User-Agent'])) {
$options['headers']['User-Agent'] = 'Drupal (+http://drupal.org/)';
}
// Set default configuration.
_chr_curl_set_defaults($options, $ch);
// Set cookie settings.
_chr_curl_set_cookie_settings($options, $ch);
// Set the port.
$success = _chr_curl_set_port($options, $ch, $uri);
if (FALSE === $success) {
$result->error = 'invalid schema ' . $uri['scheme'];
$result->code = -1003;
return $result;
}
// Set request options.
$success = _chr_curl_request_type_option($options, $ch);
if (FALSE === $success) {
$result->error = 'invalid method ' . $options['method'];
$result->code = -1004;
return $result;
}
// If the server URL has a user then attempt to use basic authentication.
if (isset($uri['user'])) {
$options['headers']['Authorization'] = 'Basic ' . base64_encode($uri['user'] . (isset($uri['pass']) ? ':' . $uri['pass'] : ''));
}
// Set headers.
_chr_curl_set_headers($options, $ch);
// Set any last minute options.
_chr_curl_set_options($options, $ch);
// Make request.
$result->data = ltrim(curl_exec($ch));
// Check for errors.
$result->errno = curl_errno($ch);
// Get Response Info.
$info = curl_getinfo($ch);
// If there's been an error, do not continue.
if ($result->errno != 0) {
// Request timed out.
if (CURLE_OPERATION_TIMEOUTED == $result->errno) {
$result->code = HTTP_REQUEST_TIMEOUT;
$result->error = 'request timed out';
return $result;
}
$result->error = curl_error($ch);
$result->code = $result->errno;
return $result;
}
// The last effective URL should correspond to the Redirect URL.
$result->redirect_url = curl_getinfo($ch, CURLINFO_EFFECTIVE_URL);
// Save the request sent into the result object.
$result->request = curl_getinfo($ch, CURLINFO_HEADER_OUT);
// Close the connection.
curl_close($ch);
// For NTLM requests:
// Since NTLM responses contain multiple header sections, we must parse them
// differently than standard response data.
if (isset($options['curl_opts'][CURLOPT_HTTPAUTH]) && $options['curl_opts'][CURLOPT_HTTPAUTH] & CURLAUTH_NTLM) {
// @todo don't need right now, fix later
// $result->ntlm_response = _chr_curl_parse_ntlm_response($headers);
}
// Parse response headers from the response body.
// Be tolerant of malformed HTTP responses that separate header and body with
// \n\n or \r\r instead of \r\n\r\n.
$matches = preg_split("/\r\n\r\n|\n\n|\r\r/", $result->data, 2);
$response = isset($matches[0]) ? $matches[0] : NULL;
// Sometimes there isn't payload e.g. when a HEAD request is sent.
$result->data = isset($matches[1]) ? $matches[1] : NULL;
// Sometimes when making an HTTP request via proxy using cURL, you end up with a multiple set of headers:
// from the web server being the actual target, from the proxy itself, etc.
// The following 'if' statement is to check for such a situation and make sure we get a proper split between
// actual response body and actual response headers both coming from the web server.
while ('HTTP/' == substr($result->data, 0, 5)) {
$matches = preg_split("/\r\n\r\n|\n\n|\r\r/", $result->data, 2);
$response = isset($matches[0]) ? $matches[0] : NULL;
// Sometimes there isn't payload e.g. when a HEAD request is sent.
$result->data = isset($matches[1]) ? $matches[1] : NULL;
}
$response = preg_split("/\r\n|\n|\r/", $response);
// Parse the response status line.
list($protocol, $code, $status_message) = explode(' ', trim(array_shift($response)), 3);
$result->protocol = $protocol;
$result->status_message = $status_message;
$result->headers = array();
// Parse the response headers.
while ($line = trim(array_shift($response))) {
list($name, $value) = explode(':', $line, 2);
$name = strtolower($name);
if (isset($result->headers[$name]) && $name == 'set-cookie') {
// RFC 2109: the Set-Cookie response header comprises the token Set-
// Cookie:, followed by a comma-separated list of one or more cookies.
$result->headers[$name] .= ',' . trim($value);
}
else {
$result->headers[$name] = trim($value);
}
}
$responses = chr_response_codes();
// RFC 2616 states that all unknown HTTP codes must be treated the same as the
// base code in their class.
if (!isset($responses[$code])) {
$code = floor($code / 100) * 100;
}
$result->code = $code;
switch ($code) {
case 200:
// OK
case 304:
// Not modified
break;
case 301:
// Moved permanently
case 302:
// Moved temporarily
case 307:
// Moved temporarily
$location = $result->headers['location'];
$options['timeout'] -= timer_read(__FUNCTION__) / 1000;
if ($options['timeout'] <= 0) {
$result->code = HTTP_REQUEST_TIMEOUT;
$result->error = 'request timed out';
}
elseif ($options['max_redirects']) {
// Redirect to the new location.
$options['max_redirects']--;
$result = chr_curl_http_request($location, $options);
$result->redirect_code = $code;
}
if (!isset($result->redirect_url)) {
$result->redirect_url = $location;
}
break;
default:
$result->error = $status_message;
}
// Lastly, include any cURL specific information.
$result->curl_info = $info;
// Debug output.
if (variable_get('chr_debug', FALSE) and module_exists('devel')) {
dpm($result);
}
return $result;
}
/**
* Set cURL options.
*
* @param array $options [reference]
* Options array
* @param object $ch [reference]
* cURL Object
*/
function _chr_curl_set_options(&$options, &$ch) {
// Set any extra cURL options
foreach ($options['curl_opts'] as $opt => $value) {
$set = curl_setopt($ch, $opt, $value);
if (FALSE === $set) {
watchdog('curl_http_request', 'Unable to set cURL option @opt : @value', array(
'@opt' => $opt,
'@value' => $value,
), WATCHDOG_ERROR);
}
}
}
/**
* Set default cURL settings.
*
* @param array $options [reference]
* Options array
* @param object $ch [reference]
* cURL Object
*/
function _chr_curl_set_defaults(&$options, &$ch) {
curl_setopt($ch, CURLOPT_HEADER, TRUE);
curl_setopt($ch, CURLOPT_USERAGENT, $options['headers']['User-Agent']);
curl_setopt($ch, CURLINFO_HEADER_OUT, TRUE);
curl_setopt($ch, CURLOPT_TIMEOUT, $options['timeout']);
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, $options['timeout']);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, TRUE);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, TRUE);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, $options['verify_ssl']);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, $options['verify_ssl']);
curl_setopt($ch, CURLOPT_MAXREDIRS, $options['max_redirects']);
// Remove the user agent from the headers as it is already set
unset($options['headers']['User-Agent']);
}
/**
* Set proxy settings.
*
* @param array $options [reference]
* Options array
* @param object $ch [reference]
* cURL Object
* @param array $uri [reference]
* URI array
*/
function _chr_curl_set_proxy_settings(&$options, &$ch, &$uri) {
// Select the right proxy for the right protocol.
$proxy = 'https' == $uri['scheme'] ? $options['https_proxy'] : $options['http_proxy'];
// Nullify the proxy if the host to send the request to is part of the proxy's exceptions.
if (!empty($proxy['exceptions']) && in_array($uri['host'], $proxy['exceptions'])) {
$proxy = NULL;
}
if (!empty($proxy)) {
curl_setopt($ch, CURLOPT_PROXY, $proxy['server']);
curl_setopt($ch, CURLOPT_PROXYPORT, $proxy['port']);
// For the time being let's just support HTTP proxies with basic authentication.
if (isset($proxy['username']) && isset($proxy['password'])) {
curl_setopt($ch, CURLOPT_PROXYUSERPWD, implode(':', array(
$proxy['username'],
$proxy['password'],
)));
curl_setopt($ch, CURLOPT_PROXYTYPE, CURLPROXY_HTTP);
curl_setopt($ch, CURLOPT_PROXYAUTH, CURLAUTH_BASIC);
}
}
}
/**
* Set default headers in curl object.
*
* @param array $options [reference]
* Options array
* @param object $ch [reference]
* cURL Object
*/
function _chr_curl_set_headers(&$options, &$ch) {
if (is_array($options['headers']) and !empty($options['headers'])) {
$headers = array();
foreach ($options['headers'] as $key => $value) {
$headers[] = trim($key) . ": " . trim($value);
}
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
}
}
/**
* Convert the string name of the request type to
* a cURL opt value.
*
* @param array $options [reference]
* Options array
* @param object $ch [reference]
* cURL Object
*
* @return boolean
* Returns FALSE on error.
*/
function _chr_curl_request_type_option(&$options, &$ch) {
$valid_method = FALSE;
switch (drupal_strtoupper($options['method'])) {
case 'DELETE':
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'DELETE');
$valid_method = TRUE;
break;
case 'OPTIONS':
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'OPTIONS');
$valid_method = TRUE;
break;
case 'TRACE':
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'TRACE');
$valid_method = TRUE;
break;
case 'CONNECT':
// @todo
break;
case 'PATCH':
// @todo
break;
case 'POST':
// Assign the data to the proper cURL option
curl_setopt($ch, CURLOPT_POSTFIELDS, $options['data']);
if (isset($options['multipart']) && TRUE === $options['multipart']) {
// Do nothing for now
}
else {
curl_setopt($ch, CURLOPT_POST, TRUE);
}
$valid_method = TRUE;
break;
case 'PUT':
// Assign the data to the proper cURL option
curl_setopt($ch, CURLOPT_POSTFIELDS, $options['data']);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'PUT');
curl_setopt($ch, CURLOPT_HTTPHEADER, array(
'X-HTTP-Method-Override: PUT',
));
$valid_method = TRUE;
break;
case 'GET':
curl_setopt($ch, CURLOPT_HTTPGET, TRUE);
$valid_method = TRUE;
break;
case 'HEAD':
curl_setopt($ch, CURLOPT_NOBODY, TRUE);
$valid_method = TRUE;
break;
default:
return $valid_method;
}
return $valid_method;
}
/**
* Set port options.
*
* @param array $options [reference]
* Options array
* @param object $ch [reference]
* cURL Object
* @param array $uri [reference]
* URI array
*
* @return boolean
* Returns TRUE if they were set properly, FALSE otherwise.
*/
function _chr_curl_set_port(&$options, &$ch, &$uri) {
$default_ports = array(
'http' => 80,
'feed' => 80,
'https' => 443,
);
if (array_key_exists($uri['scheme'], $default_ports)) {
if (!isset($uri['port'])) {
$uri['port'] = $default_ports[$uri['scheme']];
}
// RFC 2616: "non-standard ports MUST, default ports MAY be included".
// We don't add the standard port to prevent from breaking rewrite rules
// checking the host that do not take into account the port number.
$options['headers']['Host'] = $uri['host'] . ($uri['port'] != 80 ? ':' . $uri['port'] : '');
return TRUE;
}
else {
return FALSE;
}
}
/**
* Set default cookie settings.
*
* @param array $options [reference]
* Options array
* @param object $ch [reference]
* cURL Object
*/
function _chr_curl_set_cookie_settings(&$options, &$ch) {
// Set cookie settings
if ($options['cookiefile']) {
curl_setopt($ch, CURLOPT_COOKIEJAR, $options['cookiefile']);
curl_setopt($ch, CURLOPT_COOKIEFILE, $options['cookiefile']);
}
}
/**
* Get proxy settings for CHR.
* @param string $proxy
* The name of proxy setting variable to retrieve.
* @return
* The proxy setting variable to retrieve.
*/
function _chr_get_proxy_variable($proxy) {
// Attempt to retrieve $proxy_var, which could either
// be 'http_proxy' or 'https_proxy'.
$proxy_var = variable_get($proxy);
// If the requested $proxy_var hasn't been found,
// let's try and work it out based on other variables.
if (empty($proxy_var)) {
switch ($proxy) {
// Let's always assume to fall back on the HTTP proxy settings whenever
// HTTPS proxy settings are not found.
case 'https_proxy':
// The first attempt is to see if the CHR-specific HTTP settings have
// been set. If so, let's just use that to for 'https_proxy'.
if ($proxy_var = variable_get('http_proxy')) {
break;
}
// If 'http_proxy' wasn't set, let's fall through to the next case,
// so that we try and fall back on Drupal standard proxy
// settings for HTTP. Therefore, no 'break' required here.
case 'http_proxy':
// If a proxy server is set in Drupal, start building an 'http_proxy'
// setting in the format expected by CHR.
if ($proxy_server = variable_get('proxy_server')) {
$proxy_var['server'] = $proxy_server;
$proxy_var['port'] = variable_get('proxy_port', 8080);
if ($proxy_username = variable_get('proxy_username')) {
$proxy_var['username'] = $proxy_username;
}
if ($proxy_password = variable_get('proxy_password')) {
$proxy_var['password'] = $proxy_password;
}
elseif (isset($proxy_var['username'])) {
$proxy_var['password'] = '';
}
if ($proxy_exceptions = variable_get('proxy_exceptions')) {
$proxy_var['exceptions'] = $proxy_exceptions;
}
}
break;
}
}
$drupal_proxy_server = variable_get('proxy_server');
if (empty($proxy_var) and isset($drupal_proxy_server)) {
$proxy_var = array(
'server' => $drupal_proxy_server,
'port' => variable_get('proxy_port'),
'username' => variable_get('proxy_username'),
'password' => variable_get('proxy_password'),
'exceptions' => variable_get('proxy_exceptions'),
);
$proxy_var = array_filter($proxy_var);
}
// If a server wasn't set, $proxy_var remains NULL.
// Either way, return $proxy_var.
return $proxy_var;
}
/**
* Parse NTLM requests.
*
* @param array $options [reference]
* Options array
* @param object $ch [reference]
* cURL Object
*/
/*
function _chr_curl_parse_ntlm_response(&$options, &$ch) {
// @todo Rework this flow as it currently doesn't work
$ntlm = FALSE;
$result = new stdClass();
// Until we verify that the NTLM header is present, we leave the original
// variable untouched
$temp = $data;
// Parse response headers from the response body.
// Be tolerant of malformed HTTP responses that separate header and body with
// \n\n or \r\r instead of \r\n\r\n.
list($response, $temp) = preg_split("/\r\n\r\n|\n\n|\r\r/", $temp, 2);
$response = preg_split("/\r\n|\n|\r/", $response);
// Check that the NTLM header is present
foreach($response as $header) {
if (FALSE !== stripos($header, 'WWW-AUTHENTICATE: NTLM')) {
$ntlm = TRUE;
}
}
if ($ntlm) {
// Parse the response status line.
list($protocol, $code, $status_message) = explode(' ', trim(array_shift($response)), 3);
$result->protocol = $protocol;
$result->status_message = $status_message;
$result->headers = array();
// Parse the response headers.
while ($line = trim(array_shift($response))) {
list($name, $value) = explode(':', $line, 2);
$name = strtolower($name);
if (isset($result->headers[$name]) && $name == 'set-cookie') {
// RFC 2109: the Set-Cookie response header comprises the token Set-
// Cookie:, followed by a comma-separated list of one or more cookies.
$result->headers[$name] .= ',' . trim($value);
}
else {
$result->headers[$name] = trim($value);
}
}
$data = $temp;
}
return $result;
}//*/
/**
* List of response codes.
*
* @return array
* Returns an array of response codes.
*/
function chr_response_codes() {
return array(
100 => 'Continue',
101 => 'Switching Protocols',
200 => 'OK',
201 => 'Created',
202 => 'Accepted',
203 => 'Non-Authoritative Information',
204 => 'No Content',
205 => 'Reset Content',
206 => 'Partial Content',
300 => 'Multiple Choices',
301 => 'Moved Permanently',
302 => 'Found',
303 => 'See Other',
304 => 'Not Modified',
305 => 'Use Proxy',
307 => 'Temporary Redirect',
400 => 'Bad Request',
401 => 'Unauthorized',
402 => 'Payment Required',
403 => 'Forbidden',
404 => 'Not Found',
405 => 'Method Not Allowed',
406 => 'Not Acceptable',
407 => 'Proxy Authentication Required',
408 => 'Request Time-out',
409 => 'Conflict',
410 => 'Gone',
411 => 'Length Required',
412 => 'Precondition Failed',
413 => 'Request Entity Too Large',
414 => 'Request-URI Too Large',
415 => 'Unsupported Media Type',
416 => 'Requested range not satisfiable',
417 => 'Expectation Failed',
500 => 'Internal Server Error',
501 => 'Not Implemented',
502 => 'Bad Gateway',
503 => 'Service Unavailable',
504 => 'Gateway Time-out',
505 => 'HTTP Version not supported',
);
}
Functions
Name | Description |
---|---|
chr_curl_http_request | Performs an HTTP request. |
chr_menu | Implements hook_menu(). |
chr_permission | Implements hook_permission(). |
chr_response_codes | List of response codes. |
curl_http_request | Legacy wrapper callback. |
_chr_curl_request_type_option | Convert the string name of the request type to a cURL opt value. |
_chr_curl_set_cookie_settings | Set default cookie settings. |
_chr_curl_set_defaults | Set default cURL settings. |
_chr_curl_set_headers | Set default headers in curl object. |
_chr_curl_set_options | Set cURL options. |
_chr_curl_set_port | Set port options. |
_chr_curl_set_proxy_settings | Set proxy settings. |
_chr_get_proxy_variable | Get proxy settings for CHR. |