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 7 Jul 2009 00:39:28 -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: modules/system/system.module =================================================================== RCS file: /cvs/drupal/drupal/modules/system/system.module,v retrieving revision 1.722 diff -u -r1.722 system.module --- modules/system/system.module 5 Jul 2009 18:00:10 -0000 1.722 +++ modules/system/system.module 7 Jul 2009 00:39:29 -0000 @@ -248,6 +248,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'), + ), + ); +} + +/** * Implement hook_elements(). */ function system_elements() { @@ -3025,17 +3045,17 @@ } $parsed_url = parse_url($url); $local = is_dir(file_directory_path() . '/' . $destination) ? $destination . '/' . basename($parsed_url['path']) : $destination; - + if (!$overwrite && file_exists($local)) { drupal_set_message(t('@remote could not be saved. @local already exists', array('@remote' => $url, '@local' => $local)), 'error'); return FALSE; } - + $result = drupal_http_request($url); if ($result->code != 200 || !file_save_data($result->data, $local)) { drupal_set_message(t('@remote could not be saved.', array('@remote' => $url)), 'error'); return FALSE; } - + return $local; } 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,123 @@ + t('Browser'), + 'description' => t('Test general browser functionality.'), + 'group' => t('Browser'), + ); + } + + public function setUp() { + parent::setUp('browser_test'); + } + + /** + * Test general browser functionality. + */ + public function testBrowserBackend() { + global $db_prefix; + + $browser = Browser::getInstance(); + $browser->setUserAgent($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->getContent(), '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->getContent(), 'Refresh successful', 'Meta refresh successful ($browser)'); + } +} + +/** + * Test browser backend wrappers. + */ +class BrowserBackendTestCase extends DrupalWebTestCase { + + public static function getInfo() { + return array( + 'name' => t('Browser - wrapper backends'), + 'description' => t('Test stream and curl backends execution of GET and POST requests.'), + 'group' => t('Browser'), + ); + } + + public function setUp() { + parent::setUp('browser_test'); + } + + /** + * Test stream and curl backends execution of GET and POST requests. + */ + public function testBrowserBackend() { + global $db_prefix; + + foreach (array('stream', 'curl') as $wrapper) { + $browser = Browser::getInstance($wrapper); + $browser->setUserAgent($db_prefix); + + $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'], t('String found during GET request ($request)'), $wrapper); + $this->assertEqual($string, $browser->getContent(), t('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'], t('String found during POST request ($request)'), $wrapper); + $this->assertEqual($string, $browser->getContent(), t('String found during POST request ($browser)'), $wrapper); + } + } +} + +/** + * Test browser page manipulation functionality. + */ +class BrowserPageTestCase extends DrupalWebTestCase { + + public static function getInfo() { + return array( + 'name' => t('Browser - page'), + 'description' => t('Check "BrowserPage" class functionality.'), + 'group' => t('Browser'), + ); + } + + public function setUp() { + parent::setUp('browser_test'); + } + + /** + * Check "BrowserPage" class functionality. + */ + public function testBrowserPage() { + global $db_prefix; + + $browser = Browser::getInstance(); + $browser->setUserAgent($db_prefix); + + $browser->get(url('browser_test/print/post', array('absolute' => TRUE))); + $page = $browser->getPage(); + $input = $page->xpath('//input[@name="foo"]'); + $input = $input[0]; + $this->assertEqual('foo', $input['name'], t('Field "foo" found')); + } +} 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,203 @@ +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', + 'header' => array( + 'Content-Type' => 'application/x-www-form-urlencoded', + ), + )); + + 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, $url, array $additional) { + return FALSE; + } + + /** + * Build the result of the request. + * + * @return + * An associative array containing state information, including: 1) url, 2) + * headers, 3) content. + * @see Browser->getUrl() + * @see Browser->getHeaders() + * @see Browser->getContent() + */ + protected function buildRequest() { + if ($this->content !== FALSE) { + return array( + 'url' => $this->url, + 'headers' => $this->headers, + 'content' => $this->content, + ); + } + return FALSE; + } + + /** + * 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(). + */ + 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']; + $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; + $this->parseHeaders($http_response_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. + */ + protected function generateHeaderString(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. + */ + 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($parts[1]); + } + } + } +} + +/** + * @} End of "ingroup browser". + */ 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,652 @@ +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 + */ + +/** + * Browser API class. + * + * All browser functionality is provided by this main class which manages the + * various aspects of the browser. + */ +class Browser { + + /** + * The Browser instances for each wrapper type. + * + * @var array + */ + protected static $browser = array(); + + /** + * The backend wrapper instance. + * + * @var HttpWrapperInterface + */ + protected $wrapper; + + /** + * Information about the wrapper and what it supports. + * + * @var array + */ + protected $info; + + /** + * The URL of the current page. + * + * @var string + */ + protected $url; + + /** + * The response headers of the current page. + * + * @var Array + */ + protected $headers; + + /** + * The raw content of the current page. + * + * @var string + */ + protected $content; + + /** + * The BrowserPage class representing to the current page. + * + * @var BrowserPage + */ + protected $page; + + /** + * Initialize the browser with the wrapper backend to use and wrapper info. + * + * @param $wrapper + * Name of wrapper to be used for backend HTTP requests. Natively the + * browser provides either 'stream' or 'curl', provided proper server + * support. If none specified then defaults to 'stream'. + * @param $info + * Information about the wrapper and what it supports. + */ + public function __construct($wrapper, $info) { + $class = 'HttpWrapper_' . $wrapper; + $this->wrapper = new $class(); + $this->info = $info; + + $this->setUserAgent('Drupal 7 (+http://drupal.org/)'); + } + + /** + * Get the instance of the browser for the specified wrapper. + * + * @param $wrapper + * Name of wrapper to be used for backend HTTP requests. Natively the + * browser provides either 'stream' or 'curl', provided proper server + * support. If none specified then defaults to 'stream'. + * @return + * The browser instance for the specified wrapper. + */ + public static final function getInstance($wrapper = NULL) { + if (!$wrapper) { + // Use the default wrapper specified for the site. + $wrapper = variable_get('browser_wrapper', 'stream'); + } + + // Create a browser instance for the specified wrapper if it has not + // already been created. + if (!isset(self::$browser[$wrapper])) { + // Ensure that the specified wrapper is valid, if not use the default. + $wrappers = module_invoke_all('browser_wrapper'); + if (!isset($wrappers[$wrapper])) { + $wrapper = variable_get('browser_wrapper', 'stream'); + } + + self::$browser[$wrapper] = new Browser($wrapper, $wrappers[$wrapper]); + } + return self::$browser[$wrapper]; + } + + /** + * Perform a GET request. + * + * @param $url + * Absolute URL to request. + * @return + * Associative array of state information, as returned by getState(). + * @see getState(). + */ + public function get($url) { + if (!$this->isMethodSupported('GET')) { + return FALSE; + } + + $request = $this->wrapper->get($url); + + // TODO Error check, look for meta refresh, etc. + $this->setState($request['url'], $request['headers'], $request['content']); + return $this->getState(); + } + + /** + * 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 getState(). + * @see getState(). + */ + 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) { + $this->get($url); + } + else { + $url = $this->url; + } + + if (($page = $this->getPage()) === FALSE) { + return FALSE; + } + + if (($form = $this->findForm($fields, $submit)) === FALSE) { + return FALSE; + } + + // If form specified action then use that for the post url. + if ($form['action']) { + $url = $page->getAbsoluteUrl($form['action']); + } + + $request = $this->wrapper->post($url, $form['post']); + + // TODO Error check, look for meta refresh, etc. + $this->setState($request['url'], $request['headers'], $request['content']); + return $this->getState(); + } + + /** + * 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. + */ + 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; + } + + /** + * 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. + */ + 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; + } + // Known type, field processed. + unset($fields[$name]); + } + 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; + } + } + // 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 = $this->getDefaultFieldValue($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. + */ + protected function processField($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. + */ + protected function getDefaultFieldValue($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; + } + } + + /** + * Peform a request of arbitrary type. + * + * Please use get() and post() for GET and POST requests respectively. + * + * @param $method + * The method string identifier. + * @param $url + * Absolute URL to request. + * @param $additional + * Additional parameters related to the particular request method. + * @return + * Associative array of state information, as returned by getState(). + * @see getState(). + */ + public function request($method, $url, array $additional) { + if (!$this->isMethodSupported($method)) { + return FALSE; + } + + $request = $this->wrapper->request($method, $url, $additional); + + $this->setState($request['url'], $request['headers'], $request['content']); + return $this->getState(); + } + + /** + * Check the the method is supported by the backend. + * + * @param $method + * The method string identifier. + */ + public function isMethodSupported($method) { + return in_array(strtoupper($method), $this->info['methods']); + } + + /** + * Get the request headers. + * + * The request headers are sent in every request made by the browser with a + * few changes made the the individual request methods. + * + * @return + * Associative array of request headers. + */ + public function getRequestHeaders() { + return $this->wrapper->getRequestHeaders(); + } + + /** + * Set the request headers. + * + * @param $headers + * Associative array of request headers. + */ + public function setRequestHeaders(array $headers) { + $this->wrapper->setRequestHeaders($headers); + } + + /** + * Get the user-agent that the browser is identifying itself as. + * + * @return + * Browser user-agent. + */ + public function getUserAgent() { + $headers = $this->getRequestHeaders(); + return $headers['User-Agent']; + } + + /** + * Set the user-agent that the browser will identify itself as. + * + * @param $agent + * User-agent to to identify as. + */ + public function setUserAgent($agent) { + $headers = $this->getRequestHeaders(); + $headers['User-Agent'] = $agent; + $this->wrapper->setRequestHeaders($headers); + } + + /** + * Get the current state of the browser. + * + * @return + * An associative array containing state information, including: 1) url, 2) + * headers, 3) content. + * @see getUrl() + * @see getHeaders() + * @see getContent() + */ + public function getState() { + return array( + 'url' => $this->url, + 'headers' => $this->headers, + 'content' => $this->content, + ); + } + + /** + * Set the state of the browser. + * + * @param $url + * The URL of the current page. + * @param $headers + * The response headers of the current page. + * @param $content + * The raw content of the current page. + */ + public function setState($url, $headers, $content) { + $this->url = $url; + $this->headers = $headers; + $this->content = $content; + + // Clear the page variable since the content has change. + unset($this->page); + + $this->checkForRefresh(); + } + + /** + * Check for a refresh signifier. + * + * A refresh signifier can either be the 'Location' HTTP header or the meta + * tag 'http-equiv="Refresh"'. + */ + protected function checkForRefresh() { + // If not handled by backend wrapper then go ahead and handle. + if (isset($this->headers['Location'])) { + // Expect absolute URL. + $this->get($this->headers['Location']); + } + + if (($page = $this->getPage()) !== 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)) { + $this->get($page->getAbsoluteUrl(decode_entities($match['url']))); + } + } + } + + /** + * Get the URL of the current page. + * + * @return + * The URL of the current page. + */ + public function getUrl() { + return $this->url; + } + + /** + * Get the response headers of the current page. + * + * @return + * The response headers of the current page. + */ + public function getHeaders() { + return $this->headers; + } + + /** + * Get the raw content of the current page. + * + * @return + * The raw content for the current page. + */ + public function getContent() { + return $this->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. + */ + public function getPage() { + if (!isset($this->page)) { + $this->page = new BrowserPage($this->url, $this->headers, $this->content); + if (!$this->page->isValid()) { + return FALSE; + } + } + return $this->page; + } + + /** + * Close the wrapper connection. + */ + function __destruct() { + $this->wrapper->close(); + } +} + +/** + * @} End of "defgroup browser". + */ 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,98 @@ +getState(). + * @see Browser->getState(). + */ + public function get($url); + + /** + * Peform a POST request. + * + * @param $url + * Absolute URL to request. + * @param $fields + * Associative array of fields to submit as POST variables. + * @return + * Associative array of state information, as returned by + * Browser->getState(). + * @see Browser->getState(). + */ + public function post($url, array $fields); + + /** + * Peform a request of arbitrary type. + * + * @param $method + * The method string identifier. + * @param $url + * Absolute URL to request. + * @param $additional + * Additional parameters related to the particular request method. + * @return + * Associative array of state information, as returned by + * Browser->getState(). + * @see Browser->getState(). + */ + public function request($method, $url, array $additional); +} + +/** + * @} End of "ingroup 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; +} 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,195 @@ +handle)) { + $this->handle = curl_init(); + curl_setopt_array($this->handle, $this->getDefaultOptions()); + } + } + + public function close() { + if (isset($this->handle)) { + // Close the curl handler and unset it. + curl_close($this->handle); + unset($this->handle); + } + } + + /** + * Get the default curl options to be used with each request. + */ + protected function getDefaultOptions() { + return array( + CURLOPT_COOKIEJAR => $this->cookieFile, + CURLOPT_FOLLOWLOCATION => TRUE, + CURLOPT_HEADERFUNCTION => array(&$this, 'headerCallback'), + CURLOPT_HTTPHEADER => $this->request_headers, + CURLOPT_RETURNTRANSFER => TRUE, + CURLOPT_SSL_VERIFYPEER => FALSE, + CURLOPT_SSL_VERIFYHOST => FALSE, + CURLOPT_URL => '/', + CURLOPT_USERAGENT => $this->request_headers['User-Agent'], + ); + } + + public function getRequestHeaders() { + return $this->request_headers; + } + + public function setRequestHeaders($headers = array()) { + $this->request_headers = $headers; + + // Update request headers if handle is open. + if (isset($this->handle)) { + curl_setopt($this->handle, CURLOPT_USERAGENT, $this->request_headers['User-Agent']); + curl_setopt($this->handle, CURLOPT_HTTPHEADER, $this->request_headers); + } + } + + public function get($url) { + $this->execute(array( + CURLOPT_HTTPGET => TRUE, + CURLOPT_URL => $url, + CURLOPT_NOBODY => FALSE, + )); + + return $this->buildRequest(); + } + + public function post($url, array $fields) { + // TODO Add upload handling code. + + $this->execute(array( + CURLOPT_POST => TRUE, + CURLOPT_URL => $url, + CURLOPT_POSTFIELDS => http_build_query($fields, NULL, '&'), + )); + + return $this->buildRequest(); + } + + public function request($method, $url, array $additional) { + // TODO CURLOPT_CUSTOMREQUEST + return FALSE; + } + + /** + * Build the result of the request. + * + * @return + * An associative array containing state information, including: 1) url, 2) + * headers, 3) content. + * @see Browser->getUrl() + * @see Browser->getHeaders() + * @see Browser->getContent() + */ + protected function buildRequest() { + if ($this->content !== FALSE) { + return array( + 'url' => $this->url, + 'headers' => $this->headers, + 'content' => $this->content, + ); + } + return FALSE; + } + + /** + * Perform curl exec() after calling curlConnect(). + * + * @param $options + * Complete curl options array. + */ + protected function execute($options) { + $this->open(); + + curl_setopt_array($this->handle, $options); + $this->content = curl_exec($this->handle); + $this->url = curl_getinfo($this->handle, CURLINFO_EFFECTIVE_URL); + // $this->headers should be filled by headerCallback. + } + + /** + * Reads 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) + */ + 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($parts[1]); + } + } + return strlen($header); + } +} + +/** + * @} End of "ingroup browser". + */ 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,312 @@ +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) { + return $this->root->xpath($xpath); + } + + /** + * 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 "ingroup browser". + */