Index: modules/simpletest/simpletest.info =================================================================== RCS file: /cvs/drupal/drupal/modules/simpletest/simpletest.info,v retrieving revision 1.7 diff -u -r1.7 simpletest.info --- modules/simpletest/simpletest.info 1 Jul 2009 13:44:53 -0000 1.7 +++ modules/simpletest/simpletest.info 15 Jul 2009 22:08:10 -0000 @@ -14,6 +14,7 @@ files[] = tests/actions.test files[] = tests/batch.test files[] = tests/bootstrap.test +files[] = tests/browser.test files[] = tests/cache.test files[] = tests/common.test files[] = tests/database_test.test Index: includes/common.inc =================================================================== RCS file: /cvs/drupal/drupal/includes/common.inc,v retrieving revision 1.934 diff -u -r1.934 common.inc --- includes/common.inc 14 Jul 2009 10:22:15 -0000 1.934 +++ includes/common.inc 15 Jul 2009 22:08:10 -0000 @@ -680,6 +680,16 @@ return $result; } + +/** + * Load the browser. + * + * See browser.inc for details. + */ +function browser_load() { + require_once DRUPAL_ROOT . '/includes/browser.inc'; +} + /** * @} End of "HTTP handling". */ 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,139 @@ + 'Browser', + 'description' => 'Test general browser functionality.', + 'group' => 'Browser', + ); + } + + public function setUp() { + parent::setUp('browser_test'); + + browser_load(); + } + + /** + * Test general browser functionality. + */ + public function testBrowserBackend() { + global $db_prefix; + + browser_user_agent_set($db_prefix); + + // Check browser refresh, both meta tag and HTTP header. + $request = browser_get(url('browser_test/refresh/meta', array('absolute' => TRUE))); + $this->assertEqual($request['content'], 'Refresh successful', 'Meta refresh successful ($request)'); + $this->assertEqual(browser_content_get(), 'Refresh successful', 'Meta refresh successful ($browser)'); + + $request = browser_get(url('browser_test/refresh/header', array('absolute' => TRUE))); + $this->assertEqual($request['content'], 'Refresh successful', 'Meta refresh successful ($request)'); + $this->assertEqual(browser_content_get(), 'Refresh successful', 'Meta refresh successful ($browser)'); + } +} + +/** + * Test browser backend wrappers. + */ +class BrowserBackendTestCase extends DrupalWebTestCase { + + public static function getInfo() { + return array( + 'name' => 'Browser - wrapper backends', + 'description' => 'Test stream and curl backends execution of GET and POST requests.', + 'group' => 'Browser', + ); + } + + public function setUp() { + parent::setUp('browser_test'); + + browser_load(); + } + + /** + * Test stream and curl backends execution of GET and POST requests. + */ + public function testBrowserBackend() { + global $db_prefix; + + foreach (array('stream', 'curl') as $wrapper) { + browser_user_agent_set($db_prefix); + + // Override backend for purposes of test. + $curl = &drupal_static('browser_curl', FALSE); + $curl = $wrapper == 'curl'; + + $handle = &drupal_static('browser_handle', NULL); + if ($wrapper == 'stream') { + $handle = stream_context_create(); + } + else { + $handle = curl_init(); + curl_setopt_array($handle, _browser_curl_options()); + } + + $string = $this->randomName(); + $edit = array( + 'foo' => $string, + ); + + // Test GET method. + $request = browser_get(url('browser_test/print/get', array('absolute' => TRUE, 'query' => $edit))); + $this->assertEqual($string, $request['content'], 'String found during GET request ($request)', $wrapper); + $this->assertEqual($string, browser_content_get(), 'String found during GET request ($browser)', $wrapper); + + // Test POST method. + $request = browser_post(url('browser_test/print/post', array('absolute' => TRUE)), $edit, t('Submit')); + $this->assertEqual($string, $request['content'], 'String found during POST request ($request)', $wrapper); + $this->assertEqual($string, browser_content_get(), 'String found during POST request ($browser)', $wrapper); + } + } +} + +/** + * Test browser page manipulation functionality. + */ +class BrowserPageTestCase extends DrupalWebTestCase { + + public static function getInfo() { + return array( + 'name' => 'Browser - page', + 'description' => 'Check "BrowserPage" class functionality.', + 'group' => 'Browser', + ); + } + + public function setUp() { + parent::setUp('browser_test'); + + browser_load(); + } + + /** + * Check "BrowserPage" class functionality. + */ + public function testBrowserPage() { + global $db_prefix; + + browser_user_agent_set($db_prefix); + + browser_get(url('browser_test/print/post', array('absolute' => TRUE))); + $page = browser_page_get(); + $input = $page->xpath('//input[@name="foo"]'); + $input = $input[0]; + $this->assertEqual('foo', $input['name'], t('Field "foo" found')); + } +} 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,1059 @@ +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 + */ + +/** + * Initialize the browser. + * + * Setup the default state information, detect curl support, and initialize the + * appropriate handler. + * + * @return + * The browser state. + */ +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, + ); + + // 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; +} + +/** + * Reset the browser to its initial 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'], + ); +} + +/** + * Get the current browser state. + * + * @return + * - An associative array containing the browser state information. The + * following key-value pairs are available. + * - 'request_headers': An associative array of headers sent during + * outgoing requests. + * - 'cookie_file': The location of the cookie file used by curl. + * - 'url': The URL of the page from the last request. + * - 'headers': An associative array of headers returned from the last + * request. + * - 'content': The raw content of the last request. + */ +function browser_state_get() { + return browser_init(); +} + +/** + * Set the state of the browser. + * + * @param $state + * An associative array containing state information. See browser_state_get() + * for details on the structure of the array. + * @see browser_state_get + */ +function browser_state_set($state) { + $state_old = &browser_init(); + $state_old = $state; +} + +/** + * Get the value of a request header. + * + * @param $name + * Name of the request header to retrieve a value of. + * @return + * Value of the request header, or FALSE. + */ +function browser_request_header_get($name) { + $name = browser_header_name($name); + $state = &browser_init(); + return isset($state[$name]) ? $state[$name] : FALSE; +} + +/** + * Set a request header. + * + * @param $name + * Name of the request header to set. + * @param $value + * Value of the request header. + */ +function browser_request_header_set($name, $value) { + $name = browser_header_name($name); + $state = &browser_init(); + $state['request_headers'][$name] = $value; +} + +/** + * Get the user-agent that the browser is identifying itself as. + * + * @return + * Browser user-agent. + */ +function browser_user_agent_get() { + return browser_request_header_get('User-Agent'); +} + +/** + * Set the user-agent that the browser will identify itself as. + * + * @param $agent + * User-agent to to identify as. + */ +function browser_user_agent_set($agent) { + browser_request_header_set('User-Agent', $agent); +} + +/** + * Get the URL of the current page. + * + * @return + * The URL of the current page. + */ +function browser_url_get() { + $state = browser_state_get(); + return $state['url']; +} + +/** + * Get the response headers of the current page. + * + * @return + * The response headers of the current page. + */ +function browser_headers_get() { + $state = browser_state_get(); + return $state['headers']; +} + +/** + * Get the raw content of the current page. + * + * @return + * The raw content for the current page. + */ +function browser_content_get() { + $state = browser_state_get(); + return $state['content']; +} + +/** + * Get the BrowserPage instance for the current page. + * + * If the raw content is new and the page has not yet been parsed then parse + * the content and ensure that it is valid. + * + * @return + * BrowserPage instance for the current page. + */ +function browser_page_get() { + $state = &browser_init(); + + if (!isset($state['page'])) { + $state['page'] = new BrowserPage($state['url'], $state['headers'], $state['content']); + if (!$state['page']->isValid()) { + return FALSE; + } + } + return $state['page']; +} + +/** + * 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', + ), + )); + } + + _browser_refresh_check(); + + return browser_state_get(); +} + +/** + * Peform a POST request. + * + * @param $url + * Absolute URL to request. + * @param $fields + * Associative array of fields to submit as POST variables. + * @param $submit + * Text contained in 'value' properly of submit button of which to press. + * @return + * Associative array of state information, as returned by + * browser_state_get(). + * @see browser_state_get() + */ +function browser_post($url, array $fields, $submit) { + $state = &browser_init(); + + // If URL is set then request the page, otherwise use the current page. + if ($url) { + browser_get($url); + } + else { + $url = $state['url']; + } + + if (($page = browser_page_get()) === FALSE) { + return FALSE; + } + + if (($form = _browser_form_find($fields, $submit)) === FALSE) { + return FALSE; + } + + // If form specified action then use that for the post url. + if ($form['action']) { + $url = $page->getAbsoluteUrl($form['action']); + } + + if (drupal_static('browser_curl', FALSE)) { + _browser_execute_curl(array( + CURLOPT_POST => TRUE, + CURLOPT_URL => $url, + CURLOPT_POSTFIELDS => http_build_query($form['post'], NULL, '&'), + )); + } + else { + _browser_execute_stream($url, array( + 'method' => 'POST', + 'header' => array( + 'Content-Type' => 'application/x-www-form-urlencoded', + ), + 'content' => http_build_query($form['post'], NULL, '&'), + )); + } + + _browser_refresh_check(); + + return browser_state_get(); +} + +/** + * Find the the form that patches the conditions. + * + * @param $fields + * Associative array of fields to submit as POST variables. + * @param $submit + * Text contained in 'value' properly of submit button of which to press. + * @return + * Form action and the complete post array containing default values if not + * overridden, or FALSE if no form matching the conditions was found. + */ +function _browser_form_find(array $fields, $submit) { + $page = browser_page_get(); + + $forms = $page->getForms(); + foreach ($forms as $form) { + if (($post = _browser_form_process($form, $fields, $submit)) !== FALSE) { + $action = (isset($form['action']) ? (string) $form['action'] : FALSE); + return array( + 'action' => $action, + 'post' => $post, + ); + } + } + return FALSE; +} + +/** + * Check the conditions against the specified form and process values. + * + * @param $form + * Form SimpleXMLElement object. + * @param $fields + * Associative array of fields to submit as POST variables. + * @param $submit + * Text contained in 'value' properly of submit button of which to press. + * @return + * The complete post array containing default values if not overridden, or + * FALSE if no form matching the conditions was found. + */ +function _browser_form_process($form, $fields, $submit) { + $page = browser_page_get(); + + $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; + } + // Known type, field processed. + unset($fields[$name]); + } + elseif (($processed_value = _browser_field_process($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; + } + } + // Known type, field processed. + unset($fields[$name]); + } + } + + // 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 = _browser_default_field_value($input, $type, $html_value)) !== NULL) { + $post[$name] = $default_value; + unset($fields[$name]); + } + } + + // 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; +} + +/** + * Get the value to be sent for the specified field. + * + * @param $input + * Input SimpleXMLElement object. + * @param $type + * Input type: text, textarea, password, radio, checkbox, or select. + * @param $new_value + * The new value to be assigned to the input. + * @param $html_value + * The cleaned default value for the input from the HTML value. + */ +function _browser_field_process($input, $type, $new_value, $html_value) { + 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; + } +} + +/** + * Get the cleaned default value for the input from the HTML value. + * + * @param $input + * Input SimpleXMLElement object. + * @param $type + * Input type: text, textarea, password, radio, checkbox, or select. + * @param $html_value + * The default value for the input, as specified in the HTML. + */ +function _browser_default_field_value($input, $type, $html_value) { + 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 'radio': + case 'checkbox': + if (!isset($input['checked'])) { + return NULL; + } + // Deliberately no break. + default: + return $html_value; + } +} + +/** + * 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) { + file_put_contents('output.html', "_browser_execute_curl(" . print_r($options, TRUE) . ")\n", FILE_APPEND); + $state = &drupal_static('browser', array()); + $handle = &drupal_static('browser_handle', NULL); + + // Headers need to be reset since callback appends. + $state['headers'] = array(); + + // Ensure that request headers are up to date. + curl_setopt($handle, CURLOPT_USERAGENT, $state['request_headers']['User-Agent']); + curl_setopt($handle, CURLOPT_HTTPHEADER, $state['request_headers']); + + 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(). + unset($state['page']); +} + +/** + * 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) { + file_put_contents('output.html', "_browser_execute_stream($url, " . print_r($options, TRUE) . ")\n", FILE_APPEND); + $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['header'])) { + $options['header'] = array(); + } + + // Merge default request headers with the passed headers and generate + // header string to be sent in http request. + $headers = $state['request_headers'] + $options['header']; + $options['header'] = _browser_header_string($headers); + file_put_contents('output.html', $options['header'] . "\n", FILE_APPEND); + + // 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_all($http_response_header); + unset($state['page']); +} + +/** + * Check for a refresh signifier. + * + * A refresh signifier can either be the 'Location' HTTP header or the meta + * tag 'http-equiv="Refresh"'. + */ +function _browser_refresh_check() { + $state = browser_state_get(); + + // If not handled by backend wrapper then go ahead and handle. + if (isset($state['headers']['Location'])) { + // Expect absolute URL. + browser_get($state['headers']['Location']); + } + + if (($page = browser_page_get()) !== FALSE && ($tag = $page->getMetaTag('Refresh', 'http-equiv'))) { + // Parse the content attribute of the meta tag for the format: + // "[delay]: URL=[path_to_redirect_to]". + if (preg_match('/\d+;\s*URL=(?P.*)/i', $tag['content'], $match)) { + browser_get($page->getAbsoluteUrl(decode_entities($match['url']))); + } + } +} + +/** + * 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) { + $out = array(); + foreach ($headers as $header) { + $out += _browser_header_parse($header); + } + return $out; +} + +/** + * 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); +} + + +/** + * 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 page. + * + * @var string + */ + protected $url; + + /** + * The response headers of the page. + * + * @var Array + */ + protected $headers; + + /** + * The root element of the page. + * + * @var SimpleXMLElement + */ + protected $root; + + /** + * Initialize the BrowserPage with the page state information. + * + * @param $url + * The URL of the page. + * @param $headers + * The response headers of the page. + * @param $content + * The raw content of the page. + */ + public function BrowserPage($url, $headers, $content) { + $this->url = $url; + $this->headers = $headers; + $this->root = $this->load($content); + } + + /** + * Attempt to parse the raw content using DOM and import it into SimpleXML. + * + * @param $content + * The raw content of the page. + * @return + * The root element of the page, or FALSE. + */ + protected function load($content) { + // Use DOM to load HTML soup, and hide warnings. + $document = @DOMDocument::loadHTML($content); + if ($document) { + return simplexml_import_dom($document); + } + return FALSE; + } + + /** + * Check if the raw content is valid and could be parse. + * + * @return + * TRUE if content is valid, otherwise FALSE. + */ + 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, usually the HTML tag, of the + * page. To perform a search using a different root element follow the + * example below. + * @code + * $parent = $page->xpath('.//parent'); + * $parent[0]->xpath('//children'); + * @endcode + * + * @param $xpath + * The xpath string. + * @return + * An array of SimpleXMLElement objects or FALSE in case of an error. + * @link http://us.php.net/manual/function.simplexml-element-xpath.php + */ + public function xpath($xpath) { + if ($this->isValid()) { + return $this->root->xpath($xpath); + } + return FALSE; + } + + /** + * Get all the meta tags. + * + * @return + * An array of SimpleXMLElement objects representing meta tags. + */ + public function getMetaTags() { + return $this->xpath('//meta'); + } + + /** + * Get a specific meta tag. + * + * @param $key + * The meta tag key. + * @param $type + * The type of meta tag, either: 'name' or 'http-equiv'. + * @return + * A SimpleXMLElement object representing the meta tag, or FALSE if not + * found. + */ + public function getMetaTag($key, $type = 'name') { + foreach ($this->getMetaTags() as $tag) { + if ($tag[$type] == $key) { + return $tag; + } + } + return FALSE; + } + + /** + * Get all the form elements. + * + * @return + * An array of SimpleXMLElement objects representing form elements. + */ + public function getForms() { + return $this->xpath('//form'); + } + + /** + * Get all the input elements, or only those nested within a parent element. + * + * @param $parent + * SimpleXMLElement representing the parent to search within. + * @return + * An array of SimpleXMLElement objects representing form elements. + */ + public function getInputs($parent = NULL) { + if ($parent) { + return $parent->xpath('.//input|.//textarea|.//select'); + } + return $this->xpath('.//input|.//textarea|.//select'); + } + + /** + * Get all the options contained by a select, including nested options. + * + * @param $select + * SimpleXMLElement representing the select to extract option 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 = getSelectElements($select); + + $options = array(); + foreach ($elements as $element) { + $options[(string) $element['value']] = asText($element); + } + return $options; + } + + /** + * Get all selected options contained by a select, including nested options. + * + * @param $select + * SimpleXMLElement representing the select to extract option from. + * @return + * Associative array of selected items in the format described by + * BrowserPage->getSelectOptions(). + * @see BrowserPage->getSelectOptions() + */ + public function getSelectedOptions(SimpleXMLElement $select) { + $elements = getSelectElements($select); + + $options = array(); + foreach ($elements as $element) { + if (isset($elements['selected'])) { + $options[(string) $element['value']] = asText($element); + } + } + return $options; + } + + /** + * Get all the options contained by a select, including nested options. + * + * @param $element + * SimpleXMLElement representing the select to extract option from. + * @return + * An array of SimpleXMLElement objects representing option elements. + */ + 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; + } + + /** + * Get the absolute URL for a given path, relative to the page. + * + * @param + * A path relative to the page or absolute. + * @return + * An absolute path. + */ + public function getAbsoluteUrl($path) { + $parts = @parse_url($path); + if (isset($parts['scheme'])) { + return $path; + } + + $base = $this->getBaseUrl(); + if ($path[0] == '/') { + // Lead / then use host as base. + $parts = parse_url($base); + $base = $parts['scheme'] . '://' . $parts['host']; + } + return $base . $path; + } + + /** + * Get the base URL of the page. + * + * If a 'base' HTML element is defined then the URL it defines is used as the + * base URL for the page, otherwise the page URL is used to determine the + * base URL. + * + * @return + * The base URL of the page. + */ + 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 = $this->url; + if ($pos = strpos($base, '?')) { + // Remove query string. + $base = substr($base, 0, $pos); + } + + // Ignore everything after the last forward slash. + $base = substr($base, 0, strrpos($base, '/')); + } + + // Ensure that the last character is a forward slash. + if ($base[strlen($base) - 1] != '/') { + $base .= '/'; + } + return $base; + } + + /** + * Extract the text contained by the element. + * + * Strips all XML/HTML tags, decodes HTML entities, and trims the result. + * + * @param $element + * SimpleXMLElement to extract text from. + * @return + * Extracted text. + */ + public function asText(SimpleXMLElement $element) { + return trim(html_entity_decode(strip_tags($element->asXML()))); + } +} + +/** + * @} End of "defgroup browser". + */ Index: modules/simpletest/tests/browser_test.info =================================================================== RCS file: modules/simpletest/tests/browser_test.info diff -N modules/simpletest/tests/browser_test.info --- /dev/null 1 Jan 1970 00:00:00 -0000 +++ modules/simpletest/tests/browser_test.info 1 Jan 1970 00:00:00 -0000 @@ -0,0 +1,8 @@ +; $Id$ +name = Browser test +description = Provide various pages for testing the browser. +package = Testing +version = VERSION +core = 7.x +files[] = browser_test.module +hidden = TRUE Index: modules/simpletest/tests/browser_test.module =================================================================== RCS file: modules/simpletest/tests/browser_test.module diff -N modules/simpletest/tests/browser_test.module --- /dev/null 1 Jan 1970 00:00:00 -0000 +++ modules/simpletest/tests/browser_test.module 1 Jan 1970 00:00:00 -0000 @@ -0,0 +1,79 @@ + 'browser_test_print_get', + 'access arguments' => array('access content'), + ); + $items['browser_test/print/post'] = array( + 'page callback' => 'drupal_get_form', + 'page arguments' => array('browser_test_print_post_form'), + 'access arguments' => array('access content'), + ); + + $items['browser_test/refresh/meta'] = array( + 'page callback' => 'browser_test_refresh_meta', + 'access arguments' => array('access content'), + ); + $items['browser_test/refresh/header'] = array( + 'page callback' => 'browser_test_refresh_header', + 'access arguments' => array('access content'), + ); + + return $items; +} + +function browser_test_print_get() { + echo $_GET['foo']; + exit; +} + +function browser_test_print_post_form(&$form_state) { + $form = array(); + + $form['foo'] = array( + '#type' => 'textfield', + ); + $form['op'] = array( + '#type' => 'submit', + '#value' => t('Submit'), + ); + + return $form; +} + +function browser_test_print_post_form_submit($form, &$form_state) { + echo $form_state['values']['foo']; + exit; +} + +function browser_test_refresh_meta() { + if (!isset($_GET['refresh'])) { + $url = url('browser_test/refresh/meta', array('absolute' => TRUE, 'query' => 'refresh=true')); + drupal_add_html_head(''); + return ''; + } + echo 'Refresh successful'; + exit; +} + +function browser_test_refresh_header() { + if (!isset($_GET['refresh'])) { + $url = url('browser_test/refresh/header', array('absolute' => TRUE, 'query' => 'refresh=true')); + drupal_set_header('Location', $url); + return ''; + } + echo 'Refresh successful'; + exit; +}