Index: modules/simpletest/drupal_web_test_case.php
===================================================================
RCS file: /cvs/drupal/drupal/modules/simpletest/drupal_web_test_case.php,v
retrieving revision 1.76
diff -u -r1.76 drupal_web_test_case.php
--- modules/simpletest/drupal_web_test_case.php	20 Dec 2008 18:24:39 -0000	1.76
+++ modules/simpletest/drupal_web_test_case.php	27 Dec 2008 01:01:50 -0000
@@ -20,12 +20,7 @@
    */
   protected $url;
 
-  /**
-   * The handle of the current cURL connection.
-   *
-   * @var resource
-   */
-  protected $curlHandle;
+  protected $browser;
 
   /**
    * The headers of the page currently loaded in the internal browser.
@@ -48,12 +43,7 @@
    */
   protected $plainTextContent;
 
-  /**
-   * The parsed version of the page.
-   *
-   * @var SimpleXMLElement
-   */
-  protected $elements = NULL;
+  protected $parsed = FALSE;
 
   /**
    * Whether a user is logged in the internal browser.
@@ -904,7 +894,7 @@
       $this->refreshVariables();
 
       // Close the CURL handler.
-      $this->curlClose();
+      $this->browserReset();
     }
   }
 
@@ -915,80 +905,47 @@
    * simpletest_httpauth_username and simpletest_httpauth_pass variables. Also,
    * see the description of $curl_options among the properties.
    */
-  protected function curlInitialize() {
+  protected function browserInit() {
     global $base_url, $db_prefix;
-    if (!isset($this->curlHandle)) {
-      $this->curlHandle = curl_init();
-      $curl_options = $this->additionalCurlOptions + array(
-        CURLOPT_COOKIEJAR => $this->cookieFile,
-        CURLOPT_URL => $base_url,
-        CURLOPT_FOLLOWLOCATION => TRUE,
-        CURLOPT_RETURNTRANSFER => TRUE,
-        CURLOPT_SSL_VERIFYPEER => FALSE,  // Required to make the tests run on https://
-        CURLOPT_SSL_VERIFYHOST => FALSE,  // Required to make the tests run on https://
-        CURLOPT_HEADERFUNCTION => array(&$this, 'curlHeaderCallback'),
-      );
+
+    if (!isset($this->browser)) {
+      require_once './includes/browser/browser.inc'; // TODO autoloaded?
+      $this->browser = Browser::getInstance('curl');
+      // TODO Allow setting of cookie file? or some sort of session handling.
+
       if (preg_match('/simpletest\d+/', $db_prefix, $matches)) {
-        $curl_options[CURLOPT_USERAGENT] = $matches[0];
-      }
-      if (!isset($curl_options[CURLOPT_USERPWD]) && ($auth = variable_get('simpletest_httpauth_username', ''))) {
-        if ($pass = variable_get('simpletest_httpauth_pass', '')) {
-          $auth .= ':' . $pass;
-        }
-        $curl_options[CURLOPT_USERPWD] = $auth;
+        $this->browser->setUserAgent($matches[0]);
       }
-      curl_setopt_array($this->curlHandle, $this->additionalCurlOptions + $curl_options);
+      // TODO http auth
+//      if (!isset($curl_options[CURLOPT_USERPWD]) && ($auth = variable_get('simpletest_httpauth_username', ''))) {
+//        if ($pass = variable_get('simpletest_httpauth_pass', '')) {
+//          $auth .= ':' . $pass;
+//        }
+//        $curl_options[CURLOPT_USERPWD] = $auth;
+//      }
     }
   }
 
-  /**
-   * Performs a cURL exec with the specified options after calling curlConnect().
-   *
-   * @param $curl_options
-   *   Custom cURL options.
-   * @return
-   *   Content returned from the exec.
-   */
-  protected function curlExec($curl_options) {
-    $this->curlInitialize();
-    $url = empty($curl_options[CURLOPT_URL]) ? curl_getinfo($this->curlHandle, CURLINFO_EFFECTIVE_URL) : $curl_options[CURLOPT_URL];
-    curl_setopt_array($this->curlHandle, $this->additionalCurlOptions + $curl_options);
-    $this->headers = array();
-    $this->drupalSetContent(curl_exec($this->curlHandle), curl_getinfo($this->curlHandle, CURLINFO_EFFECTIVE_URL));
-    $this->assertTrue($this->content !== FALSE, t('!method to !url, response is !length bytes.', array('!method' => !empty($curl_options[CURLOPT_NOBODY]) ? 'HEAD' : (empty($curl_options[CURLOPT_POSTFIELDS]) ? 'GET' : 'POST'), '!url' => $url, '!length' => strlen($this->content))), t('Browser'));
-    return $this->drupalGetContent();
-  }
-
-  /**
-   * Reads headers and registers errors received from the tested site.
-   *
-   * @see _drupal_log_error().
-   *
-   * @param $curlHandler
-   *   The cURL handler.
-   * @param $header
-   *   An header.
-   */
-  protected function curlHeaderCallback($curlHandler, $header) {
-    $this->headers[] = $header;
-    // Errors are being sent via X-Drupal-Assertion-* headers,
-    // generated by _drupal_log_error() in the exact form required
-    // by DrupalWebTestCase::error().
-    if (preg_match('/^X-Drupal-Assertion-[0-9]+: (.*)$/', $header, $matches)) {
-      // Call DrupalWebTestCase::error() with the parameters from the header.
-      call_user_func_array(array(&$this, 'error'), unserialize(urldecode($matches[1])));
+  protected function checkHeaders() {
+    $headers = $this->browser->getHeaders();
+    foreach ($headers as $key => $value) {
+      // Errors are being sent via X-Drupal-Assertion-* headers,
+      // generated by _drupal_log_error() in the exact form required
+      // by DrupalWebTestCase::error().
+      if (preg_match('/X-Drupal-Assertion-[0-9]+/', $key)) {
+        // Call DrupalWebTestCase::error() with the parameters from the header.
+        call_user_func_array(array(&$this, 'error'), unserialize($value));
+      }
     }
-    // This is required by cURL.
-    return strlen($header);
   }
 
   /**
    * Close the cURL handler and unset the handler.
    */
-  protected function curlClose() {
-    if (isset($this->curlHandle)) {
-      curl_close($this->curlHandle);
-      unset($this->curlHandle);
+  protected function browserReset() {
+    if (isset($this->browser)) {
+      $this->browser->resetConnection();
+      unset($this->browser);
     }
   }
 
@@ -999,22 +956,16 @@
    *   A SimpleXMLElement or FALSE on failure.
    */
   protected function parse() {
-    if (!$this->elements) {
-      // DOM can load HTML soup. But, HTML soup can throw warnings, supress
-      // them.
-      @$htmlDom = DOMDocument::loadHTML($this->content);
-      if ($htmlDom) {
+    if (!$this->parsed) {
+      if ($this->browser->getPage()) {
         $this->pass(t('Valid HTML found on "@path"', array('@path' => $this->getUrl())), t('Browser'));
-        // It's much easier to work with simplexml than DOM, luckily enough
-        // we can just simply import our DOM tree.
-        $this->elements = simplexml_import_dom($htmlDom);
       }
+      else {
+        $this->fail(t('Parsed page successfully.'), t('Browser'));
+      }
+      $this->parsed = TRUE;
     }
-    if (!$this->elements) {
-      $this->fail(t('Parsed page successfully.'), t('Browser'));
-    }
-
-    return $this->elements;
+    return $this->browser->getPage();
   }
 
   /**
@@ -1033,17 +984,20 @@
   protected function drupalGet($path, array $options = array(), array $headers = array()) {
     $options['absolute'] = TRUE;
 
-    // We re-using a CURL connection here. If that connection still has certain
-    // options set, it might change the GET into a POST. Make sure we clear out
-    // previous options.
-    $out = $this->curlExec(array(CURLOPT_HTTPGET => TRUE, CURLOPT_URL => url($path, $options), CURLOPT_NOBODY => FALSE, CURLOPT_HTTPHEADER => $headers));
-    $this->refreshVariables(); // Ensure that any changes to variables in the other thread are picked up.
+    // TODO change header format.
 
-    // Replace original page output with new output from redirected page(s).
-    if (($new = $this->checkForMetaRefresh())) {
-      $out = $new;
-    }
-    return $out;
+    $this->browserInit();
+//    $this->browser->setRequestHeaders($headers);
+    $request = $this->browser->get($url = url($path, $options));
+    $this->assertTrue($request, t('!method to !url, response is !length bytes.', array(
+      '!method' => 'GET',
+      '!url' => $url,
+      '!length' => strlen($request['content']))), t('Browser'));
+    $this->checkHeaders();
+
+    // Ensure that any changes to variables in the other thread are picked up.
+    $this->refreshVariables();
+    return $request['content'];
   }
 
   /**
@@ -1082,85 +1036,33 @@
    *   "name: value".
    */
   protected function drupalPost($path, $edit, $submit, array $options = array(), array $headers = array()) {
-    $submit_matches = FALSE;
-    if (isset($path)) {
-      $html = $this->drupalGet($path, $options);
-    }
-    if ($this->parse()) {
-      $edit_save = $edit;
-      // Let's iterate over all the forms.
-      $forms = $this->xpath('//form');
-      foreach ($forms as $form) {
-        // We try to set the fields of this form as specified in $edit.
-        $edit = $edit_save;
-        $post = array();
-        $upload = array();
-        $submit_matches = $this->handleForm($post, $edit, $upload, $submit, $form);
-        $action = isset($form['action']) ? $this->getAbsoluteUrl($form['action']) : $this->getUrl();
-
-        // We post only if we managed to handle every field in edit and the
-        // submit button matches.
-        if (!$edit && $submit_matches) {
-          if ($upload) {
-            // TODO: cURL handles file uploads for us, but the implementation
-            // is broken. This is a less than elegant workaround. Alternatives
-            // are being explored at #253506.
-            foreach ($upload as $key => $file) {
-              $file = realpath($file);
-              if ($file && is_file($file)) {
-                $post[$key] = '@' . $file;
-              }
-            }
-          }
-          else {
-            foreach ($post 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
-              $post[$key] = urlencode($key) . '=' . urlencode($value);
-            }
-            $post = implode('&', $post);
-          }
-          $out = $this->curlExec(array(CURLOPT_URL => $action, CURLOPT_POST => TRUE, CURLOPT_POSTFIELDS => $post, CURLOPT_HTTPHEADER => $headers));
-          // Ensure that any changes to variables in the other thread are picked up.
-          $this->refreshVariables();
-
-          // Replace original page output with new output from redirected page(s).
-          if (($new = $this->checkForMetaRefresh())) {
-            $out = $new;
-          }
-          return $out;
-        }
-      }
-      // We have not found a form which contained all fields of $edit.
-      foreach ($edit as $name => $value) {
-        $this->fail(t('Failed to set field @name to @value', array('@name' => $name, '@value' => $value)));
-      }
-      $this->assertTrue($submit_matches, t('Found the @submit button', array('@submit' => $submit)));
-      $this->fail(t('Found the requested form fields at @path', array('@path' => $path)));
-    }
-  }
+    // TODO change header format.
 
-  /**
-   * Check for meta refresh tag and if found call drupalGet() recursively. This
-   * function looks for the http-equiv attribute to be set to "Refresh"
-   * and is case-sensitive.
-   *
-   * @return
-   *   Either the new page content or FALSE.
-   */
-  protected function checkForMetaRefresh() {
-    if ($this->drupalGetContent() != '' && $this->parse()) {
-      $refresh = $this->xpath('//meta[@http-equiv="Refresh"]');
-      if (!empty($refresh)) {
-        // Parse the content attribute of the meta tag for the format:
-        // "[delay]: URL=[page_to_redirect_to]".
-        if (preg_match('/\d+;\s*URL=(?P<url>.*)/i', $refresh[0]['content'], $match)) {
-          return $this->drupalGet($this->getAbsoluteUrl(decode_entities($match['url'])));
-        }
-      }
-    }
-    return FALSE;
+    $this->browserInit();
+
+    $options['absolute'] = TRUE;
+
+    $url = (isset($path) ? url($path, $options) : NULL);
+    $pre_url = ($url ? $url : $this->browser->getUrl());
+
+//    $this->browser->setRequestHeaders($headers);
+    $request = $this->browser->post($url, $edit, $submit);
+    $this->assertTrue($request, t('!method to !url, response is !length bytes.', array(
+      '!method' => 'POST',
+      '!url' => $pre_url,
+      '!length' => strlen($request['content']))), t('Browser'));
+    $this->checkHeaders();
+
+    // Ensure that any changes to variables in the other thread are picked up.
+    $this->refreshVariables();
+
+    // TODO add error handling in post that lets us know this.
+//      // We have not found a form which contained all fields of $edit.
+//      foreach ($edit as $name => $value) {
+//        $this->fail(t('Failed to set field @name to @value', array('@name' => $name, '@value' => $value)));
+//      }
+//      $this->assertTrue($submit_matches, t('Found the @submit button', array('@submit' => $submit)));
+//      $this->fail(t('Found the requested form fields at @path', array('@path' => $path)));
   }
 
   /**
@@ -1177,6 +1079,7 @@
    *   The retrieved headers, also available as $this->drupalGetContent()
    */
   protected function drupalHead($path, array $options = array(), array $headers = array()) {
+    // TODO
     $options['absolute'] = TRUE;
     $out = $this->curlExec(array(CURLOPT_NOBODY => TRUE, CURLOPT_URL => url($path, $options), CURLOPT_HTTPHEADER => $headers));
     $this->refreshVariables(); // Ensure that any changes to variables in the other thread are picked up.
@@ -1184,136 +1087,6 @@
   }
 
   /**
-   * Handle form input related to drupalPost(). Ensure that the specified fields
-   * exist and attempt to create POST data in the correct manner for the particular
-   * field type.
-   *
-   * @param $post
-   *   Reference to array of post values.
-   * @param $edit
-   *   Reference to array of edit values to be checked against the form.
-   * @param $submit
-   *   Form submit button value.
-   * @param $form
-   *   Array of form elements.
-   * @return
-   *   Submit value matches a valid submit input in the form.
-   */
-  protected function handleForm(&$post, &$edit, &$upload, $submit, $form) {
-    // Retrieve the form elements.
-    $elements = $form->xpath('.//input|.//textarea|.//select');
-    $submit_matches = FALSE;
-    foreach ($elements as $element) {
-      // SimpleXML objects need string casting all the time.
-      $name = (string) $element['name'];
-      // This can either be the type of <input> or the name of the tag itself
-      // for <select> or <textarea>.
-      $type = isset($element['type']) ? (string)$element['type'] : $element->getName();
-      $value = isset($element['value']) ? (string)$element['value'] : '';
-      $done = FALSE;
-      if (isset($edit[$name])) {
-        switch ($type) {
-          case 'text':
-          case 'textarea':
-          case 'password':
-            $post[$name] = $edit[$name];
-            unset($edit[$name]);
-            break;
-          case 'radio':
-            if ($edit[$name] == $value) {
-              $post[$name] = $edit[$name];
-              unset($edit[$name]);
-            }
-            break;
-          case 'checkbox':
-            // To prevent checkbox from being checked.pass in a FALSE,
-            // otherwise the checkbox will be set to its value regardless
-            // of $edit.
-            if ($edit[$name] === FALSE) {
-              unset($edit[$name]);
-              continue 2;
-            }
-            else {
-              unset($edit[$name]);
-              $post[$name] = $value;
-            }
-            break;
-          case 'select':
-            $new_value = $edit[$name];
-            $index = 0;
-            $key = preg_replace('/\[\]$/', '', $name);
-            $options = $this->getAllOptions($element);
-            foreach ($options as $option) {
-              if (is_array($new_value)) {
-                $option_value= (string)$option['value'];
-                if (in_array($option_value, $new_value)) {
-                  $post[$key . '[' . $index++ . ']'] = $option_value;
-                  $done = TRUE;
-                  unset($edit[$name]);
-                }
-              }
-              elseif ($new_value == $option['value']) {
-                $post[$name] = $new_value;
-                unset($edit[$name]);
-                $done = TRUE;
-              }
-            }
-            break;
-          case 'file':
-            $upload[$name] = $edit[$name];
-            unset($edit[$name]);
-            break;
-        }
-      }
-      if (!isset($post[$name]) && !$done) {
-        switch ($type) {
-          case 'textarea':
-            $post[$name] = (string)$element;
-            break;
-          case 'select':
-            $single = empty($element['multiple']);
-            $first = TRUE;
-            $index = 0;
-            $key = preg_replace('/\[\]$/', '', $name);
-            $options = $this->getAllOptions($element);
-            foreach ($options as $option) {
-              // For single select, we load the first option, if there is a
-              // selected option that will overwrite it later.
-              if ($option['selected'] || ($first && $single)) {
-                $first = FALSE;
-                if ($single) {
-                  $post[$name] = (string)$option['value'];
-                }
-                else {
-                  $post[$key . '[' . $index++ . ']'] = (string)$option['value'];
-                }
-              }
-            }
-            break;
-          case 'file':
-            break;
-          case 'submit':
-          case 'image':
-            if ($submit == $value) {
-              $post[$name] = $value;
-              $submit_matches = TRUE;
-            }
-            break;
-          case 'radio':
-          case 'checkbox':
-            if (!isset($element['checked'])) {
-              break;
-            }
-            // Deliberate no break.
-          default:
-            $post[$name] = $value;
-        }
-      }
-    }
-    return $submit_matches;
-  }
-
-  /**
    * Peform an xpath search on the contents of the internal browser. The search
    * is relative to the root element (HTML tag normally) of the page.
    *
@@ -1325,37 +1098,13 @@
    *   http://us.php.net/manual/function.simplexml-element-xpath.php
    */
   protected function xpath($xpath) {
-    if ($this->parse()) {
-      return $this->elements->xpath($xpath);
+    if ($page = $this->parse()) {
+      return $page->xpath($xpath);
     }
     return FALSE;
   }
 
   /**
-   * Get all option elements, including nested options, in a select.
-   *
-   * @param $element
-   *   The element for which to get the options.
-   * @return
-   *   Option elements in select.
-   */
-  protected function getAllOptions(SimpleXMLElement $element) {
-    $options = array();
-    // Add all options items.
-    foreach ($element->option as $option) {
-      $options[] = $option;
-    }
-
-    // Search option group children.
-    if (isset($element->optgroup)) {
-      foreach ($element->optgroup as $group) {
-        $options = array_merge($options, $this->getAllOptions($group));
-      }
-    }
-    return $options;
-  }
-
-  /**
    * Pass if a link with the specified label is found, and optional with the
    * specified index.
    *
@@ -1408,19 +1157,17 @@
    *   Page on success, or FALSE on failure.
    */
   protected function clickLink($label, $index = 0) {
-    $url_before = $this->getUrl();
-    $urls = $this->xpath('//a[text()="' . $label . '"]');
-
-    if (isset($urls[$index])) {
-      $url_target = $this->getAbsoluteUrl($urls[$index]['href']);
+    if ($page = $this->parse()) {
+      $url_target = $page->getLink($label, $index);
+      $url_before = $this->getUrl();
     }
 
-    $this->assertTrue(isset($urls[$index]), t('Clicked link "!label" (!url_target) from !url_before', array('!label' => $label, '!url_target' => $url_target, '!url_before' => $url_before)), t('Browser'));
+    $this->assertTrue(isset($url_target), t('Clicked link "!label" (!url_target) from !url_before', array('!label' => $label, '!url_target' => $url_target, '!url_before' => $url_before)), t('Browser'));
 
-    if (isset($urls[$index])) {
-      return $this->drupalGet($url_target);
+    if (isset($url_target)) {
+      $request = $this->drupalGet($url_target);
     }
-    return FALSE;
+    return (isset($request) ? $request['content'] : FALSE);
   }
 
   /**
@@ -1458,7 +1205,7 @@
    *   The current url.
    */
   protected function getUrl() {
-    return $this->url;
+    return $this->browser->getUrl();
   }
 
   /**
@@ -1551,7 +1298,7 @@
    * Gets the current raw HTML of requested page.
    */
   protected function drupalGetContent() {
-    return $this->content;
+    return $this->browser->getContent();
   }
 
   /**
@@ -1584,7 +1331,7 @@
    *   TRUE on pass, FALSE on fail.
    */
   protected function assertRaw($raw, $message = '%s found', $group = 'Other') {
-    return $this->assert(strpos($this->content, $raw) !== FALSE, $message, $group);
+    return $this->assert(strpos($this->drupalGetContent(), $raw) !== FALSE, $message, $group);
   }
 
   /**
@@ -1601,7 +1348,7 @@
    *   TRUE on pass, FALSE on fail.
    */
   protected function assertNoRaw($raw, $message = '%s found', $group = 'Other') {
-    return $this->assert(strpos($this->content, $raw) === FALSE, $message, $group);
+    return $this->assert(strpos($this->drupalGetContent(), $raw) === FALSE, $message, $group);
   }
 
   /**
@@ -1657,13 +1404,10 @@
    *   TRUE on pass, FALSE on fail.
    */
   protected function assertTextHelper($text, $message, $group, $not_exists) {
-    if ($this->plainTextContent === FALSE) {
-      $this->plainTextContent = filter_xss($this->content, array());
-    }
     if (!$message) {
       $message = '"' . $text . '"' . ($not_exists ? ' not found' : ' found');
     }
-    return $this->assert($not_exists == (strpos($this->plainTextContent, $text) === FALSE), $message, $group);
+    return $this->assert($not_exists == (strpos($this->browser->getContentPlain(), $text) === FALSE), $message, $group);
   }
 
   /**
@@ -1711,7 +1455,10 @@
    *   TRUE on pass, FALSE on fail.
    */
   protected function assertTitle($title, $message, $group = 'Other') {
-    return $this->assertTrue($this->xpath('//title[text()="' . $title . '"]'), $message, $group);
+    if ($page = $this->parse()) {
+      $pass = $title == $page->getTitle();
+    }
+    return $this->assertTrue((isset($pass) ? $pass : FALSE), $message, $group);
   }
 
   /**
@@ -1727,7 +1474,10 @@
    *   TRUE on pass, FALSE on fail.
    */
   protected function assertNoTitle($title, $message, $group = 'Other') {
-    return $this->assertFalse($this->xpath('//title[text()="' . $title . '"]'), $message, $group);
+    if ($page = $this->parse()) {
+      $pass = $title == $page->getTitle();
+    }
+    return $this->assertFalse((isset($pass) ? $pass : FALSE), $message, $group);
   }
 
   /**
@@ -1759,13 +1509,16 @@
           }
           elseif (isset($field->option)) {
             // Select element found.
-            if ($this->getSelectedItem($field) == $value) {
-              $found = TRUE;
+            $options = $this->browser->getPage()->getSelectedOptions();
+            foreach ($options as $key => $text) {
+              if ($key == $value) {
+                $found = TRUE;
+              }
             }
-            else {
-              // No item selected so use first item.
-              $items = $this->getAllOptions($field);
-              if (!empty($items) && $items[0]['value'] == $value) {
+            if (!$found && !$options) {
+              // No item(s) selected so use first item.
+              $options = $this->browser->getPage()->getSelectElements();
+              if (!empty($options) && $options[0]['value'] == $value) {
                 $found = TRUE;
               }
             }
@@ -1781,28 +1534,6 @@
   }
 
   /**
-   * Get the selected value from a select field.
-   *
-   * @param $element
-   *   SimpleXMLElement select element.
-   * @return
-   *   The selected value or FALSE.
-   */
-  protected function getSelectedItem(SimpleXMLElement $element) {
-    foreach ($element->children() as $item) {
-      if (isset($item['selected'])) {
-        return $item['value'];
-      }
-      elseif ($item->getName() == 'optgroup') {
-        if ($value = $this->getSelectedItem($item)) {
-          return $value;
-        }
-      }
-    }
-    return FALSE;
-  }
-
-  /**
    * Assert that a field does not exist in the current page by the given XPath.
    *
    * @param $xpath
@@ -1964,7 +1695,7 @@
    *   Assertion result.
    */
   protected function assertResponse($code, $message = '') {
-    $curl_code = curl_getinfo($this->curlHandle, CURLINFO_HTTP_CODE);
+    $curl_code = $this->browser->getCode();
     $match = is_array($code) ? in_array($curl_code, $code) : $curl_code == $code;
     return $this->assertTrue($match, $message ? $message : t('HTTP response expected !code, actual !curl_code', array('!code' => $code, '!curl_code' => $curl_code)), t('Browser'));
   }
Index: modules/system/system.module
===================================================================
RCS file: /cvs/drupal/drupal/modules/system/system.module,v
retrieving revision 1.653
diff -u -r1.653 system.module
--- modules/system/system.module	24 Dec 2008 09:59:22 -0000	1.653
+++ modules/system/system.module	27 Dec 2008 01:01:50 -0000
@@ -228,6 +228,26 @@
 }
 
 /**
+ * Implementation of hook_browser_wrapper().
+ */
+function system_browser_wrapper() {
+  return array(
+    'stream' => array(
+      'file' => './includes/browser/stream.inc',
+      'name' => 'Stream',
+      'cookie' => FALSE,
+      'methods' => array('GET', 'POST'),
+    ),
+    'curl' => array(
+      'file' => './includes/browser/curl.inc',
+      'name' => 'cURL',
+      'cookies' => TRUE,
+      'methods' => array('GET', 'POST'),
+    ),
+  );
+}
+
+/**
  * Implementation of hook_elements().
  */
 function system_elements() {
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,59 @@
+<?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/');
+//$request = $browser->get('http://www.archive.org/');
+
+$fields = array(
+  'name' => 'user',
+  'pass' => 'password',
+);
+$request = $browser->post(url('', array('absolute' => TRUE)), $fields, 'Log in');
+//$request = $browser->post(url('user/password', array('absolute' => TRUE)), $fields, 'E-mail new password');
+
+//$request = $browser->get(url('user', array('absolute' => TRUE)));
+//$browser->get(url('user', array('absolute' => TRUE)));
+
+//$request = $browser->get(url('user', array('absolute' => TRUE)));
+
+$curl = Browser::getInstance('curl');
+$curl->get(url('user', array('absolute' => TRUE)));
+
+echo $curl->getContent();
+//echo $browser->getContent();
+
+if ($request) {
+//  print_r($request['headers']);
+//  echo 'url: ' . $request['url'];
+  echo $request['content'];
+
+//  echo $browser->getContent();
+
+//  $page = $browser->getPage();
+//  echo ($page ? 'true' : 'false');
+//  print_r($page->xpath('//img[@src="images/ipc.gif"]'));
+}
+else {
+  echo 'FAIL!';
+}
Index: modules/simpletest/tests/browser.test
===================================================================
RCS file: modules/simpletest/tests/browser.test
diff -N modules/simpletest/tests/browser.test
--- /dev/null	1 Jan 1970 00:00:00 -0000
+++ modules/simpletest/tests/browser.test	1 Jan 1970 00:00:00 -0000
@@ -0,0 +1,18 @@
+<?php
+// $Id$
+
+class BrowserTestCase extends DrupalWebTestCase {
+  public function getInfo() {
+    return array(
+      'name' => t('GET Request'),
+      'description' => t('Checks the GET request funcitonality.'),
+      'group' => t('Browser')
+    );
+  }
+
+  public function testGetRequest() {
+    $this->drupalGet('user');
+    $this->assertText('Drupal');
+    $this->assertNoText('Drupal243525265');
+  }
+}
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,24 @@
+<?php
+// $Id$
+
+/**
+ * @file
+ * Text web browser for Drupal.
+ */
+
+interface HttpWrapper {
+
+  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,131 @@
+<?php
+// $Id$
+
+/**
+ * @file
+ * Stream implementation of for the browser.
+ */
+
+class HttpWrapper_stream implements HttpWrapper {
+
+  protected $handle;
+
+  protected $request_headers = array();
+
+  protected $cookie = array();
+
+  protected $url;
+
+  protected $headers;
+
+  protected $content;
+
+  public function open() {
+    if (!isset($this->handle)) {
+      $this->handle = stream_context_create();
+    }
+  }
+
+  public function close() {
+
+  }
+
+  public function getRequestHeaders() {
+    return $this->request_headers;
+  }
+
+  public function setRequestHeaders($headers = array()) {
+    $this->request_headers = $headers;
+  }
+
+  public function get($url) {
+    $this->execute($url, array(
+      'method' => 'GET',
+    ));
+
+    return $this->buildRequest();
+  }
+
+  public function post($url, array $fields) {
+    $this->execute($url, array(
+      'method'  => 'POST',
+      'header'  => array(
+        'Content-Type' => 'application/x-www-form-urlencoded',
+      ),
+      'content' => http_build_query($fields, NULL, '&'),
+    ));
+
+    return $this->buildRequest();
+  }
+
+  public function request($method) {
+    return FALSE;
+  }
+
+  protected function buildRequest() {
+    if ($this->content !== FALSE) {
+      return array(
+        'url' => $this->url,
+        'headers' => $this->headers,
+        'content' => $this->content,
+      );
+    }
+    return FALSE;
+  }
+
+  protected function execute($url, $options) {
+    global $http_response_header;
+
+    $this->open();
+
+    if (!isset($options['header'])) {
+      $options['header'] = array();
+    }
+
+    // Merge default request headers with the passed headers and generate
+    // header string to be sent in http request.
+    $headers = $this->request_headers + $options['header'];
+    $headers['Cookie'] = $this->cookie;
+    $options['header'] = $this->generateHeaderString($headers);
+
+    // Update the handler options.
+    stream_context_set_params($this->handle, array(
+      'options' => array(
+        'http' => $options,
+      )
+    ));
+
+    // Make the request.
+    $this->content = file_get_contents($url, FALSE, $this->handle);
+    $this->url = $url; // TODO check headers.
+    $this->parseHeaders($http_response_header);
+
+    // Check for specific headers.
+//    if (isset($this->headers['Set-Cookie'])) { // TODO Support an case keys.
+//      $this->cookie = $this->headers['Set-Cookie'];
+//    }
+  }
+
+  protected function generateHeaderString(array $headers) {
+    $string = '';
+    foreach ($headers as $key => $header) {
+      // Remove blank headers.
+      if ($header) {
+        $string .= "$key: $header\r\n";
+      }
+    }
+    return $string;
+  }
+
+  protected function parseHeaders(array $headers) {
+    $this->headers = array();
+    foreach ($headers as $header) {
+      $parts = explode(':', $header, 2);
+
+      // Ensure header line is valid.
+      if (count($parts) == 2) {
+        $this->headers[trim($parts[0])] = trim(urldecode($parts[1]));
+      }
+    }
+  }
+}
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,156 @@
+<?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 $requestHeaders = array();
+
+  protected $url;
+
+  protected $code;
+
+  protected $headers;
+
+  protected $content;
+
+  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->requestHeaders,
+      CURLOPT_RETURNTRANSFER => TRUE,
+      CURLOPT_SSL_VERIFYPEER => FALSE,
+      CURLOPT_SSL_VERIFYHOST => FALSE,
+      CURLOPT_URL => '/',
+      CURLOPT_USERAGENT => $this->requestHeaders['User-Agent'],
+    );
+  }
+
+  public function getRequestHeaders() {
+    return $this->requestHeaders;
+  }
+
+  public function setRequestHeaders($headers = array()) {
+    $this->requestHeaders = $headers;
+
+    // Update request headers if handle is open.
+    if (isset($this->handle)) {
+      curl_setopt($this->handle, CURLOPT_USERAGENT, $this->requestHeaders['User-Agent']);
+      curl_setopt($this->handle, CURLOPT_HTTPHEADER, $this->requestHeaders);
+    }
+  }
+
+  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) {
+    $this->execute(array(
+      CURLOPT_POST => TRUE,
+      CURLOPT_URL => $url,
+      CURLOPT_POSTFIELDS => $fields,
+    ));
+
+    return $this->buildRequest();
+  }
+
+  public function request($method) {
+    // TODO CURLOPT_CUSTOMREQUEST
+  }
+
+  protected function buildRequest() {
+    if ($this->content !== FALSE) {
+      return array(
+        'url' => $this->url,
+        'code' => $this->code,
+        '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->code = curl_getinfo($this->handle, CURLINFO_HTTP_CODE);
+
+    // $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) {
+    $clean_header = trim($header);
+    if ($clean_header) {
+      $parts = explode(':', $clean_header, 2);
+
+      // Ensure header line is valid.
+      if (count($parts) == 2) {
+        $this->headers[trim($parts[0])] = trim(urldecode($parts[1]));
+      }
+    }
+    return strlen($header);
+  }
+}
Index: includes/browser/page.inc
===================================================================
RCS file: includes/browser/page.inc
diff -N includes/browser/page.inc
--- /dev/null	1 Jan 1970 00:00:00 -0000
+++ includes/browser/page.inc	1 Jan 1970 00:00:00 -0000
@@ -0,0 +1,265 @@
+<?php
+// $Id$
+
+/**
+ * @file
+ * BrowserPage class used with Browser.
+ */
+
+/**
+ * Represents a page of content that has been fetched by the Browser. The class
+ * provides a number of convenience methods that relate to page content.
+ */
+class BrowserPage {
+
+  /**
+   * The URL of the current page.
+   *
+   * @var string
+   */
+  protected $url;
+
+  /**
+   * The headers of the page current page.
+   *
+   * @var Array
+   */
+  protected $headers;
+
+  /**
+   * The root element of the loaded content.
+   *
+   * @var SimpleXMLElement
+   */
+  protected $root;
+
+  /**
+   * Load content into page.
+   *
+   * @param $content
+   *   Content to load.
+   */
+  public function BrowserPage($url, $headers, $content) {
+    $this->url = $url;
+    $this->headers = $headers;
+    $this->root = $this->load($content);
+  }
+
+  /**
+   * Load contents into simplexml.
+   *
+   * @param $content
+   *   Content to load.
+   * @return
+   *   Root SimpleXML element or FALSE.
+   */
+  protected function load($content) {
+    // Use DOM to load HTML soup, and hide warnings.
+    $dom = @DOMDocument::loadHTML($content);
+    if ($dom) {
+      return simplexml_import_dom($dom);
+    }
+    return FALSE;
+  }
+
+  /**
+   * Check if the content could be loaded.
+   *
+   * @return
+   *   TRUE if content is loaded, FALSE if content failed to load.
+   */
+  public function isValid() {
+    return ($this->root !== FALSE);
+  }
+
+  /**
+   * Peform an xpath search on the contents of the page. The search is relative
+   * to the root element (HTML tag normally) of the page.
+   *
+   * @param $xpath
+   *   The xpath string to use in the search.
+   * @return
+   *   The return value of the xpath search. For details on the xpath string
+   *   format and return values see the SimpleXML documentation.
+   *   http://us.php.net/manual/function.simplexml-element-xpath.php
+   */
+  public function xpath($xpath) {
+    return $this->root->xpath($xpath);
+  }
+
+  public function getTitle() {
+    $title = $this->xpath('//title');
+    return ($title ? $this->asText($title) : FALSE);
+  }
+
+  /**
+   * Get all the form elements contained by the page.
+   *
+   * @return
+   *   An array of form elements.
+   */
+  public function getForms() {
+    return $this->xpath('//form');
+  }
+
+  /**
+   * Get all the input elements contained by the page, or nested within a form
+   * when specified.
+   *
+   * @param $form
+   *   Searched for inputs contained by the form.
+   * @return
+   *   Array of input elements.
+   */
+  public function getInputs($form = NULL) {
+    if ($form) {
+      return $form->xpath('.//input|.//textarea|.//select');
+    }
+    return $this->xpath('.//input|.//textarea|.//select');
+  }
+
+  public function getField() {
+    // TODO
+  }
+
+  /**
+   * Get all the options contained by a select, including nested options.
+   *
+   * @param $select
+   *   The select to get the options from.
+   * @return
+   *   Associative array where the keys represent each option value and the
+   *   value is the text contained within the option tag. For example:
+   * @code
+   *   array(
+   *     'option1' => 'Option 1',
+   *     'option2' => 'Option 2',
+   *   )
+   * @endcode
+   */
+  public function getSelectOptions(SimpleXMLElement $select) {
+    $elements = $this->getSelectElements($select);
+
+    $options = array();
+    foreach ($elements as $element) {
+      $options[(string) $element['value']] = $this->asText($element);
+    }
+    return $options;
+  }
+
+  /**
+   * Get all selected options contained by a select, including nested options.
+   *
+   * @param $select
+   *   The select to get the options from.
+   * @return
+   *   Associative array of selected items in the format described by
+   *   getSelectOptions().
+   */
+  public function getSelectedOptions(SimpleXMLElement $select) {
+    $elements = getSelectElements($select);
+
+    $options = array();
+    foreach ($elements as $element) {
+      if (isset($elements['selected'])) {
+        $options[(string) $element['value']] = $this->asText($element);
+      }
+    }
+    return $options;
+  }
+
+  /**
+   * Get all the options contained by a select, including nested options.
+   *
+   * @param $element
+   *   The element to get the options from.
+   * @return
+   *   An array of options contained by the select.
+   */
+  public function getSelectElements(SimpleXMLElement $element) {
+    $options = array();
+
+    // Add all options items.
+    foreach ($element->option as $option) {
+      $options[] = $option;
+    }
+
+    // Search option group children.
+    if (isset($element->optgroup)) {
+      foreach ($element->optgroup as $group) {
+        $options = array_merge($options, $this->getAllOptions($group));
+      }
+    }
+    return $options;
+  }
+
+  public function getLinks($label) {
+    $links = $this->xpath('//a[text()="' . $label . '"]');
+
+    $urls = array();
+    foreach ($links as $link) {
+      $urls[] = $this->getAbsoluteUrl(trim($link['href']));
+    }
+    return $urls;
+  }
+
+  public function getLink($label, $index = 0) {
+    $urls = $this->getLinks($label);
+
+    return isset($urls[$index]) ? $urls[$index] : FALSE;
+  }
+
+  public function getAbsoluteUrl($path) {
+    $parts = @parse_url($path);
+    if (isset($parts['scheme'])) {
+      return $path;
+    }
+
+    $base = $this->getBaseUrl();
+//    $str = "$base : $path";
+
+    if ($path[0] == '/') {
+      // Lead / then use host as base.
+      $parts = parse_url($base);
+      $base = $parts['scheme'] . '://' . $parts['host'];
+    }
+//    $str .= ' : ' . $base . $path;
+//    $str .= ' : ' . ($path[0] == '/' ? 'TRUE' : 'FALSE');
+//    trigger_error($str);
+    return $base . $path;
+  }
+
+  public function getBaseUrl() {
+    // Check for base element.
+    $elements = $this->xpath('.//base');
+    if ($elements) {
+      // More than one may be specified.
+      foreach ($elements as $element) {
+        if (isset($element['href'])) {
+          $base = (string) $element['href'];
+          break;
+        }
+      }
+    }
+    else {
+      $base = substr($this->url, strpos($this->url, '?'));
+    }
+
+    if ($base[strlen($base) - 1] != '/') {
+      $base .= '/';
+    }
+    return $base;
+  }
+
+  /**
+   * Extract the text contained by the element.
+   *
+   * @param $element
+   *   Element to extract text from.
+   * @return
+   *   Extracted text.
+   */
+  public function asText(SimpleXMLElement $element) {
+    return trim(html_entity_decode(strip_tags($element->asXML())));
+  }
+}
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,448 @@
+<?php
+// $Id$
+
+/**
+ * @file
+ * Text web browser for Drupal.
+ */
+
+/*
+ * TODO
+ *
+ * Hooks for get, post events.
+ * HTTP authentication
+ * Decide on parsing which will allow for meta refresh suppose
+ * Deal with Drupal-Assertion callback stuff.
+ * 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"
+ * Error handling.
+ * Deal a from containing one button without a name, even no value?
+ * Page support proxy behavior.
+ *
+ * parse header information, specific url in stream backend
+ * http://us.php.net/manual/en/context.http.php
+ *
+ * Check if backend supports files.
+ * use FALSE as blank state
+ */
+
+class Browser {
+
+  /**
+   * The DrupalBrowser instance.
+   *
+   * @var DrupalBrowser
+   */
+  protected static $browser = array();
+
+  protected $wrapper;
+
+  protected $info;
+
+  /**
+   * The URL of the current page.
+   *
+   * @var string
+   */
+  protected $url;
+
+  protected $request;
+
+  protected $code;
+
+  /**
+   * The headers of the page current page.
+   *
+   * @var Array
+   */
+  protected $headers;
+
+  /**
+   * The content of the page currently loaded in the internal browser.
+   *
+   * @var string
+   */
+  protected $content;
+
+  protected $content_plain;
+
+  protected $page;
+
+  function __construct($wrapper, $info) {
+    // Include the required wrapper and page files.
+    require_once './includes/browser/wrapper.inc';
+    require_once './includes/browser/page.inc';
+    require_once $info['file']; // TODO better way?
+
+    $class = 'HttpWrapper_' . $wrapper;
+    $this->wrapper = new $class();
+    $this->info = $info;
+
+    $this->setUserAgent('Drupal (+http://drupal.org/)');
+  }
+
+  final public static function getInstance($wrapper = 'default') {
+    // Use the default wrapper specified for the site.
+    if ($wrapper == 'default') {
+      $wrapper = variable_get('browser_wrapper', 'stream');
+    }
+
+    // Create a browser instance for the specified wrapper if it does not already exist.
+    if (!isset(self::$browser[$wrapper])) {
+      // Ensure that the specified wrapper is valid, if not use the default.
+      $wrappers = module_invoke_all('browser_wrapper');
+      if (!array_key_exists($wrapper, $wrappers)) {
+        $wrapper = variable_get('browser_wrapper', 'stream');
+      }
+
+      self::$browser[$wrapper] = new Browser($wrapper, $wrappers[$wrapper]);
+    }
+    return self::$browser[$wrapper];
+  }
+
+  public function getUserAgent() {
+    $headers = $this->getRequestHeaders();
+    return $headers['User-Agent'];
+  }
+
+  public function setUserAgent($agent) {
+    $headers = $this->getRequestHeaders();
+    $headers['User-Agent'] = $agent;
+    $this->wrapper->setRequestHeaders($headers);
+  }
+
+  public function getRequestHeaders() {
+    return $this->wrapper->getRequestHeaders();
+  }
+
+  public function setRequestHeaders(array $headers) {
+    $this->wrapper->setRequestHeaders($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')) {
+      return FALSE;
+    }
+
+    $request = $this->wrapper->get($url);
+
+    // TODO Error check.
+    return $this->setState($request);
+  }
+
+  public function post($url, array $fields, $submit) {
+    if (!$this->isMethodSupported('POST')) {
+      return FALSE;
+    }
+
+    // If URL is set then request the page, otherwise use the current page.
+    if ($url) {
+//      trigger_error('post(): 0:1');
+      $this->get($url);
+    }
+    else {
+//      trigger_error('post(): 0:2');
+      $url = $this->url;
+    }
+
+//    trigger_error('post(): 1');
+
+    if (($page = $this->getPage()) === FALSE) {
+      return FALSE;
+    }
+
+//    trigger_error('post(): 2');
+
+    if (($form = $this->findForm($fields, $submit)) === FALSE) {
+//      file_put_contents('output.html', $this->getContent());
+      return FALSE;
+    }
+//    trigger_error('post(): 3');
+
+    // If form specified action then use that for the post url.
+    if ($form['action']) {
+      $url = $page->getAbsoluteUrl($form['action']);
+    }
+//    file_put_contents('output.html', print_r($form['post'], TRUE));
+
+    $request = $this->wrapper->post($url, $form['post']);
+
+//    trigger_error('post(): 4');
+
+    // TODO Error check.
+    return $this->setState($request);
+  }
+
+  protected function findForm(array $fields, $submit) {
+    $page = $this->getPage();
+
+    $forms = $page->getForms();
+    foreach ($forms as $form) {
+      if (($post = $this->processForm($form, $fields, $submit)) !== FALSE) {
+        $action = (isset($form['action']) ? (string) $form['action'] : FALSE);
+        return array(
+          'action' => $action,
+          'post' => $post,
+        );
+      }
+    }
+    return FALSE;
+  }
+
+  protected function processForm($form, $fields, $submit) {
+    $page = $this->getPage();
+
+    $post = array();
+    $submit_found = FALSE;
+    $inputs = $page->getInputs($form);
+    foreach ($inputs as $input) {
+      $name = (string) $input['name'];
+      $html_value = isset($input['value']) ? (string) $input['value'] : '';
+
+      // Get type from input vs textarea and select.
+      $type = isset($input['type']) ? (string) $input['type'] : $input->getName();
+
+      if (isset($fields[$name])) {
+        if ($type == 'file') {
+          // Make sure the file path is the absolute path.
+          $file = realpath($fields[$name]);
+          if ($file && is_file($file)) {
+            // Signify that the post field is a file in case backend needs to
+            // perform additional processing.
+            $post[$name] = '@' . $file;
+          }
+        }
+        elseif (($processed_value = $this->processField($input, $type, $fields[$name], $html_value)) !== NULL) {
+          // Value may be ommitted (checkbox).
+          if ($processed_value !== FALSE) {
+            if (is_array($processed_value)) {
+              $post += $processed_value;
+            }
+            else {
+              $post[$name] = $processed_value;
+            }
+          }
+        }
+      }
+
+      // No post value for the field means that: no post field value specified,
+      // the value does not match the field (checkbox, radio, select), or the
+      // field is of an unknown type.
+      if (!isset($post[$name])) {
+        // No value specified so use default value (value in HTML).
+        if (($default_value = $this->getDefaultFieldValue($input, $type, $html_value)) !== NULL) {
+          $post[$name] = $default_value;
+        }
+      }
+
+      // Check if the
+      if (($type == 'submit' || $type == 'image') && $submit == $html_value) {
+        $post[$name] = $html_value;
+        $submit_found = TRUE;
+      }
+    }
+
+    if ($submit_found) {
+      return $post;
+    }
+    return FALSE;
+  }
+
+  protected function processField($input, $type, $new_value, $html_value) {
+    $page = $this->getPage();
+
+    switch ($type) {
+      case 'text':
+      case 'textarea':
+      case 'password':
+        return $new_value;
+      case 'radio':
+        if ($new_value == $html_value) {
+          return $new_value;
+        }
+        return NULL;
+      case 'checkbox':
+        // If $new_value is set to FALSE then ommit checkbox value, otherwise
+        // pass original value.
+        if ($new_value === FALSE) {
+          return FALSE;
+        }
+        return $html_value;
+      case 'select':
+        // Remove the ending [] from multi-select element name.
+        $key = preg_replace('/\[\]$/', '', (string) $input['name']);
+
+        $options = $page->getSelectOptions($input);
+        $index = 0;
+        $out = array();
+        foreach ($options as $value => $text) {
+          if (is_array($value)) {
+            if (in_array($value, $new_value)) {
+              $out[$key . '[' . $index++ . ']'] = $value;
+            }
+          }
+          elseif ($new_value == $value) {
+            return $new_value;
+          }
+        }
+        return ($out ? $out : NULL);
+      default:
+        return NULL;
+    }
+  }
+
+  protected function getDefaultFieldValue($input, $type, $html_value) {
+    $page = $this->getPage();
+
+    switch ($type) {
+      case 'textarea':
+        return (string) $input;
+      case 'select':
+        // Remove the ending [] from multi-select element name.
+        $key = preg_replace('/\[\]$/', '', (string) $input['name']);
+        $single = empty($input['multiple']);
+
+        $options = $page->getSelectElements($input);
+        $first = TRUE;
+        $index = 0;
+        $out = array();
+        foreach ($options as $option) {
+          // For single select, we load the first option, if there is a
+          // selected option that will overwrite it later.
+          if ($option['selected'] || ($first && $single)) {
+            $first = FALSE;
+            if ($single) {
+              $out[$key] = (string) $option['value'];
+            }
+            else {
+              $out[$key . '[' . $index++ . ']'] = (string) $option['value'];
+            }
+          }
+          return ($single ? $out[$key] : $out);
+        }
+        break;
+      case 'file':
+        return NULL;
+      case 'submit':
+      case 'button':
+      case 'image':
+        // Dealt with later during submit check, but button should always be ignored.
+        return NULL;
+      case 'radio':
+      case 'checkbox':
+        if (!isset($input['checked'])) {
+          return NULL;
+        }
+        // Deliberately no break.
+      default:
+        return $html_value;
+    }
+  }
+
+  public function request($method) {
+    if (!$this->isMethodSupported($method)) { // TODO or should it be 'REQUEST'
+      return FALSE;
+    }
+
+    // TODO Support abitrary method.
+  }
+
+  public function isMethodSupported($method) {
+    return in_array(strtoupper($method), $this->info['methods']);
+  }
+
+  protected function setState(array $request) {
+    // TODO use FALSE as blank state
+    $this->request = $request;
+    unset($this->content_plain);
+    unset($this->page);
+
+    if ($new_request = $this->checkForMetaRefresh()) {
+      return $new_request;
+    }
+
+//    module_invoke_all('browser_request', self::$browser); // TODO decide on hooks
+
+    return $request;
+  }
+
+  /**
+   * Check for meta refresh tag and if found make another request recursively.
+   * This function looks for the http-equiv attribute to be set to "Refresh"
+   * and is case-sensitive.
+   *
+   * @return
+   *   Either the new request information or FALSE.
+   */
+  protected function checkForMetaRefresh() {
+    if ($page = $this->getPage()) {
+      $refresh = $page->xpath('//meta[@http-equiv="Refresh"]');
+      if (!empty($refresh)) {
+        // Parse the content attribute of the meta tag for the format:
+        // "[delay]: URL=[page_to_redirect_to]".
+        if (preg_match('/\d+;\s*URL=(?P<url>.*)/i', $refresh[0]['content'], $match)) {
+          return $this->get($page->getAbsoluteUrl(decode_entities($match['url'])));
+        }
+      }
+    }
+    return FALSE;
+  }
+
+  public function getUrl() {
+    return $this->request['url'];
+  }
+
+  public function getCode() {
+    return $this->request['code'];
+  }
+
+  public function getHeaders() {
+    return $this->request['headers'];
+  }
+
+  /**
+   * Gets the current raw HTML of the last requested page.
+   *
+   * @return
+   *   Raw HTML of last requested page.
+   */
+  public function getContent() {
+    return $this->request['content'];
+  }
+
+  public function getContentPlain() {
+    if (!isset($this->content_plain)) {
+      $this->content_plain = filter_xss($this->getContent(), array());
+    }
+    return $this->content_plain;
+  }
+
+  public function getPage() {
+    if (!isset($this->page)) {
+      $this->page = new BrowserPage($this->getUrl(), $this->getHeaders(), $this->getContent());
+      if (!$this->page->isValid()) {
+        $this->page = FALSE;
+        return FALSE;
+      }
+    }
+    return $this->page;
+  }
+
+  public function resetConnection() {
+    $this->wrapper->close();
+    $this->wrapper->open();
+  }
+
+  function __destruct() {
+    $this->wrapper->close();
+  }
+}
