Index: includes/browser.inc =================================================================== RCS file: /cvs/drupal/drupal/includes/browser.inc,v retrieving revision 1.3 diff -u -9 -p -r1.3 browser.inc --- includes/browser.inc 31 Aug 2009 05:56:54 -0000 1.3 +++ includes/browser.inc 14 Sep 2009 17:46:30 -0000 @@ -631,37 +631,80 @@ class Browser { public function request($method, $url, array $additional) { if (!$this->isMethodSupported($method)) { return FALSE; } // TODO } /** + * Check whether the request should be made through a proxy server. + * + * @param $url + * The URL to request. + */ + public function shouldUseProxy($url) { + if (!variable_get('proxy_hostname')) { + return FALSE; + } + $hostname = parse_url($url, PHP_URL_HOST); + foreach (explode(',', variable_get('proxy_exceptions')) as $exception) { + $exception = trim($exception); + if ($exception) { + if ($hostname == $exception) { + return FALSE; + } + // If the exception begins with a period, it should match the end of + // the hostname, e.g. ".example.com" matches "foo.example.com" but not + // "foo.example.com.example.org". + elseif ($exception[0] == '.' && substr($hostname, -strlen($exception)) == $exception) { + return FALSE; + } + // If the exception is an IP address, resolve the hostname and compare + // its address. If $hostname is already an IP address, gethostbyname() + // returns it unmodified. + elseif (filter_var($exception, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4) && $exception == gethostbyname($hostname)) { + return FALSE; + } + } + } + return TRUE; + } + + /** * Perform the request using the PHP stream wrapper. * * @param $url - * The url to request. + * The URL to request. * @param $options * The HTTP stream context options to be passed to * stream_context_set_params(). */ protected function streamExecute($url, array $options) { // 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. + // Merge default request headers with the passed headers. $headers = $this->requestHeaders + $options['header']; + + if ($this->shouldUseProxy($url)) { + $options['proxy'] = 'tcp://' . variable_get('proxy_hostname') . ':' . variable_get('proxy_port', 8080); + $options['request_fulluri'] = TRUE; + if (variable_get('proxy_username')) { + $headers['Proxy-Authorization'] = 'Basic ' . base64_encode(variable_get('proxy_username') . ':' . variable_get('proxy_password')); + } + } + + // Generate header string to be sent in HTTP request. $options['header'] = $this->headerString($headers); // Update the handler options. stream_context_set_params($this->handle, array( 'options' => array( 'http' => $options, ) )); @@ -680,22 +723,33 @@ class Browser { * Curl options to set, any options not set will maintain their previous * value. */ function curlExecute(array $options) { // Headers need to be reset since callback appends. $this->headers = array(); // Ensure that request headers are up to date. if ($this->getHttpAuthentication()) { - curl_setopt($this->handle, CURLOPT_USERPWD, $this->getHttpAuthentication()); + $options[CURLOPT_USERPWD] = $this->getHttpAuthentication(); + } + $options[CURLOPT_USERAGENT] = $this->requestHeaders['User-Agent']; + $options[CURLOPT_HTTPHEADER] = $this->requestHeaders; + + if ($this->shouldUseProxy($options[CURLOPT_URL])) { + $options[CURLOPT_PROXY] = variable_get('proxy_hostname'); + $options[CURLOPT_PROXYPORT] = variable_get('proxy_port', 8080); + if (variable_get('proxy_username')) { + $options[CURLOPT_PROXYUSERPWD] = variable_get('proxy_username') . ':' . variable_get('proxy_password'); + } + } + else { + $options[CURLOPT_PROXY] = FALSE; } - curl_setopt($this->handle, CURLOPT_USERAGENT, $this->requestHeaders['User-Agent']); - curl_setopt($this->handle, CURLOPT_HTTPHEADER, $this->requestHeaders); 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 $this->curlHeaderCallback(). unset($this->page); } Index: modules/simpletest/tests/browser.test =================================================================== RCS file: /cvs/drupal/drupal/modules/simpletest/tests/browser.test,v retrieving revision 1.1 diff -u -9 -p -r1.1 browser.test --- modules/simpletest/tests/browser.test 17 Aug 2009 06:08:47 -0000 1.1 +++ modules/simpletest/tests/browser.test 14 Sep 2009 17:46:30 -0000 @@ -35,18 +35,44 @@ class BrowserTestCase extends DrupalWebT // 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 the proxy exception list. + */ + public function testShouldUseProxy() { + $browser = new Browser(); + $this->assertFalse($browser->shouldUseProxy('http://example.com/'), t('Proxy is not used when a proxy hostname is specified.')); + + variable_set('proxy_hostname', 'proxy.example.com'); + $this->assertTrue($browser->shouldUseProxy('http://example.com/'), t('Proxy is used when a proxy hostname is specified.')); + + variable_set('proxy_exceptions', '.example.com,example.org'); + $this->assertTrue($browser->shouldUseProxy('http://example.com/'), t('Proxy is used for host in exception list.')); + $this->assertFalse($browser->shouldUseProxy('http://example.org/'), t('Proxy is not used for host in exception list.')); + $this->assertTrue($browser->shouldUseProxy('http://www.example.org/'), t('Proxy is used for host not in exception list.')); + $this->assertFalse($browser->shouldUseProxy('http://www.example.com/'), t('Proxy is not used for host that matches exception suffix.')); + $this->assertTrue($browser->shouldUseProxy('http://www.example.com.example.org/'), t('Proxy is used for host not in exception list.')); + + variable_set('proxy_exceptions', '127.0.0.1'); + $this->assertFalse($browser->shouldUseProxy('http://localhost/'), t('Proxy is not used for host that resolves to an IP address in exception list.')); + $this->assertFalse($browser->shouldUseProxy('http://127.0.0.1/'), t('Proxy is not used for IP address in exception list.')); + $this->assertTrue($browser->shouldUseProxy('http://127.0.0.2/'), t('Proxy is used for IP address in exception list.')); + + variable_set('proxy_exceptions', '10.0.0.1'); + $this->assertTrue($browser->shouldUseProxy('http://localhost/'), t('Proxy is used for host that does not resolve to an IP address in exception list.')); + } } /** * Test browser backend wrappers. */ class BrowserBackendTestCase extends DrupalWebTestCase { public static function getInfo() { return array( Index: modules/system/system.admin.inc =================================================================== RCS file: /cvs/drupal/drupal/modules/system/system.admin.inc,v retrieving revision 1.201 diff -u -9 -p -r1.201 system.admin.inc --- modules/system/system.admin.inc 11 Sep 2009 04:09:26 -0000 1.201 +++ modules/system/system.admin.inc 14 Sep 2009 17:46:30 -0000 @@ -1462,18 +1462,94 @@ function system_performance_settings() { * * @ingroup forms */ function system_clear_cache_submit($form, &$form_state) { drupal_flush_all_caches(); drupal_set_message(t('Caches cleared.')); } /** + * Form builder; Configure the site proxy settings. + * + * @ingroup forms + * @see system_settings_form() + */ +function system_proxy_settings() { + + $form['forward_proxy'] = array( + '#type' => 'fieldset', + '#title' => t('Outgoing proxy server'), + '#description' => t('Specify a proxy server to be used when Drupal connects to other websites on the internet.'), + ); + $form['forward_proxy']['proxy_hostname'] = array( + '#type' => 'textfield', + '#title' => t('Hostname'), + '#default_value' => '', + '#description' => t('The hostname of the proxy server, e.g. localhost. Leave this field empty, if Drupal should connect directly to the internet.'), + '#element_validate' => array('system_hostname_validate'), + ); + $form['forward_proxy']['proxy_port'] = array( + '#type' => 'textfield', + '#title' => t('Port number'), + '#size' => 5, + '#maxlength' => 5, + '#default_value' => 8080, + '#description' => t('The port number of the proxy server, e.g. 3128 or 8080'), + '#element_validate' => array('system_port_validate'), + ); + $form['forward_proxy']['proxy_username'] = array( + '#type' => 'textfield', + '#title' => t('Username'), + '#default_value' => '', + '#description' => t('The username used to authenticate with the proxy server. Leave this field empty, if the proxy server does not require authentication.'), + ); + $form['forward_proxy']['proxy_password'] = array( + '#type' => 'password', + '#title' => t('Password'), + '#default_value' => '', + '#attributes' => array('value' => variable_get('proxy_password')), + '#description' => t('The password used to connect to the proxy server. This is stored as plain text.'), + ); + $form['forward_proxy']['proxy_exceptions'] = array( + '#type' => 'textfield', + '#title' => t('No proxy for'), + '#default_value' => 'localhost', + '#description' => t('Example: .example.com,localhost,192.168.1.2'), + ); + + return system_settings_form($form); +} + +/** + * Element validate function for proxy hostname. + */ +function system_hostname_validate($element, &$form_state) { + // Check that an IP address or a valid hostname is specified. + if (strlen($element['#value']) > 0 + && !filter_var($element['#value'], FILTER_VALIDATE_IP, FILTER_FLAG_IPV4) + && gethostbyname($element['#value']) == $element['#value']) { + form_error($element, t('The hostname cannot be found.')); + } +} + +/** + * Element validate function for proxy port number. + */ +function system_port_validate($element, &$form_state) { + if (strlen($element['#value']) > 0) { + // TCP allows the port to be between 0 and 65535 inclusive. + if (!is_numeric($element['#value']) || ($element['#value'] < 0) || ($element['#value'] > 65535)) { + form_error($element, t('The port number is invalid. It must be a number between 0 and 65535.')); + } + } +} + +/** * Form builder; Configure the site file handling. * * @ingroup forms * @see system_settings_form() */ function system_file_system_settings() { $form['file_public_path'] = array( '#type' => 'textfield', Index: modules/system/system.module =================================================================== RCS file: /cvs/drupal/drupal/modules/system/system.module,v retrieving revision 1.789 diff -u -9 -p -r1.789 system.module --- modules/system/system.module 11 Sep 2009 02:14:20 -0000 1.789 +++ modules/system/system.module 14 Sep 2009 17:46:31 -0000 @@ -137,18 +137,20 @@ function system_help($path, $arg) { case 'admin/config/system/actions/manage': $output = '
' . t('Actions are individual tasks that the system can do, such as unpublishing a piece of content or banning a user. Modules, such as the trigger module, can fire these actions when certain system events happen; for example, when a new post is added or when a user logs in. Modules may also provide additional actions.') . '
'; $output .= '' . t('There are two types of actions: simple and advanced. Simple actions do not require any additional configuration, and are listed here automatically. Advanced actions can do more than simple actions; for example, send an e-mail to a specified address, or check for certain words within a piece of content. These actions need to be created and configured first before they may be used. To create an advanced action, select the action from the drop-down below and click the Create button.') . '
'; if (module_exists('trigger')) { $output .= '' . t('You may proceed to the Triggers page to assign these actions to system events.', array('@url' => url('admin/structure/trigger'))) . '
'; } return $output; case 'admin/config/system/actions/configure': return t('An advanced action offers additional configuration options which may be filled out below. Changing the Description field is recommended, in order to better identify the precise action taking place. This description will be displayed in modules such as the trigger module when assigning actions to system events, so it is best if it is as descriptive as possible (for example, "Send e-mail to Moderation Team" rather than simply "Send e-mail").'); + case 'admin/config/system/proxy': + return '' . t('If Drupal is installed on a server behind a firewall that does not allow direct outbound connections to other websites, you may specify a proxy server below. In most network environments, this is not necessary.') . '
'; case 'admin/config/people/ip-blocking': return '' . t('IP addresses listed here are blocked from your site before any modules are loaded. You may add IP addresses to the list, or delete existing entries.') . '
'; case 'admin/reports/status': return '' . t("Here you can find a short overview of your site's parameters as well as any problems detected with your installation. It may be useful to copy and paste this information into support requests filed on drupal.org's support forums and project issue queues.") . '
'; } } /** * Implement hook_theme(). @@ -842,18 +844,26 @@ function system_menu() { $items['admin/config/system/site-information'] = array( 'title' => 'Site information', 'description' => 'Change basic site name, e-mail address, slogan, default front page, number of posts per page, error pages and cron.', 'page callback' => 'drupal_get_form', 'page arguments' => array('system_site_information_settings'), 'access arguments' => array('administer site configuration'), 'file' => 'system.admin.inc', 'weight' => -10, ); + $items['admin/config/system/proxy'] = array( + 'title' => 'Proxy server', + 'description' => 'Configure how Drupal connects to other websites.', + 'page callback' => 'drupal_get_form', + 'page arguments' => array('system_proxy_settings'), + 'access arguments' => array('administer site configuration'), + 'file' => 'system.admin.inc', + ); // Reports. $items['admin/reports'] = array( 'title' => 'Reports', 'description' => 'View reports from system logs and other status information.', 'page callback' => 'system_admin_menu_block_page', 'access arguments' => array('access site reports'), 'weight' => 5, 'position' => 'left',