Index: includes/browser.inc
===================================================================
RCS file: includes/browser.inc
diff -N includes/browser.inc
--- /dev/null	1 Jan 1970 00:00:00 -0000
+++ includes/browser.inc	1 Jan 1970 00:00:00 -0000
@@ -0,0 +1,330 @@
+<?php
+// $Id$
+
+/**
+ * @file
+ * Browser API class.
+ */
+
+/**
+ * @defgroup browser Browser
+ * @{
+ * Provides a powerful text based browser through a class based API.
+ * The browser provides a pluggable backend architecture and supports two
+ * backends natively: 1) PHP streams, and 2) curl. The browser also supports
+ * arbitrary HTTP request types in addtion to GET and POST, given that the
+ * backend supports them.
+ *
+ * The browser can be used to make a simple GET request to example.com as
+ * shown below.
+ * @code
+ *   $browser = Browser::getInstance();
+ *   $browser->get('http://example.com');
+ * @endcode
+ * The result of the GET request can be accessed in two ways: 1) the get()
+ * method returns an array defining the result of the request, or 2) the
+ * individual properties can be accessed from the browser instance via their
+ * respective access methods. The following demonstrates the properties that
+ * are avaialable and how to access them.
+ * @code
+ *   $browser->getUrl();
+ *   $browser->getHeaders();
+ *   $browser->getContent();
+ * @endcode
+ *
+ * When peforming a POST request the following format is used.
+ * @code
+ *   $browser = Browser::getInstance();
+ *   $post = array(
+ *     'field_name1' => 'foo',
+ *     'checkbox1' => TRUE,
+ *     'multipleselect1[]' => array(
+ *       'value1',
+ *       'value2',
+ *     ),
+ *   );
+ *   $browser->post('http://example.com/form', $post, 'Submit button text');
+ * @endcode
+ * To submit a multi-step form or to post to the current page the URL passed to
+ * post() may be set to NULL. If there were two steps on the form shown in the
+ * example above with the mutliple select field on the second page and a submit
+ * button with the title "Next" on the first page the code be as follows.
+ * @code
+ *   $browser = Browser::getInstance();
+ *   $post = array(
+ *     'field_name1' => 'foo',
+ *     'checkbox1' => TRUE,
+ *   );
+ *   $browser->post('http://example.com/form', $post, 'Next');
+ *
+ *   $post = array(
+ *     'multipleselect1[]' => array(
+ *       'value1',
+ *       'value2',
+ *     ),
+ *   );
+ *   $browser->post(NULL, $post, 'Final');
+ * @endcode
+ */
+
+function browser_init() {
+  $state = &drupal_static('browser', array());
+
+  if (!$state) {
+    // Set the initial browser state.
+    $state = array(
+      'request_headers' => array(
+        'User-Agent' => 'Drupal (+http://drupal.org/)'
+      ),
+      'cookie_file' => NULL,
+      'url' => NULL,
+      'headers' => array(),
+      'content' => NULL,
+      'page' => NULL,
+    );
+
+    // Detect the availability of curl.
+    $curl = &drupal_static('browser_curl', FALSE);
+    $curl = function_exists('curl_init');
+
+    // Initialize the appropriate handler.
+    $handle = &drupal_static('browser_handle', NULL);
+    if ($curl) {
+      $handle = curl_init();
+      curl_setopt_array($handle, _browser_curl_options());
+    }
+    else {
+      $handle = stream_context_create();
+    }
+  }
+
+  return $state;
+}
+
+function browser_reset() {
+  drupal_static('browser', array(), TRUE);
+  drupal_static('browser_curl', FALSE, TRUE);
+  drupal_static('browser_handle', NULL, TRUE);
+}
+
+/**
+ * Get the default curl options to be used with each request.
+ */
+function _browser_curl_options() {
+  $state = browser_state_get();
+  return array(
+    CURLOPT_COOKIEJAR => $state['cookie_file'],
+    CURLOPT_FOLLOWLOCATION => TRUE,
+    CURLOPT_HEADERFUNCTION => '_browser_curl_header_callback',
+    CURLOPT_HTTPHEADER => $state['request_headers'],
+    CURLOPT_RETURNTRANSFER => TRUE,
+    CURLOPT_SSL_VERIFYPEER => FALSE,
+    CURLOPT_SSL_VERIFYHOST => FALSE,
+    CURLOPT_URL => '/',
+    CURLOPT_USERAGENT => $state['request_headers']['User-Agent'],
+  );
+}
+
+function browser_state_get() {
+  return drupal_static('browser', array());
+}
+
+function browser_state_set($state) {
+  $state_old = &drupal_static('browser', array());
+  $state_old = $state;
+}
+
+function browser_request_header_get($name) {
+  $name = browser_header_name($name);
+  $state = &drupal_static('browser', array());
+  return isset($state[$name]) ? $state[$name] : FALSE;
+}
+
+function browser_request_header_set($name, $value) {
+  $name = browser_header_name($name);
+  $state = &drupal_static('browser', array());
+  $state['request_headers'][$name] = $value;
+}
+
+/**
+ * Perform a GET request.
+ *
+ * @param $url
+ *   Absolute URL to request.
+ * @return
+ *   Associative array of state information, as returned by
+ *   browser_state_get().
+ * @see browser_state_get()
+ */
+function browser_get($url) {
+  $state = &browser_init();
+
+  if (drupal_static('browser_curl', FALSE)) {
+    _browser_execute_curl(array(
+      CURLOPT_HTTPGET => TRUE,
+      CURLOPT_URL => $url,
+      CURLOPT_NOBODY => FALSE,
+    ));
+  }
+  else {
+    _browser_execute_stream($url, array(
+      'method' => 'GET',
+      'header'  => array(
+        'Content-Type' => 'application/x-www-form-urlencoded',
+      ),
+    ));
+  }
+
+  return browser_state_get();
+}
+
+/**
+ * Perform curl_exec() with the specified option changes.
+ *
+ * @param $options
+ *   Curl options to set, any options not set will maintain their previous
+ *   value.
+ */
+function _browser_execute_curl(array $options) {
+  $state = &drupal_static('browser', array());
+  $handle = &drupal_static('browser_handle', NULL);
+
+  curl_setopt_array($handle, $options);
+  $state['content'] = curl_exec($handle);
+  $state['url'] = curl_getinfo($handle, CURLINFO_EFFECTIVE_URL);
+  // $state['headers'] should be filled by _browser_curl_header_callback().
+}
+
+/**
+ * Peform the request using the PHP stream wrapper.
+ *
+ * @param $url
+ *   The url to request.
+ * @param $options
+ *   The HTTP stream context options to be passed to
+ *   stream_context_set_params().
+ */
+function _browser_execute_stream($url, $options) {
+  $state = &drupal_static('browser', array());
+  $handle = &drupal_static('browser_handle', NULL);
+
+  // Global variable provided by PHP stream wapper.
+  global $http_response_header;
+
+  if (!isset($options['headers'])) {
+    $options['headers'] = array();
+  }
+
+  // Merge default request headers with the passed headers and generate
+  // header string to be sent in http request.
+  $headers = $state['request_headers'] + $options['headers'];
+  $options['headers'] = _browser_header_string($headers);
+
+  // Update the handler options.
+  stream_context_set_params($handle, array(
+    'options' => array(
+      'http' => $options,
+    )
+  ));
+
+  // Make the request.
+  $state['content'] = file_get_contents($url, FALSE, $handle);
+  $state['url'] = $url;
+  $state['headers'] = _browser_header_parse($http_response_header);
+}
+
+/**
+ * Reads reponse headers and stores in $headers array.
+ *
+ * @param $curlHandler
+ *   The curl handler.
+ * @param $header
+ *   An header.
+ * @return
+ *   The string length of the header. (required by curl)
+ */
+function _browser_curl_header_callback($handler, $header) {
+  // Ignore blank header lines.
+  $clean_header = trim($header);
+  if ($clean_header) {
+    $state = &drupal_static('browser', array());
+    $state['headers'] += _browser_header_parse($clean_header);
+  }
+
+  // Curl requires strlen() to be returned.
+  return strlen($header);
+}
+
+/**
+ * Generate a header string given he associative array of headers.
+ *
+ * @param $headers
+ *   Associative array of headers.
+ * @return
+ *   Header string to be used with stream.
+ */
+function _browser_header_string(array $headers) {
+  $string = '';
+  foreach ($headers as $key => $header) {
+    // Remove blank headers.
+    if ($header) {
+      $string .= "$key: $header\r\n";
+    }
+  }
+  return $string;
+}
+
+/**
+ * Parse the response header array to create an associative array.
+ *
+ * @param $headers
+ *   Array of headers.
+ * @return
+ *   An associative array of headers.
+ */
+function _browser_header_parse_all(array $headers) {
+  $headers = array();
+  foreach ($headers as $header) {
+    $headers += _browser_header_parse($header);
+  }
+  return $headers;
+}
+
+/**
+ * Parse an idividual header into name and value.
+ *
+ * @param $header
+ *   A string header string.
+ * @return
+ *   Parsed header as array($name => $value), or array() if parse failed.
+ */
+function _browser_header_parse($header) {
+  $parts = explode(':', $header, 2);
+
+  // Ensure header line is valid.
+  if (count($parts) == 2) {
+    $name = browser_header_name(trim($parts[0]));
+    return array($name => trim($parts[1]));
+  }
+  return array();
+}
+
+/**
+ * Ensure that header name is formatted with capital letters.
+ *
+ * @param $name
+ *   Header name to format.
+ * @return
+ *   Formatted header name.
+ */
+function browser_header_name($name) {
+  $parts = explode('-', $name);
+  foreach ($parts as &$part) {
+    $part = ucfirst($part);
+  }
+  return implode('-', $parts);
+}
+
+/**
+ * @} End of "defgroup browser".
+ */
