Index: .htaccess =================================================================== RCS file: /cvs/drupal/drupal/.htaccess,v retrieving revision 1.99 diff -u -9 -p -r1.99 .htaccess --- .htaccess 9 Jan 2009 02:49:01 -0000 1.99 +++ .htaccess 4 Mar 2009 23:34:48 -0000 @@ -42,20 +42,22 @@ DirectoryIndex index.php # Requires mod_expires to be enabled. # Enable expirations. ExpiresActive On # Cache all files for 2 weeks after access (A). ExpiresDefault A1209600 - # Do not cache dynamically generated pages. - ExpiresByType text/html A1 + + # Caching headers for dynamically generated pages are set from PHP. + ExpiresActive Off + # Various rewrite rules. RewriteEngine on # If your site can be accessed both with and without the 'www.' prefix, you # can use one of the following settings to redirect users to your preferred # URL, either WITH or WITHOUT the 'www.' prefix. Choose ONLY one option: Index: CHANGELOG.txt =================================================================== RCS file: /cvs/drupal/drupal/CHANGELOG.txt,v retrieving revision 1.296 diff -u -9 -p -r1.296 CHANGELOG.txt --- CHANGELOG.txt 26 Jan 2009 14:08:40 -0000 1.296 +++ CHANGELOG.txt 4 Mar 2009 23:34:48 -0000 @@ -31,18 +31,20 @@ Drupal 7.0, xxxx-xx-xx (development vers * Added an edit tab to taxonomy term pages. * Redesigned password strength validator. * Redesigned the add content type screen. * Highlight duplicate URL aliases. * Renamed "input formats" to "text formats". * Added configurable ability for users to cancel their own accounts. - Performance: * Improved performance on uncached page views by loading multiple core objects in a single database query. + * Improved support for HTTP proxies (including reverse proxies), allowing + anonymous pageviews to be served entirely from the proxy. - Documentation: * Hook API documentation now included in Drupal core. - News aggregator: * Added OPML import functionality for RSS feeds. * Optionally, RSS feeds may be configured to not automatically generate feed blocks. - Search: * Added support for language-aware searches. - Aggregator: * Introduced architecture that allows pluggable parsers and processors for Index: includes/bootstrap.inc =================================================================== RCS file: /cvs/drupal/drupal/includes/bootstrap.inc,v retrieving revision 1.271 diff -u -9 -p -r1.271 bootstrap.inc --- includes/bootstrap.inc 1 Mar 2009 09:32:17 -0000 1.271 +++ includes/bootstrap.inc 4 Mar 2009 23:34:48 -0000 @@ -740,84 +740,272 @@ function drupal_load($type, $name) { $files[$type][$name] = TRUE; return TRUE; } return FALSE; } /** + * Set an HTTP response header for the current page. + * + * Note: When sending a Content-Type header, always include a 'charset' type, + * too. This is necessary to avoid security bugs (e.g. UTF-7 XSS). + * + * @param $name + * The HTTP header name, or a status code followed by a reason phrase, e.g. + * "404 Not Found". + * @param $value + * The HTTP header value; if omitted, the specified header is unset. + * @param $append + * Whether to append the value to an existing header or to replace it. + */ +function drupal_set_header($name = NULL, $value = NULL, $append = FALSE) { + // The headers as name/value pairs. + static $headers = array(); + + if (!isset($name)) { + return $headers; + } + + // Save status codes using the special key ":status". + if (preg_match('/^\d{3} /', $name)) { + $value = $name; + $name = ':status'; + } + else { + _drupal_set_preferred_header_name($name); + $name = strtolower($name); + } + + if (!isset($value)) { + $headers[$name] = FALSE; + } + elseif (isset($headers[$name]) && $append) { + // Multiple headers with identical names may be combined using comma (RFC + // 2616, section 4.2). + $headers[$name] .= ',' . $value; + } + else { + $headers[$name] = $value; + } + drupal_send_headers(array($name => $headers[$name]), TRUE); +} + +/** + * Get the HTTP response headers for the current page. + * + * @param $name + * An HTTP header name. If omitted, all headers are returned as name/value + * pairs. If an array value is FALSE, the header has been unset. + * @return + * A string containing the header value, or FALSE if the header has been set, + * or NULL if the header has not been set. + */ +function drupal_get_header($name = NULL) { + $headers = drupal_set_header(); + if (isset($name)) { + $name = strtolower($name); + return isset($headers[$name]) ? $headers[$name] : NULL; + } + else { + return $headers; + } +} + +/** + * Header names are case-insensitive, but for maximum compatibility they should + * follow "common form" (see RFC 2617, section 4.2). + */ +function _drupal_set_preferred_header_name($name = NULL) { + static $header_names = array(); + + if (!isset($name)) { + return $header_names; + } + $header_names[strtolower($name)] = $name; +} + +/** + * Send the HTTP response headers previously set using drupal_set_header(). + * Add default headers, unless they have been replaced or unset using + * drupal_set_header(). + * + * @param $default_headers + * An array of headers as name/value pairs. + * @param $single + * If TRUE and headers have already be sent, send only the specified header. + */ +function drupal_send_headers($default_headers = array(), $only_default = FALSE) { + static $headers_sent = FALSE; + $headers = drupal_get_header(); + if ($only_default && $headers_sent) { + $headers = array(); + } + $headers_sent = TRUE; + + $header_names = _drupal_set_preferred_header_name(); + foreach ($default_headers as $name => $value) { + $name_lower = strtolower($name); + if (!isset($headers[$name_lower])) { + $headers[$name_lower] = $value; + $header_names[$name_lower] = $name; + } + } + foreach ($headers as $name_lower => $value) { + if ($name_lower == ':status') { + header($_SERVER['SERVER_PROTOCOL'] . ' ' . $value); + } + // Skip headers that have been unset. + elseif ($value) { + header($header_names[$name_lower] . ': ' . $value); + } + } +} + +/** * Set HTTP headers in preparation for a page response. * - * Authenticated users are always given a 'no-cache' header, and will - * fetch a fresh page on every request. This prevents authenticated - * users seeing locally cached pages that show them as logged out. + * Authenticated users are always given a 'no-cache' header, and will fetch a + * fresh page on every request. This prevents authenticated users from seeing + * locally cached pages. + * + * Also give each page a unique ETag. This will force clients to include both + * an If-Modified-Since header and an If-None-Match header when doing + * conditional requests for the page (required by RFC 2616, section 13.3.4), + * making the validation more robust. This is a workaround for a bug in Mozilla + * Firefox that is triggered when Drupal's caching is enabled and the user + * accesses Drupal via an HTTP proxy (see + * https://bugzilla.mozilla.org/show_bug.cgi?id=269303): When an authenticated + * user requests a page, and then logs out and requests the same page again, + * Firefox may send a conditional request based on the page that was cached + * locally when the user was logged in. If this page did not have an ETag + * header, the request only contains an If-Modified-Since header. The date will + * be recent, because with authenticated users the Last-Modified header always + * refers to the time of the request. If the user accesses Drupal via a proxy + * server, and the proxy already has a cached copy of the anonymous page with an + * older Last-Modified date, the proxy may respond with 304 Not Modified, making + * the client think that the anonymous and authenticated pageviews are + * identical. * * @see page_set_cache() */ function drupal_page_header() { - header("Expires: Sun, 19 Nov 1978 05:00:00 GMT"); - header("Last-Modified: " . gmdate("D, d M Y H:i:s") . " GMT"); - header("Cache-Control: store, no-cache, must-revalidate"); - header("Cache-Control: post-check=0, pre-check=0", FALSE); + static $headers_sent = FALSE; + if ($headers_sent) { + return TRUE; + } + $headers_sent = TRUE; + + $default_headers = array( + 'Expires' => 'Sun, 19 Nov 1978 05:00:00 GMT', + 'Last-Modified' => gmdate(DATE_RFC1123, REQUEST_TIME), + 'Cache-Control' => 'no-cache, must-revalidate, post-check=0, pre-check=0', + 'ETag' => '"' . REQUEST_TIME . '"', + ); + drupal_send_headers($default_headers); } /** * Set HTTP headers in preparation for a cached page response. * - * The general approach here is that anonymous users can keep a local - * cache of the page, but must revalidate it on every request. Then, - * they are given a '304 Not Modified' response as long as they stay - * logged out and the page has not been modified. - * - */ -function drupal_page_cache_header($cache) { - // Create entity tag based on cache update time. - $etag = '"' . md5($cache->created) . '"'; + * The headers allow as much as possible in proxies and browsers without any + * particular knowledge about the pages. Modules can override these headers + * using drupal_set_header(). + * + * If the request is conditional (using If-Modified-Since and If-None-Match), + * and the conditions match those currently in the cache, a 304 Not Modified + * response is sent. + */ +function drupal_page_cache_header(stdClass $cache) { + // Negotiate whether to use compression. + $page_compression = variable_get('page_compression', TRUE) && extension_loaded('zlib'); + $return_compressed = $page_compression && isset($_SERVER['HTTP_ACCEPT_ENCODING']) && strpos($_SERVER['HTTP_ACCEPT_ENCODING'], 'gzip') !== FALSE; + + // Get headers set in hook_boot(). Keys are lower-case. + $hook_boot_headers = drupal_get_header(); + + // Headers generated in this function, that may be replaced or unset using + // drupal_set_headers(). Keys are mixed-case. + $default_headers = array(); + + foreach ($cache->headers as $name => $value) { + // In the case of a 304 response, certain headers must be sent, and the + // remaining may not (see RFC 2616, section 10.3.5). Do not override + // headers set in hook_boot(). + $name_lower = strtolower($name); + if (in_array($name_lower, array('content-location', 'expires', 'cache-control', 'vary')) && !isset($hook_boot_headers[$name_lower])) { + drupal_set_header($name, $value); + unset($cache->headers[$name]); + } + } + + // If a cache is served from a HTTP proxy without hitting the web server, + // the boot and exit hooks cannot be fired, so only allow caching in + // proxies with aggressive caching. If the client send a session cookie, do + // not bother caching the page in a public proxy, because the cached copy + // will only be served to that particular user due to Vary: Cookie, unless + // the Vary header has been replaced or unset in hook_boot() (see below). + $max_age = variable_get('cache') == CACHE_AGGRESSIVE && (!isset($_COOKIE[session_name()]) || isset($hook_boot_headers['vary'])) ? variable_get('cache_lifetime', 0) : 0; + $default_headers['Cache-Control'] = 'public, max-age=' . $max_age; + + // Entity tag should change if the output changes. + $etag = '"' . $cache->created . '-' . intval($return_compressed) . '"'; + header('Etag: ' . $etag); // See if the client has provided the required HTTP headers. $if_modified_since = isset($_SERVER['HTTP_IF_MODIFIED_SINCE']) ? strtotime($_SERVER['HTTP_IF_MODIFIED_SINCE']) : FALSE; $if_none_match = isset($_SERVER['HTTP_IF_NONE_MATCH']) ? stripslashes($_SERVER['HTTP_IF_NONE_MATCH']) : FALSE; if ($if_modified_since && $if_none_match && $if_none_match == $etag // etag must match && $if_modified_since == $cache->created) { // if-modified-since must match header($_SERVER['SERVER_PROTOCOL'] . ' 304 Not Modified'); - // All 304 responses must send an etag if the 200 response for the same object contained an etag - header("Etag: $etag"); + drupal_send_headers($default_headers); return; } - // Send appropriate response: - header("Last-Modified: " . gmdate(DATE_RFC1123, $cache->created)); - header("ETag: $etag"); - - // The following headers force validation of cache: - header("Expires: Sun, 19 Nov 1978 05:00:00 GMT"); - header("Cache-Control: must-revalidate"); - - if (variable_get('page_compression', TRUE)) { - // Determine if the browser accepts gzipped data. - if (@strpos($_SERVER['HTTP_ACCEPT_ENCODING'], 'gzip') === FALSE && function_exists('gzencode')) { - // Strip the gzip header and run uncompress. - $cache->data = gzinflate(substr(substr($cache->data, 10), 0, -8)); - } - elseif (function_exists('gzencode')) { + // Send the remaining headers. + foreach ($cache->headers as $name => $value) { + drupal_set_header($name, $value); + } + + $default_headers['Last-Modified'] = gmdate(DATE_RFC1123, $cache->created); + + // HTTP/1.0 proxies does not support the Vary header, so prevent any caching + // by sending an Expires date in the past. HTTP/1.1 clients ignores the + // Expires header if a Cache-Control: max-age= directive is specified (see RFC + // 2616, section 14.9.3). + $default_headers['Expires'] = 'Sun, 19 Nov 1978 05:00:00 GMT'; + + drupal_send_headers($default_headers); + + // Allow HTTP proxies to cache pages for anonymous users without a session + // cookie. The Vary header is used to indicates the set of request-header + // fields that fully determines whether a cache is permitted to use the + // response to reply to a subsequent request for a given URL without + // revalidation. If a Vary header has been set in hook_boot(), it is assumed + // that the module knows how to cache the page. + if (!isset($hook_boot_headers['vary'])) { + header('Vary: Cookie'); + } + + if ($page_compression) { + header('Vary: Accept-Encoding', FALSE); + // If page_compression is enabled, the cache contains gzipped data. + if ($return_compressed) { header('Content-Encoding: gzip'); } - } - - // Send the original request's headers. We send them one after - // another so PHP's header() function can deal with duplicate - // headers. - $headers = explode("\n", $cache->headers); - foreach ($headers as $header) { - header($header); + else { + // The client does not support compression, so unzip the data in the + // cache. Strip the gzip header and run uncompress. + $cache->data = gzinflate(substr(substr($cache->data, 10), 0, -8)); + } } print $cache->data; } /** * Unserializes and appends elements from a serialized string. * * @param $obj @@ -1190,35 +1378,41 @@ function _drupal_bootstrap($phase) { require_once DRUPAL_ROOT . '/includes/module.inc'; module_invoke_all('boot'); } // If there is a cached page, display it. if (is_object($cache)) { // Destroy empty anonymous sessions. if (drupal_session_is_started() && empty($_SESSION)) { session_destroy(); } + header('X-Drupal-Cache: HIT'); drupal_page_cache_header($cache); // If the skipping of the bootstrap hooks is not enforced, call hook_exit. if ($cache_mode != CACHE_AGGRESSIVE) { module_invoke_all('exit'); } // We are done. exit; } + // Prepare for non-cached page workflow. - drupal_page_header(); + // If the session has not already been started and output buffering is - // not enabled, the session must be started now before the HTTP headers - // are sent. If output buffering is enabled, the session may be started + // not enabled, the HTTP headers must be sent now, including the session + // cookie. If output buffering is enabled, the session may be started // at any time using drupal_session_start(). if ($cache === FALSE) { + drupal_page_header(); drupal_session_start(); } + else { + header('X-Drupal-Cache: MISS'); + } break; case DRUPAL_BOOTSTRAP_LANGUAGE: drupal_init_language(); break; case DRUPAL_BOOTSTRAP_PATH: require_once DRUPAL_ROOT . '/includes/path.inc'; // Initialize $_GET['q'] prior to loading modules and invoking hook_init(). Index: includes/cache.inc =================================================================== RCS file: /cvs/drupal/drupal/includes/cache.inc,v retrieving revision 1.28 diff -u -9 -p -r1.28 cache.inc --- includes/cache.inc 3 Feb 2009 12:30:14 -0000 1.28 +++ includes/cache.inc 4 Mar 2009 23:34:48 -0000 @@ -24,45 +24,42 @@ function cache_get($cid, $table = 'cache variable_set('cache_flush', 0); // Time to flush old cache data db_delete($table) ->condition('expire', CACHE_PERMANENT, '<>') ->condition('expire', $cache_flush, '<=') ->execute(); } $cache = db_query("SELECT data, created, headers, expire, serialized FROM {" . $table . "} WHERE cid = :cid", array(':cid' => $cid))->fetchObject(); - if (isset($cache->data)) { - // If the data is permanent or we're not enforcing a minimum cache lifetime - // always return the cached data. - if ($cache->expire == CACHE_PERMANENT || !variable_get('cache_lifetime', 0)) { - if ($cache->serialized) { - $cache->data = unserialize($cache->data); - } - } - // If enforcing a minimum cache lifetime, validate that the data is - // currently valid for this user before we return it by making sure the - // cache entry was created before the timestamp in the current session's - // cache timer. The cache variable is loaded into the $user object by - // _sess_read() in session.inc. - else { - if ($user->cache > $cache->created) { - // This cache data is too old and thus not valid for us, ignore it. - return FALSE; - } - else { - if ($cache->serialized) { - $cache->data = unserialize($cache->data); - } - } - } - return $cache; + + if (!isset($cache->data)) { + return FALSE; + } + + // If enforcing a minimum cache lifetime, validate that the data is + // currently valid for this user before we return it by making sure the cache + // entry was created before the timestamp in the current session's cache + // timer. The cache variable is loaded into the $user object by _sess_read() + // in session.inc. If the data is permanent or we're not enforcing a minimum + // cache lifetime always return the cached data. + if ($cache->expire != CACHE_PERMANENT && variable_get('cache_lifetime', 0) && $user->cache > $cache->created) { + // This cache data is too old and thus not valid for us, ignore it. + return FALSE; } - return FALSE; + + if ($cache->serialized) { + $cache->data = unserialize($cache->data); + } + if (isset($cache->headers)) { + $cache->headers = unserialize($cache->headers); + } + + return $cache; } /** * Store data in the persistent cache. * * The persistent cache is split up into four database * tables. Contributed modules can add additional tables. * * 'cache_page': This table stores generated pages for anonymous @@ -98,24 +95,24 @@ function cache_get($cid, $table = 'cache * - CACHE_PERMANENT: Indicates that the item should never be removed unless * explicitly told to using cache_clear_all() with a cache ID. * - CACHE_TEMPORARY: Indicates that the item should be removed at the next * general cache wipe. * - A Unix timestamp: Indicates that the item should be kept at least until * the given time, after which it behaves like CACHE_TEMPORARY. * @param $headers * A string containing HTTP header information for cached pages. */ -function cache_set($cid, $data, $table = 'cache', $expire = CACHE_PERMANENT, $headers = NULL) { +function cache_set($cid, $data, $table = 'cache', $expire = CACHE_PERMANENT, array $headers = NULL) { $fields = array( 'serialized' => 0, 'created' => REQUEST_TIME, 'expire' => $expire, - 'headers' => $headers, + 'headers' => isset($headers) ? serialize($headers) : NULL, ); if (!is_string($data)) { $fields['data'] = serialize($data); $fields['serialized'] = 1; } else { $fields['data'] = $data; $fields['serialized'] = 0; } Index: includes/common.inc =================================================================== RCS file: /cvs/drupal/drupal/includes/common.inc,v retrieving revision 1.870 diff -u -9 -p -r1.870 common.inc --- includes/common.inc 28 Feb 2009 07:36:06 -0000 1.870 +++ includes/common.inc 4 Mar 2009 23:34:49 -0000 @@ -149,44 +149,18 @@ function drupal_get_html_head() { /** * Reset the static variable which holds the aliases mapped for this request. */ function drupal_clear_path_cache() { drupal_lookup_path('wipe'); } /** - * Set an HTTP response header for the current page. - * - * Note: When sending a Content-Type header, always include a 'charset' type, - * too. This is necessary to avoid security bugs (e.g. UTF-7 XSS). - */ -function drupal_set_header($header = NULL) { - // We use an array to guarantee there are no leading or trailing delimiters. - // Otherwise, header('') could get called when serving the page later, which - // ends HTTP headers prematurely on some PHP versions. - static $stored_headers = array(); - - if (strlen($header)) { - header($header); - $stored_headers[] = $header; - } - return implode("\n", $stored_headers); -} - -/** - * Get the HTTP response headers for the current page. - */ -function drupal_get_headers() { - return drupal_set_header(); -} - -/** * Add a feed URL for the current page. * * This function can be called as long the HTML header hasn't been sent. * * @param $url * A url for the feed. * @param $title * The title of the feed. */ @@ -351,29 +325,29 @@ function drupal_goto($path = '', $query // drupal_goto() call gets executed upon redirection. exit(); } /** * Generates a site offline message. */ function drupal_site_offline() { drupal_maintenance_theme(); - drupal_set_header($_SERVER['SERVER_PROTOCOL'] . ' 503 Service unavailable'); + drupal_set_header('503 Service unavailable'); drupal_set_title(t('Site offline')); print theme('maintenance_page', filter_xss_admin(variable_get('site_offline_message', t('@site is currently under maintenance. We should be back shortly. Thank you for your patience.', array('@site' => variable_get('site_name', 'Drupal')))))); } /** * Generates a 404 error if the request can not be handled. */ function drupal_not_found() { - drupal_set_header($_SERVER['SERVER_PROTOCOL'] . ' 404 Not Found'); + drupal_set_header('404 Not Found'); watchdog('page not found', check_plain($_GET['q']), NULL, WATCHDOG_WARNING); // Keep old path for reference. if (!isset($_REQUEST['destination'])) { $_REQUEST['destination'] = $_GET['q']; } $path = drupal_get_normal_path(variable_get('site_404', '')); @@ -395,19 +369,19 @@ function drupal_not_found() { $page['#show_blocks'] = FALSE; print drupal_render_page($page); } /** * Generates a 403 error if the request is not allowed. */ function drupal_access_denied() { - drupal_set_header($_SERVER['SERVER_PROTOCOL'] . ' 403 Forbidden'); + drupal_set_header('403 Forbidden'); watchdog('access denied', check_plain($_GET['q']), NULL, WATCHDOG_WARNING); // Keep old path for reference. if (!isset($_REQUEST['destination'])) { $_REQUEST['destination'] = $_GET['q']; } $path = drupal_get_normal_path(variable_get('site_403', '')); if ($path && $path != $_GET['q']) { @@ -812,19 +786,19 @@ function _drupal_log_error($error, $fata try { watchdog('php', '%type: %message in %function (line %line of %file).', $error, WATCHDOG_ERROR); } catch (Exception $e) { $new_error = _drupal_decode_exception($e); drupal_set_message(t('%type: %message in %function (line %line of %file).', $new_error), 'error'); } if ($fatal) { - drupal_set_header($_SERVER['SERVER_PROTOCOL'] . ' Service unavailable'); + drupal_set_header('503 Service unavailable'); drupal_set_title(t('Error')); if (!defined('MAINTENANCE_MODE') && drupal_get_bootstrap_phase() == DRUPAL_BOOTSTRAP_FULL) { // To conserve CPU and bandwidth, omit the blocks. $page = drupal_get_page(t('The website encountered an unexpected error. Please try again later.')); $page['#show_blocks'] = FALSE; print drupal_render_page($page); } else { print theme('maintenance_page', t('The website encountered an unexpected error. Please try again later.'), FALSE); @@ -2811,19 +2785,19 @@ function drupal_to_js($var) { * * This function should be used for JavaScript callback functions returning * data in JSON format. It sets the header for JavaScript output. * * @param $var * (optional) If set, the variable will be converted to JSON and output. */ function drupal_json($var = NULL) { // We are returning JavaScript, so tell the browser. - drupal_set_header('Content-Type: text/javascript; charset=utf-8'); + drupal_set_header('Content-Type', 'text/javascript; charset=utf-8'); if (isset($var)) { echo drupal_to_js($var); } } /** * Wrapper around urlencode() which avoids Apache quirks. * @@ -2979,19 +2953,19 @@ function _drupal_bootstrap_full() { require_once DRUPAL_ROOT . '/includes/image.inc'; require_once DRUPAL_ROOT . '/includes/form.inc'; require_once DRUPAL_ROOT . '/includes/mail.inc'; require_once DRUPAL_ROOT . '/includes/actions.inc'; // Set the Drupal custom error handler. set_error_handler('_drupal_error_handler'); set_exception_handler('_drupal_exception_handler'); // Emit the correct charset HTTP header. - drupal_set_header('Content-Type: text/html; charset=utf-8'); + drupal_set_header('Content-Type', 'text/html; charset=utf-8'); // Detect string handling method unicode_check(); // Undo magic quotes fix_gpc_magic(); // Load all enabled modules module_load_all(); // Let all modules take action before menu system handles the request // We do not want this while running update.php. @@ -3011,36 +2985,47 @@ function _drupal_bootstrap_full() { * We thus only deal with the gzip variant and unzip the cache in case * the browser does not accept gzip encoding. * * @see drupal_page_header */ function page_set_cache() { global $user, $base_root; if (page_get_cache(FALSE)) { - $cache = TRUE; - $data = ob_get_contents(); + $cache_page = TRUE; + $cache = (object) array( + 'cid' => $base_root . request_uri(), + 'data' => ob_get_clean(), + 'expire' => CACHE_TEMPORARY, + 'created' => REQUEST_TIME, + 'headers' => drupal_get_header(), + ); if (variable_get('page_compression', TRUE) && function_exists('gzencode')) { // We do not store the data in case the zlib mode is deflate. This should // be rarely happening. if (zlib_get_coding_type() == 'deflate') { - $cache = FALSE; + $cache_page = FALSE; } elseif (zlib_get_coding_type() == FALSE) { - $data = gzencode($data, 9, FORCE_GZIP); + $cache->data = gzencode($cache->data, 9, FORCE_GZIP); } // The remaining case is 'gzip' which means the data is already // compressed and nothing left to do but to store it. } - ob_end_flush(); - if ($cache && $data) { - cache_set($base_root . request_uri(), $data, 'cache_page', CACHE_TEMPORARY, drupal_get_headers()); + if ($cache_page && $cache->data) { + cache_set($cache->cid, $cache->data, 'cache_page', $cache->expire, $cache->headers); } + drupal_page_cache_header($cache); + } + else { + // If output buffering was enabled during bootstrap, and the headers were + // not sent in the DRUPAL_BOOTSTRAP_LATE_PAGE_CACHE phase, send them now. + drupal_page_header(); } } /** * Executes a cron run when called * @return * Returns TRUE if ran successfully */ function drupal_cron_run() { Index: includes/file.inc =================================================================== RCS file: /cvs/drupal/drupal/includes/file.inc,v retrieving revision 1.162 diff -u -9 -p -r1.162 file.inc --- includes/file.inc 26 Feb 2009 07:30:26 -0000 1.162 +++ includes/file.inc 4 Mar 2009 23:34:49 -0000 @@ -1307,25 +1307,22 @@ function file_unmanaged_save_data($data, * String specifying the file path to transfer. * @param $headers * An array of HTTP headers to send along with file. */ function file_transfer($source, $headers) { if (ob_get_level()) { ob_end_clean(); } - foreach ($headers as $header) { - // To prevent HTTP header injection, we delete new lines that are - // not followed by a space or a tab. - // See http://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.2 - $header = preg_replace('/\r?\n(?!\t| )/', '', $header); - drupal_set_header($header); + foreach ($headers as $name => $value) { + drupal_set_header($name, $value); } + drupal_send_headers(); $source = file_create_path($source); // Transfer file in 1024 byte chunks to save memory usage. if ($fd = fopen($source, 'rb')) { while (!feof($fd)) { print fread($fd, 1024); } fclose($fd); Index: includes/registry.inc =================================================================== RCS file: /cvs/drupal/drupal/includes/registry.inc,v retrieving revision 1.12 diff -u -9 -p -r1.12 registry.inc --- includes/registry.inc 22 Feb 2009 17:55:29 -0000 1.12 +++ includes/registry.inc 4 Mar 2009 23:34:49 -0000 @@ -248,16 +248,25 @@ function _registry_skip_body(&$tokens) { // Scan through the rest of the tokens until we reach the matching // end brace. while ($num_braces && ($token = next($tokens))) { if ($token == '{') { ++$num_braces; } elseif ($token == '}') { --$num_braces; } + // Consume strings manually as workaround for a bug in PHP < 5.2.3 (see + // http://drupal.org/node/368116). + elseif ($token == '"' || $token == '`') { + $stop_token = $token; + while (($token = next($tokens)) && $token != $stop_token); + } + elseif (is_array($token) && $token[0] == T_START_HEREDOC) { + while (($token = next($tokens)) && !(is_array($token) && $token[0] == T_END_HEREDOC)); + } } } /** * @} End of "defgroup registry". */ Index: includes/theme.maintenance.inc =================================================================== RCS file: /cvs/drupal/drupal/includes/theme.maintenance.inc,v retrieving revision 1.21 diff -u -9 -p -r1.21 theme.maintenance.inc --- includes/theme.maintenance.inc 24 Nov 2008 10:41:39 -0000 1.21 +++ includes/theme.maintenance.inc 4 Mar 2009 23:34:49 -0000 @@ -102,19 +102,19 @@ function theme_task_list($items, $active /** * Generate a themed installation page. * * Note: this function is not themeable. * * @param $content * The page content to show. */ function theme_install_page($content) { - drupal_set_header('Content-Type: text/html; charset=utf-8'); + drupal_set_header('Content-Type', 'text/html; charset=utf-8'); // Assign content. $variables['content'] = $content; // Delay setting the message variable so it can be processed below. $variables['show_messages'] = FALSE; // The maintenance preprocess function is recycled here. template_preprocess_maintenance_page($variables); // Special handling of error messages @@ -156,19 +156,19 @@ function theme_install_page($content) { * * @param $content * The page content to show. * @param $show_messages * Whether to output status and error messages. * FALSE can be useful to postpone the messages to a subsequent page. */ function theme_update_page($content, $show_messages = TRUE) { // Set required headers. - drupal_set_header('Content-Type: text/html; charset=utf-8'); + drupal_set_header('Content-Type', 'text/html; charset=utf-8'); // Assign content and show message flag. $variables['content'] = $content; $variables['show_messages'] = $show_messages; // The maintenance preprocess function is recycled here. template_preprocess_maintenance_page($variables); // Special handling of warning messages. $messages = drupal_set_message(); Index: modules/aggregator/aggregator.pages.inc =================================================================== RCS file: /cvs/drupal/drupal/modules/aggregator/aggregator.pages.inc,v retrieving revision 1.24 diff -u -9 -p -r1.24 aggregator.pages.inc --- modules/aggregator/aggregator.pages.inc 3 Feb 2009 18:55:29 -0000 1.24 +++ modules/aggregator/aggregator.pages.inc 4 Mar 2009 23:34:49 -0000 @@ -365,19 +365,19 @@ function aggregator_page_rss() { * Theme the RSS output. * * @param $feeds * An array of the feeds to theme. * @param $category * A common category, if any, for all the feeds. * @ingroup themeable */ function theme_aggregator_page_rss($feeds, $category = NULL) { - drupal_set_header('Content-Type: application/rss+xml; charset=utf-8'); + drupal_set_header('Content-Type', 'application/rss+xml; charset=utf-8'); $items = ''; $feed_length = variable_get('feed_item_length', 'teaser'); foreach ($feeds as $feed) { switch ($feed_length) { case 'teaser': $teaser = node_teaser($feed->description); if ($teaser != $feed->description) { $teaser .= '

' . t('read more') . "

\n"; @@ -425,19 +425,19 @@ function aggregator_page_opml($cid = NUL /** * Theme the OPML feed output. * * @param $feeds * An array of the feeds to theme. * @ingroup themeable */ function theme_aggregator_page_opml($feeds) { - drupal_set_header('Content-Type: text/xml; charset=utf-8'); + drupal_set_header('Content-Type', 'text/xml; charset=utf-8'); $output = "\n"; $output .= "\n"; $output .= "\n"; $output .= '' . check_plain(variable_get('site_name', 'Drupal')) . "\n"; $output .= '' . gmdate('r') . "\n"; $output .= "\n"; $output .= "\n"; foreach ($feeds as $feed) { Index: modules/aggregator/aggregator_test.module =================================================================== RCS file: /cvs/drupal/drupal/modules/aggregator/aggregator_test.module,v retrieving revision 1.1 diff -u -9 -p -r1.1 aggregator_test.module --- modules/aggregator/aggregator_test.module 1 Mar 2009 07:21:02 -0000 1.1 +++ modules/aggregator/aggregator_test.module 4 Mar 2009 23:34:49 -0000 @@ -26,33 +26,33 @@ function aggregator_test_feed($use_last_ $last_modified = strtotime('Sun, 19 Nov 1978 05:00:00 GMT'); $etag = md5($last_modified); $if_modified_since = isset($_SERVER['HTTP_IF_MODIFIED_SINCE']) ? strtotime($_SERVER['HTTP_IF_MODIFIED_SINCE']) : FALSE; $if_none_match = isset($_SERVER['HTTP_IF_NONE_MATCH']) ? stripslashes($_SERVER['HTTP_IF_NONE_MATCH']) : FALSE; // Send appropriate response. We respond with a 304 not modified on either // etag or on last modified. if ($use_last_modified) { - drupal_set_header("Last-Modified: " . gmdate(DATE_RFC1123, $last_modified)); + drupal_set_header('Last-Modified', gmdate(DATE_RFC1123, $last_modified)); } if ($use_etag) { - drupal_set_header("ETag: " .$etag); + drupal_set_header('ETag', $etag); } // Return 304 not modified if either last modified or etag match. if ($last_modified == $if_modified_since || $etag == $if_none_match) { - drupal_set_header($_SERVER['SERVER_PROTOCOL'] . ' 304 Not Modified'); + drupal_set_header('304 Not Modified'); return; } // The following headers force validation of cache: - drupal_set_header("Expires: Sun, 19 Nov 1978 05:00:00 GMT"); - drupal_set_header("Cache-Control: must-revalidate"); - drupal_set_header('Content-Type: application/rss+xml; charset=utf-8'); + drupal_set_header('Expires', 'Sun, 19 Nov 1978 05:00:00 GMT'); + drupal_set_header('Cache-Control', 'must-revalidate'); + drupal_set_header('Content-Type', 'application/rss+xml; charset=utf-8'); // Read actual feed from file. $file_name = DRUPAL_ROOT . '/' . drupal_get_path('module', 'aggregator') . '/aggregator_test_rss091.xml'; $handle = fopen($file_name, 'r'); $feed = fread($handle, filesize($file_name)); fclose($handle); print $feed; } Index: modules/blogapi/blogapi.module =================================================================== RCS file: /cvs/drupal/drupal/modules/blogapi/blogapi.module,v retrieving revision 1.144 diff -u -9 -p -r1.144 blogapi.module --- modules/blogapi/blogapi.module 26 Feb 2009 07:30:26 -0000 1.144 +++ modules/blogapi/blogapi.module 4 Mar 2009 23:34:49 -0000 @@ -843,19 +843,19 @@ function blogapi_init() { } function blogapi_rsd() { global $base_url; $xmlrpc = $base_url . '/xmlrpc.php'; $base = url('', array('absolute' => TRUE)); $blogid = 1; # until we figure out how to handle multiple bloggers - drupal_set_header('Content-Type: application/rsd+xml; charset=utf-8'); + drupal_set_header('Content-Type', 'application/rsd+xml; charset=utf-8'); print <<<__RSD__ Drupal http://drupal.org/ $base Index: modules/node/node.module =================================================================== RCS file: /cvs/drupal/drupal/modules/node/node.module,v retrieving revision 1.1027 diff -u -9 -p -r1.1027 node.module --- modules/node/node.module 26 Feb 2009 07:30:27 -0000 1.1027 +++ modules/node/node.module 4 Mar 2009 23:34:49 -0000 @@ -1987,19 +1987,19 @@ function node_feed($nids = FALSE, $chann 'language' => $language->language ); $channel = array_merge($channel_defaults, $channel); $output = "\n"; $output .= "\n"; $output .= format_rss_channel($channel['title'], $channel['link'], $channel['description'], $items, $channel['language']); $output .= "\n"; - drupal_set_header('Content-Type: application/rss+xml; charset=utf-8'); + drupal_set_header('Content-Type', 'application/rss+xml; charset=utf-8'); print $output; } /** * Construct a drupal_render() style array from an array of loaded nodes. * * @param $nodes * An array of nodes as returned by node_load_multiple(). * @param $teaser Index: modules/simpletest/tests/bootstrap.test =================================================================== RCS file: /cvs/drupal/drupal/modules/simpletest/tests/bootstrap.test,v retrieving revision 1.12 diff -u -9 -p -r1.12 bootstrap.test --- modules/simpletest/tests/bootstrap.test 31 Jan 2009 16:50:57 -0000 1.12 +++ modules/simpletest/tests/bootstrap.test 4 Mar 2009 23:34:49 -0000 @@ -84,56 +84,99 @@ class BootstrapPageCacheTestCase extends function getInfo() { return array( 'name' => t('Page cache test'), 'description' => t('Enable the page cache and test it with conditional HTTP requests.'), 'group' => t('Bootstrap') ); } + function setUp() { + parent::setUp('system_test'); + } + /** - * Enable cache and examine HTTP headers. + * Test support for requests containing If-Modified-Since and If-None-Match headers. */ - function testPageCache() { + function testConditionalRequests() { variable_set('cache', CACHE_NORMAL); // Fill the cache. $this->drupalGet(''); $this->drupalHead(''); + $this->assertEqual($this->drupalGetHeader('X-Drupal-Cache'), 'HIT', t('Page was cached.')); $etag = $this->drupalGetHeader('ETag'); - $this->assertTrue($etag, t('An ETag header was sent, indicating that page was cached.')); $last_modified = $this->drupalGetHeader('Last-Modified'); $this->drupalGet('', array(), array('If-Modified-Since: ' . $last_modified, 'If-None-Match: ' . $etag)); $this->assertResponse(304, t('Conditional request returned 304 Not Modified.')); $this->drupalGet('', array(), array('If-Modified-Since: ' . gmdate(DATE_RFC822, strtotime($last_modified)), 'If-None-Match: ' . $etag)); $this->assertResponse(304, t('Conditional request with obsolete If-Modified-Since date returned 304 Not Modified.')); $this->drupalGet('', array(), array('If-Modified-Since: ' . gmdate(DATE_RFC850, strtotime($last_modified)), 'If-None-Match: ' . $etag)); $this->assertResponse(304, t('Conditional request with obsolete If-Modified-Since date returned 304 Not Modified.')); $this->drupalGet('', array(), array('If-Modified-Since: ' . $last_modified)); $this->assertResponse(200, t('Conditional request without If-None-Match returned 200 OK.')); - $this->assertTrue($this->drupalGetHeader('ETag'), t('An ETag header was sent, indicating that page was cached.')); + $this->assertEqual($this->drupalGetHeader('X-Drupal-Cache'), 'HIT', t('Page was cached.')); $this->drupalGet('', array(), array('If-Modified-Since: ' . gmdate(DATE_RFC1123, strtotime($last_modified) + 1), 'If-None-Match: ' . $etag)); $this->assertResponse(200, t('Conditional request with new a If-Modified-Since date newer than Last-Modified returned 200 OK.')); - $this->assertTrue($this->drupalGetHeader('ETag'), t('An ETag header was sent, indicating that page was cached.')); + $this->assertEqual($this->drupalGetHeader('X-Drupal-Cache'), 'HIT', t('Page was cached.')); $user = $this->drupalCreateUser(); $this->drupalLogin($user); $this->drupalGet('', array(), array('If-Modified-Since: ' . $last_modified, 'If-None-Match: ' . $etag)); $this->assertResponse(200, t('Conditional request returned 200 OK for authenticated user.')); - $this->assertFalse($this->drupalGetHeader('ETag'), t('An ETag header was not sent, indicating that page was not cached.')); + $this->assertFalse($this->drupalGetHeader('X-Drupal-Cache'), t('Absense of Page was not cached.')); } + /** + * Test cache headers. + */ + function testPageCache() { + variable_set('cache', CACHE_NORMAL); + + // Fill the cache. + $this->drupalGet('system-test/set-header', array('query' => array('name' => 'Foo', 'value' => 'bar'))); + $this->assertEqual($this->drupalGetHeader('X-Drupal-Cache'), 'MISS', t('Page was not cached.')); + $this->assertEqual($this->drupalGetHeader('Vary'), 'Cookie,Accept-Encoding', t('Vary header was sent.')); + $this->assertEqual($this->drupalGetHeader('Cache-Control'), 'public, max-age=0', t('Cache-Control header was sent.')); + $this->assertEqual($this->drupalGetHeader('Expires'), 'Sun, 19 Nov 1978 05:00:00 GMT', t('Expires header was sent.')); + $this->assertEqual($this->drupalGetHeader('Foo'), 'bar', t('Custom header was sent.')); + + // Check cache. + $this->drupalGet('system-test/set-header', array('query' => array('name' => 'Foo', 'value' => 'bar'))); + $this->assertEqual($this->drupalGetHeader('X-Drupal-Cache'), 'HIT', t('Page was cached.')); + $this->assertEqual($this->drupalGetHeader('Vary'), 'Cookie,Accept-Encoding', t('Vary: Cookie header was sent.')); + $this->assertEqual($this->drupalGetHeader('Cache-Control'), 'public, max-age=0', t('Cache-Control header was sent.')); + $this->assertEqual($this->drupalGetHeader('Expires'), 'Sun, 19 Nov 1978 05:00:00 GMT', t('Expires header was sent.')); + $this->assertEqual($this->drupalGetHeader('Foo'), 'bar', t('Custom header was sent.')); + + // Check replacing default headers. + $this->drupalGet('system-test/set-header', array('query' => array('name' => 'Expires', 'value' => 'Fri, 19 Nov 2008 05:00:00 GMT'))); + $this->assertEqual($this->drupalGetHeader('Expires'), 'Fri, 19 Nov 2008 05:00:00 GMT', t('Default header was replaced.')); + $this->drupalGet('system-test/set-header', array('query' => array('name' => 'Vary', 'value' => 'User-Agent'))); + $this->assertEqual($this->drupalGetHeader('Vary'), 'User-Agent,Accept-Encoding', t('Default header was replaced.')); + + // Check that authenticated users bypass the cache. + $user = $this->drupalCreateUser(); + $this->drupalLogin($user); + $this->drupalGet('system-test/set-header', array('query' => array('name' => 'Foo', 'value' => 'bar'))); + $this->assertFalse($this->drupalGetHeader('X-Drupal-Cache'), t('Caching was bypassed.')); + $this->assertFalse($this->drupalGetHeader('Vary'), t('Vary header was not sent.')); + $this->assertEqual($this->drupalGetHeader('Cache-Control'), 'no-cache, must-revalidate, post-check=0, pre-check=0', t('Cache-Control header was sent.')); + $this->assertEqual($this->drupalGetHeader('Expires'), 'Sun, 19 Nov 1978 05:00:00 GMT', t('Expires header was sent.')); + $this->assertEqual($this->drupalGetHeader('Foo'), 'bar', t('Custom header was sent.')); + + } } class BootstrapVariableTestCase extends DrupalWebTestCase { function setUp() { parent::setUp('system_test'); } function getInfo() { Index: modules/simpletest/tests/common.test =================================================================== RCS file: /cvs/drupal/drupal/modules/simpletest/tests/common.test,v retrieving revision 1.30 diff -u -9 -p -r1.30 common.test --- modules/simpletest/tests/common.test 28 Feb 2009 07:36:06 -0000 1.30 +++ modules/simpletest/tests/common.test 4 Mar 2009 23:34:49 -0000 @@ -599,33 +599,33 @@ class DrupalErrorHandlerUnitTest extends /** * Test the error handler. */ function testErrorHandler() { $error_notice = array( '%type' => 'Notice', '%message' => 'Undefined variable: bananas', '%function' => 'system_test_generate_warnings()', - '%line' => 184, + '%line' => 194, '%file' => realpath('modules/simpletest/tests/system_test.module'), ); $error_warning = array( '%type' => 'Warning', '%message' => 'Division by zero', '%function' => 'system_test_generate_warnings()', - '%line' => 186, + '%line' => 196, '%file' => realpath('modules/simpletest/tests/system_test.module'), ); $error_user_notice = array( '%type' => 'User notice', '%message' => 'Drupal is awesome', '%function' => 'system_test_generate_warnings()', - '%line' => 188, + '%line' => 198, '%file' => realpath('modules/simpletest/tests/system_test.module'), ); // Set error reporting to collect notices. variable_set('error_level', 2); $this->drupalGet('system-test/generate-warnings'); $this->assertErrorMessage($error_notice); $this->assertErrorMessage($error_warning); $this->assertErrorMessage($error_user_notice); @@ -647,26 +647,26 @@ class DrupalErrorHandlerUnitTest extends /** * Test the exception handler. */ function testExceptionHandler() { $error_exception = array( '%type' => 'Exception', '%message' => 'Drupal is awesome', '%function' => 'system_test_trigger_exception()', - '%line' => 197, + '%line' => 207, '%file' => realpath('modules/simpletest/tests/system_test.module'), ); $error_pdo_exception = array( '%type' => 'PDOException', '%message' => 'SQLSTATE', '%function' => 'system_test_trigger_pdo_exception()', - '%line' => 205, + '%line' => 215, '%file' => realpath('modules/simpletest/tests/system_test.module'), ); $this->drupalGet('system-test/trigger-exception'); $this->assertErrorMessage($error_exception); $this->drupalGet('system-test/trigger-pdo-exception'); // We cannot use assertErrorMessage() since the extact error reported // varies from database to database. Check for the error keyword 'SQLSTATE'. Index: modules/simpletest/tests/file.test =================================================================== RCS file: /cvs/drupal/drupal/modules/simpletest/tests/file.test,v retrieving revision 1.25 diff -u -9 -p -r1.25 file.test --- modules/simpletest/tests/file.test 22 Feb 2009 17:55:30 -0000 1.25 +++ modules/simpletest/tests/file.test 4 Mar 2009 23:34:49 -0000 @@ -1893,19 +1893,19 @@ class FileDownloadTest extends FileTestC function testPrivateFileTransfer() { // Set file downloads to private so handler functions get called. variable_set('file_downloads', FILE_DOWNLOADS_PRIVATE); // Create a file. $file = $this->createFile(); $url = file_create_url($file->filename); // Set file_test access header to allow the download. - file_test_set_return('download', array('x-foo: Bar')); + file_test_set_return('download', array('x-foo' => 'Bar')); $this->drupalHead($url); $headers = $this->drupalGetHeaders(); $this->assertEqual($headers['x-foo'] , 'Bar', t('Found header set by file_test module on private download.')); $this->assertResponse(200, t('Correctly allowed access to a file when file_test provides headers.')); // Deny access to all downloads via a -1 header. file_test_set_return('download', -1); $this->drupalHead($url); $this->assertResponse(403, t('Correctly denied access to a file when file_test sets the header to -1.')); Index: modules/simpletest/tests/session.test =================================================================== RCS file: /cvs/drupal/drupal/modules/simpletest/tests/session.test,v retrieving revision 1.10 diff -u -9 -p -r1.10 session.test --- modules/simpletest/tests/session.test 19 Jan 2009 10:46:51 -0000 1.10 +++ modules/simpletest/tests/session.test 4 Mar 2009 23:34:49 -0000 @@ -157,19 +157,19 @@ class SessionTestCase extends DrupalWebT variable_set('cache', CACHE_NORMAL); // During this request the session is destroyed in drupal_page_footer(), // and the session cookie is unset. $this->drupalGet(''); $this->assertSessionCookie(TRUE); $this->assertSessionStarted(TRUE); $this->assertSessionEmpty(TRUE); - $this->assertFalse($this->drupalGetHeader('ETag'), t('Page was not cached.')); + $this->assertEqual($this->drupalGetHeader('X-Drupal-Cache'), 'MISS', t('Page was not cached.')); // When PHP deletes a cookie, it sends "Set-Cookie: cookiename=deleted; // expires=..." $this->assertTrue(preg_match('/SESS\w+=deleted/', $this->drupalGetHeader('Set-Cookie')), t('Session cookie was deleted.')); // Verify that the session cookie was actually deleted. $this->drupalGet(''); $this->assertSessionCookie(FALSE); $this->assertSessionStarted(FALSE); $this->assertFalse($this->drupalGetHeader('Set-Cookie'), t('New session was not started.')); @@ -179,36 +179,36 @@ class SessionTestCase extends DrupalWebT $this->assertSessionCookie(FALSE); $this->assertSessionStarted(FALSE); $this->assertTrue($this->drupalGetHeader('Set-Cookie'), t('New session was started.')); // Display the message. $this->drupalGet(''); $this->assertSessionCookie(TRUE); $this->assertSessionStarted(TRUE); $this->assertSessionEmpty(FALSE); - $this->assertFalse($this->drupalGetHeader('ETag'), t('Page was not cached.')); + $this->assertFalse($this->drupalGetHeader('X-Drupal-Cache'), t('Caching was bypassed.')); $this->assertText(t('This is a dummy message.'), t('Message was displayed.')); // During this request the session is destroyed in _drupal_bootstrap(), // and the session cookie is unset. $this->drupalGet(''); $this->assertSessionCookie(TRUE); $this->assertSessionStarted(TRUE); $this->assertSessionEmpty(TRUE); - $this->assertTrue($this->drupalGetHeader('ETag'), t('Page was cached.')); + $this->assertEqual($this->drupalGetHeader('X-Drupal-Cache'), 'HIT', t('Page was cached.')); $this->assertNoText(t('This is a dummy message.'), t('Message was not cached.')); $this->assertTrue(preg_match('/SESS\w+=deleted/', $this->drupalGetHeader('Set-Cookie')), t('Session cookie was deleted.')); // Verify that session was destroyed. $this->drupalGet(''); $this->assertSessionCookie(FALSE); $this->assertSessionStarted(FALSE); - $this->assertTrue($this->drupalGetHeader('ETag'), t('Page was cached.')); + $this->assertEqual($this->drupalGetHeader('X-Drupal-Cache'), 'HIT', t('Page was cached.')); $this->assertFalse($this->drupalGetHeader('Set-Cookie'), t('New session was not started.')); // Verify that modifying $_SESSION without having started a session // generates a watchdog message, and that no messages have been generated // so far. $this->assertEqual($this->getWarningCount(), 0, t('No watchdog messages have been generated')); $this->drupalGet('/session-test/set-not-started'); $this->assertSessionCookie(FALSE); $this->assertSessionStarted(FALSE); Index: modules/simpletest/tests/system_test.module =================================================================== RCS file: /cvs/drupal/drupal/modules/simpletest/tests/system_test.module,v retrieving revision 1.8 diff -u -9 -p -r1.8 system_test.module --- modules/simpletest/tests/system_test.module 9 Dec 2008 11:09:26 -0000 1.8 +++ modules/simpletest/tests/system_test.module 4 Mar 2009 23:34:49 -0000 @@ -11,18 +11,23 @@ function system_test_menu() { 'type' => MENU_CALLBACK, ); $items['system-test/redirect/%'] = array( 'title' => 'Redirect', 'page callback' => 'system_test_redirect', 'page arguments' => array(2), 'access arguments' => array('access content'), 'type' => MENU_CALLBACK, ); + $items['system-test/set-header'] = array( + 'page callback' => 'system_test_set_header', + 'access arguments' => array('access content'), + 'type' => MENU_CALLBACK, + ); $items['system-test/redirect-noscheme'] = array( 'page callback' => 'system_test_redirect_noscheme', 'access arguments' => array('access content'), 'type' => MENU_CALLBACK, ); $items['system-test/redirect-noparse'] = array( 'page callback' => 'system_test_redirect_noparse', 'access arguments' => array('access content'), 'type' => MENU_CALLBACK, @@ -89,18 +94,23 @@ function system_test_basic_auth_page() { function system_test_redirect($code) { $code = (int)$code; if ($code != 200) { header("Location: " . url('system-test/redirect/200', array('absolute' => TRUE)), TRUE, $code); exit; } return ''; } +function system_test_set_header() { + drupal_set_header($_GET['name'], $_GET['value']); + return t('The following header was set: %name: %value', array('%name' => $_GET['name'], '%value' => $_GET['value'])); +} + function system_test_redirect_noscheme() { header("Location: localhost/path", TRUE, 301); exit; } function system_test_redirect_noparse() { header("Location: http:///path", TRUE, 301); exit; } Index: modules/system/system.module =================================================================== RCS file: /cvs/drupal/drupal/modules/system/system.module,v retrieving revision 1.668 diff -u -9 -p -r1.668 system.module --- modules/system/system.module 22 Feb 2009 17:55:30 -0000 1.668 +++ modules/system/system.module 4 Mar 2009 23:34:49 -0000 @@ -2277,18 +2277,18 @@ function theme_meta_generator_html($vers drupal_set_html_head(''); } /** * Send Drupal and the major version number in the HTTP headers. * * @ingroup themeable */ function theme_meta_generator_header($version = VERSION) { - drupal_set_header('X-Generator: Drupal ' . $version . ' (http://drupal.org)'); + drupal_set_header('X-Generator', 'Drupal ' . $version . ' (http://drupal.org)'); } /** * Implementation of hook_image_toolkits(). */ function system_image_toolkits() { return array('gd'); }