? .DS_Store ? abstract-browser-into-its-own-class.patch ? test.txt ? modules/.DS_Store ? profiles/.DS_Store ? profiles/single_user_blog ? sites/.DS_Store ? sites/default/files ? sites/default/settings.php ? themes/garland/.DS_Store Index: CHANGELOG.txt =================================================================== RCS file: /cvs/drupal/drupal/CHANGELOG.txt,v retrieving revision 1.289 diff -u -p -r1.289 CHANGELOG.txt --- CHANGELOG.txt 25 Nov 2008 02:37:31 -0000 1.289 +++ CHANGELOG.txt 28 Nov 2008 17:28:31 -0000 @@ -79,6 +79,10 @@ Drupal 7.0, xxxx-xx-xx (development vers preserved but renamed to file_unmanaged_*(). - Added aliased multi-site support: * Added support for mapping domain names to sites directories. +- Added a code-based browser: + * The code-based browser can trigger both GET and POST requests on any website, + and is based on cURL. + * The browser is used by the Testing framework. Drupal 6.0, 2008-02-13 ---------------------- 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 28 Nov 2008 17:28:31 -0000 @@ -0,0 +1,566 @@ +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 (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; + } + curl_setopt_array($this->curlHandle, $this->additionalCurlOptions + $curl_options); + } + } + + /** + * 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(); + $this->elements = NULL; + $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->content = curl_exec($this->curlHandle); + $this->url = curl_getinfo($this->curlHandle, CURLINFO_EFFECTIVE_URL); + $this->debug($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->content; + } + + /** + * 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) { + // 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]))); + } + // This is required by cURL. + return strlen($header); + } + + /** + * Close the cURL handler and unset the handler. + */ + public function curlClose() { + if (isset($this->curlHandle)) { + curl_close($this->curlHandle); + unset($this->curlHandle); + } + } + + /** + * Parse content returned from curlExec using DOM and SimpleXML. + * + * @return + * A SimpleXMLElement or FALSE on failure. + */ + public function parseHTMLcontent() { + if (!$this->elements) { + // DOM can load HTML soup. But, HTML soup can throw warnings, supress + // them. + @$htmlDom = DOMDocument::loadHTML($this->content); + if ($htmlDom) { + $this->debug(TRUE, t('Valid HTML found on "@path"', array('@path' => $this->url)), 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); + } + } + if (!$this->elements) { + $this->debug(FALSE, t('Parsed page successfully.'), t('Browser')); + } + + return $this->elements; + } + + /** + * Retrieves a Drupal path or an absolute path. + * + * @param $path + * Drupal path or URL to load into internal browser + * @param $options + * Options to be forwarded to url(). + * @return + * The retrieved HTML string, also available as $this->drupalGetContent() + */ + public function get($path, $options = 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_HEADER => FALSE, CURLOPT_NOBODY => FALSE)); + $this->refreshVariables(); // Ensure that any changes to variables in the other thread are picked up. + + // Replace original page output with new output from redirected page(s). + if (($new = $this->checkForMetaRefresh())) { + $out = $new; + } + return $out; + } + + /** + * Execute a POST request on a Drupal page. + * It will be done as usual POST request with SimpleBrowser. + * + * @param $path + * Location of the post form. Either a Drupal path or an absolute path or + * NULL to post to the current page. For multi-stage forms you can set the + * path to NULL and have it post to the last received page. Example: + * + * // First step in form. + * $edit = array(...); + * $this->drupalPost('some_url', $edit, t('Save')); + * + * // Second step in form. + * $edit = array(...); + * $this->drupalPost(NULL, $edit, t('Save')); + * @param $edit + * Field data in an assocative array. Changes the current input fields + * (where possible) to the values indicated. A checkbox can be set to + * TRUE to be checked and FALSE to be unchecked. Note that when a form + * contains file upload fields, other fields cannot start with the '@' + * character. + * + * Multiple select fields can be set using name[] and setting each of the + * possible values. Example: + * $edit = array(); + * $edit['name[]'] = array('value1', 'value2'); + * @param $submit + * Value of the submit button. + * @param $options + * Options to be forwarded to url(). + */ + public function post($path, $edit, $submit, $options = array()) { + $submit_matches = FALSE; + if (isset($path)) { + $html = $this->get($path, $options); + } + if ($this->parseHTMLcontent()) { + $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->url; + + // 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 { + file_put_contents('test.txt', print_r($post, 1), FILE_APPEND); + 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_HEADER => FALSE)); + // 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->debug(FALSE, t('Failed to set field @name to @value', array('@name' => $name, '@value' => $value))); + } + $this->debug($submit_matches, t('Found the @submit button', array('@submit' => $submit))); + $this->debug(FALSE, t('Found the requested form fields at @path', array('@path' => $path))); + } + } + + /** + * 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->content != '' && $this->parseHTMLcontent()) { + $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.*)/i', $refresh[0]['content'], $match)) { + return $this->get($this->getAbsoluteUrl(decode_entities($match['url']))); + } + } + } + return FALSE; + } + + /** + * Retrieves only the headers for a Drupal path or an absolute path. + * + * @param $path + * Drupal path or URL to load into internal browser + * @param $options + * Options to be forwarded to url(). + * @return + * The retrieved headers, also available as $this->drupalGetContent() + */ + public function head($path, Array $options = array()) { + $options['absolute'] = TRUE; + $out = $this->curlExec(array(CURLOPT_HEADER => TRUE, CURLOPT_NOBODY => TRUE, CURLOPT_URL => url($path, $options))); + $this->refreshVariables(); // Ensure that any changes to variables in the other thread are picked up. + return $out; + } + + /** + * 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 or the name of the tag itself + // for