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	9 Sep 2009 22:37:08 -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 (substr($exception, 0, 1) == '.' && substr_compare($hostname, $exception, -strlen($exception)) == 0) {
+          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) && $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,
       )
     ));
 
@@ -685,18 +728,26 @@ class Browser {
     $this->headers = array();
 
     // Ensure that request headers are up to date.
     if ($this->getHttpAuthentication()) {
       curl_setopt($this->handle, CURLOPT_USERPWD, $this->getHttpAuthentication());
     }
     curl_setopt($this->handle, CURLOPT_USERAGENT, $this->requestHeaders['User-Agent']);
     curl_setopt($this->handle, 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');
+      }
+    }
+
     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	9 Sep 2009 22:37:09 -0000
@@ -35,18 +35,40 @@ 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', implode(',', gethostbynamel('drupal.org')));
+    $this->assertTrue($browser->shouldUseProxy('http://example.com/'), t('Proxy is used for host not in exception list.'));
+    $this->assertFalse($browser->shouldUseProxy('http://drupal.org/'), t('Proxy is not used for host that resolves 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.197
diff -u -9 -p -r1.197 system.admin.inc
--- modules/system/system.admin.inc	26 Aug 2009 10:53:45 -0000	1.197
+++ modules/system/system.admin.inc	9 Sep 2009 22:37:10 -0000
@@ -1461,18 +1461,88 @@ 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.'),
+  );
+  $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'),
+  );
+  $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' => 'textfield',
+    '#title' => t('Password'),
+    '#default_value' => '',
+    '#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'),
+  );
+
+  $form['#validate'][] = 'system_proxy_settings_validate';
+
+  return system_settings_form($form);
+}
+
+/**
+ * Validate the submitted proxy form.
+ */
+function system_proxy_settings_validate($form, &$form_state) {
+  // Check that an IP address or a valid hostname is specified.
+  if (strlen($form_state['values']['proxy_hostname']) > 0
+      && !filter_var($form_state['values']['proxy_hostname'], FILTER_VALIDATE_IP)
+      && gethostbyname($form_state['values']['proxy_hostname']) == $form_state['values']['proxy_hostname']) {
+    form_set_error('proxy_hostname', t('The proxy host name cannot be found.'));
+  }
+  if (strlen($form_state['values']['proxy_port']) > 0) {
+    $proxy_port = $form_state['values']['proxy_port'];
+    // TCP allows the port to be between 0 and 65535 inclusive
+    if (!is_numeric($proxy_port) || ($proxy_port < 0) || ($proxy_port > 65535)) {
+      form_set_error('proxy_port', t('The proxy port 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.785
diff -u -9 -p -r1.785 system.module
--- modules/system/system.module	5 Sep 2009 15:05:04 -0000	1.785
+++ modules/system/system.module	9 Sep 2009 22:37:11 -0000
@@ -878,18 +878,26 @@ function system_menu() {
   $items['admin/config/system/site-information'] = array(
     'title' => 'Site information',
     'description' => 'Change basic site information, such as the site name, slogan, e-mail address, mission, front page and more.',
     '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',
