Index: test.php
===================================================================
RCS file: test.php
diff -N test.php
--- /dev/null	1 Jan 1970 00:00:00 -0000
+++ test.php	1 Jan 1970 00:00:00 -0000
@@ -0,0 +1,37 @@
+<?php
+// $Id: index.php,v 1.96 2008/09/20 20:22:23 webchick Exp $
+
+/**
+ * @file
+ * The PHP page that serves all page requests on a Drupal installation.
+ *
+ * The routines here dispatch control to the appropriate handler, which then
+ * prints the appropriate page.
+ *
+ * All Drupal code is released under the GNU General Public License.
+ * See COPYRIGHT.txt and LICENSE.txt.
+ */
+
+/**
+ * Root directory of Drupal installation.
+ */
+define('DRUPAL_ROOT', dirname(realpath(__FILE__)));
+
+require_once DRUPAL_ROOT . '/includes/bootstrap.inc';
+drupal_bootstrap(DRUPAL_BOOTSTRAP_FULL);
+
+require_once DRUPAL_ROOT . '/includes/browser/browser.inc';
+$browser = Browser::getInstance();
+//$request = $browser->get('http://ipchicken.com/');
+
+$fields = array(
+  'pattern' => 'strlen',
+);
+$request = $browser->post('http://php.net/search.php', $fields, '...');
+
+if ($request) {
+  echo $request['content'];
+}
+else {
+  echo 'FAIL!';
+}
Index: includes/browser/wrapper.inc
===================================================================
RCS file: includes/browser/wrapper.inc
diff -N includes/browser/wrapper.inc
--- /dev/null	1 Jan 1970 00:00:00 -0000
+++ includes/browser/wrapper.inc	1 Jan 1970 00:00:00 -0000
@@ -0,0 +1,25 @@
+<?php
+// $Id$
+
+/**
+ * @file
+ * Text web browser for Drupal.
+ */
+
+interface HttpWrapper {
+  public static function info();
+
+  public function open();
+
+  public function close();
+
+  public function getRequestHeaders();
+
+  public function setRequestHeaders($headers = array());
+
+  public function get($url);
+
+  public function post($url, array $fields);
+
+  public function request($method);
+}
Index: includes/browser/stream.inc
===================================================================
RCS file: includes/browser/stream.inc
diff -N includes/browser/stream.inc
--- /dev/null	1 Jan 1970 00:00:00 -0000
+++ includes/browser/stream.inc	1 Jan 1970 00:00:00 -0000
@@ -0,0 +1,9 @@
+<?php
+// $Id$
+
+/**
+ * @file
+ * Curl implementation of for the browser.
+ */
+
+// Best file in Drupal.
Index: includes/browser/curl.inc
===================================================================
RCS file: includes/browser/curl.inc
diff -N includes/browser/curl.inc
--- /dev/null	1 Jan 1970 00:00:00 -0000
+++ includes/browser/curl.inc	1 Jan 1970 00:00:00 -0000
@@ -0,0 +1,163 @@
+<?php
+// $Id$
+
+/**
+ * @file
+ * Curl implementation of for the browser.
+ */
+
+class HttpWrapper_curl implements HttpWrapper {
+  /**
+   * The handle of the current cURL connection.
+   *
+   * @var resource
+   */
+  protected $handle;
+
+  /**
+   * The current cookie file used by cURL.
+   *
+   * We do not reuse the cookies in further runs, so we do not need a file
+   * but we still need cookie handling, so we set the jar to NULL.
+   */
+  protected $cookieFile = NULL;
+
+  protected $request_headers = array();
+
+  protected $url;
+
+  protected $headers;
+
+  protected $content;
+
+  public function Browser_curl() {
+
+  }
+
+  public static function info() {
+    return array(
+      'name' => 'cURL',
+      'cookies' => TRUE,
+      'methods' => array('POST', 'GET'),
+    );
+  }
+
+  public function open() {
+    if (!isset($this->handle)) {
+      $this->handle = curl_init();
+      curl_setopt_array($this->handle, $this->getDefaultOptions());
+    }
+  }
+
+  /**
+   * Close the cURL handler and unset the handler.
+   */
+  public function close() {
+    if (isset($this->handle)) {
+      curl_close($this->handle);
+      unset($this->handle);
+    }
+  }
+
+  protected function getDefaultOptions() {
+    return array(
+      CURLOPT_COOKIEJAR => $this->cookieFile,
+      CURLOPT_FOLLOWLOCATION => TRUE,
+      CURLOPT_HEADERFUNCTION => array(&$this, 'headerCallback'),
+      CURLOPT_HTTPHEADER => $this->request_headers,
+      CURLOPT_RETURNTRANSFER => TRUE,
+      CURLOPT_SSL_VERIFYPEER => FALSE,
+      CURLOPT_SSL_VERIFYHOST => FALSE,
+      CURLOPT_URL => '/',
+      CURLOPT_USERAGENT => $this->request_headers['User-Agent'],
+    );
+  }
+
+  public function getRequestHeaders() {
+    return $this->request_headers;
+  }
+
+  public function setRequestHeaders($headers = array()) {
+    $this->request_headers = $headers;
+
+    // Update request headers if handle is open.
+    if (isset($this->handle)) {
+      curl_setopt($this->handle, CURLOPT_USERAGENT, $this->request_headers['User-Agent']);
+      curl_setopt($this->handle, CURLOPT_HTTPHEADER, $this->request_headers);
+    }
+  }
+
+  public function get($url) {
+    $this->execute(array(
+      CURLOPT_HTTPGET => TRUE,
+      CURLOPT_URL => $url,
+      CURLOPT_NOBODY => FALSE,
+    ));
+
+    return $this->buildRequest();
+  }
+
+  public function post($url, array $fields) {
+    // TODO Add upload handling code.
+
+    foreach ($fields as $key => $value) {
+      // Encode according to application/x-www-form-urlencoded
+      // Both names and values needs to be urlencoded, according to
+      // http://www.w3.org/TR/html4/interact/forms.html#h-17.13.4.1
+      $fields[$key] = urlencode($key) . '=' . urlencode($value);
+    }
+
+    $this->execute(array(
+      CURLOPT_POST => TRUE,
+      CURLOPT_URL => $url,
+      CURLOPT_POSTFIELDS => implode('&', $fields),
+    ));
+
+    return $this->buildRequest();
+  }
+
+  public function request($method) {
+    // TODO CURLOPT_CUSTOMREQUEST
+  }
+
+  protected function buildRequest() {
+    if ($this->content) {
+      return array(
+        'url' => $this->url,
+        'headers' => $this->headers,
+        'content' => $this->content,
+      );
+    }
+    return FALSE;
+  }
+
+  /**
+   * Performs a cURL exec with the specified options after calling curlConnect().
+   *
+   * @param $options
+   *   Changes to the current cURL options.
+   */
+  protected function execute($options) {
+    $this->open();
+
+    curl_setopt_array($this->handle, $options);
+    $this->content = curl_exec($this->handle);
+    $this->url = curl_getinfo($this->handle, CURLINFO_EFFECTIVE_URL);
+    // $this->headers should be filled by headerCallback.
+  }
+
+  /**
+   * Reads 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)
+   */
+  protected function headerCallback($handler, $header) {
+    $this->headers[] = $header;
+    return strlen($header);
+  }
+}
Index: includes/browser/browser.inc
===================================================================
RCS file: includes/browser/browser.inc
diff -N includes/browser/browser.inc
--- /dev/null	1 Jan 1970 00:00:00 -0000
+++ includes/browser/browser.inc	1 Jan 1970 00:00:00 -0000
@@ -0,0 +1,150 @@
+<?php
+// $Id$
+
+/**
+ * @file
+ * Text web browser for Drupal.
+ */
+
+/*
+ * TODO
+ *
+ * Hooks for get, post events.
+ * HTTP authentication
+ * Allow custom headers
+ * Decide on parsing which will allow for meta refresh suppose
+ * Deal with Drupal-Assertion callback stuff.
+ * Add context driven url handling.
+ * Support PUT, DELETE, HEAD, and OPTIONS -chx
+ *   http://moggy.laceous.com/2008/08/01/custom-request-methods-and-php/
+ *   "RESTfully designed web-services use POST, GET, PUT, and DELETE"
+ *
+ * Look at: http://pear.php.net/package/HTTP_Request2/download
+ */
+
+class Browser {
+  /**
+   * The DrupalBrowser instance.
+   *
+   * @var DrupalBrowser
+   */
+  protected static $browser;
+
+  protected $wrapper;
+
+  protected $info;
+
+  /**
+   * The URL currently loaded in the browser.
+   *
+   * @var string
+   */
+  protected $url;
+
+  /**
+   * The headers of the page currently loaded in the internal browser.
+   *
+   * @var Array
+   */
+  protected $headers;
+
+  /**
+   * The content of the page currently loaded in the internal browser.
+   *
+   * @var string
+   */
+  protected $content;
+
+  protected function Browser() {
+    // TODO use variable to switch between curl and stream.
+    require_once './includes/browser/wrapper.inc';
+    require_once './includes/browser/curl.inc';
+
+    $this->wrapper = new HttpWrapper_curl();
+    $this->info = HttpWrapper_curl::info();
+    $this->setUserAgent('Drupal (+http://drupal.org/)');
+  }
+
+  final public static function getInstance() {
+    if (!isset(self::$browser)) {
+      self::$browser = new Browser();
+    }
+    return self::$browser;
+  }
+
+  public function getUserAgent() {
+    return $this->request_headers['User-Agent'];
+  }
+
+  public function setUserAgent($agent) {
+    $this->request_headers['User-Agent'] = $agent;
+    $this->wrapper->setRequestHeaders($this->request_headers);
+  }
+
+  /**
+   * Make an HTTP GET request to the specified URL.
+   *
+   * @param $url
+   *   Full URL to retrieve.
+   * @return
+   *   Associative array...
+   */
+  public function get($url) {
+    if (!$this->isMethodSupported('GET')) {
+      trigger_error(t('@backend HTTP wrapper does not support GET requests.', array('@backend' => $this->info['name'])));
+      return FALSE;
+    }
+
+    // TODO Add context driven url handling.
+    $request = $this->wrapper->get($url);
+
+    // TODO Error check, look for meta refresh, etc.
+    $this->setState($request['url'], $request['headers'], $request['content']);
+    return $request;
+  }
+
+  public function post($url, array $fields, $submit) {
+    if (!$this->isMethodSupported('POST')) {
+      trigger_error(t('@backend HTTP wrapper does not support POST requests.', array('@backend' => $this->info['name'])));
+      return FALSE;
+    }
+
+    // TODO Make sure all values exist on page.
+    if ($url) {
+      $this->wrapper->get($url);
+    }
+
+    // TODO Add context driven url handling.
+    $request = $this->wrapper->post($url, $fields);
+
+    // TODO Error check, look for meta refresh, etc.
+    $this->setState($request['url'], $request['headers'], $request['content']);
+    return $request;
+  }
+
+  public function request($method) {
+
+  }
+
+  public function isMethodSupported($method) {
+    return in_array(strtoupper($method), $this->info['methods']);
+  }
+
+  protected function setState($url, $headers, $content) {
+    $this->url = $url;
+    $this->headers = $headers;
+    $this->content = $content;
+
+//    module_invoke_all('browser_request', self::$browser); // TODO decide on hooks
+  }
+
+  /**
+   * Gets the current raw HTML of the last requested page.
+   *
+   * @return
+   *   Raw HTML of last requested page.
+   */
+  public function getContent() {
+    return $this->content;
+  }
+}
