From 07c8da02a96216b87842190cca2777e82a668d1f Mon Sep 17 00:00:00 2001 From: David Hicks Date: Tue, 27 Oct 2009 17:02:27 +1100 Subject: [PATCH] Fix #11075: Add Content-Disposition workaround for Internet Explorer/Chrome Internet Explorer and Chrome don't support RFC2231 and also ignore the fallback method currently implemented in MantisBT. See http://greenbytes.de/tech/tc2231/#attfnboth2 for the current method. We can however use another method to display UTF8 filenames to IE and Chrome. This workaround is actually in breach in RFC2231. See http://greenbytes.de/tech/tc2231/#attwithfnrawpctenclong for details. We use this method when the user agent is determined to be IE or Chrome. Otherwise we just keep using the original RFC2231 fallback technique mentioned above. It also appears that urlencode() is the wrong method to use for encoding filenames. Browsers seem to expect %20 as a space instead of +. Thus we should use rawurlencode() instead for the old method of encoding URLs. RFC2231 actually contains examples with %20 being used. Some minor cleanups were also performed in relation to sending the Content-Disposition header and also performing browser checks. --- core/file_api.php | 12 ++++-- core/http_api.php | 78 ++++++++++++++++++++++++++++++------- file_download.php | 12 ++---- print_all_bug_page_word.php | 7 +--- 4 files changed, 79 insertions(+), 30 deletions(-) diff --git a/core/file_api.php b/core/file_api.php index f8292641df..8def51af81 100644 --- a/core/file_api.php +++ b/core/file_api.php @@ -835,13 +835,19 @@ function file_get_extension( $p_filename ) { $t_extension = ''; $t_basename = $p_filename; if( utf8_strpos( $t_basename, '/' ) !== false ) { - $t_basename = end( explode( '/', $t_basename ) ); + // Note that we can't use end(explode(...)) on a single line because + // end() expects a reference to a variable and thus we first need to + // copy the result of explode() into a variable that end() can modify. + $t_components = explode( '/', $t_basename ); + $t_basename = end( $t_components ); } if( utf8_strpos( $t_basename, '\\' ) !== false ) { - $t_basename = end( explode( '\\', $t_basename ) ); + $t_components = explode( '\\', $t_basename ); + $t_basename = end( $t_components ); } if( utf8_strpos( $t_basename, '.' ) !== false ) { - $t_extension = end( explode( '.', $t_basename ) ); + $t_components = explode( '\\', $t_basename ); + $t_extension = end( $t_components ); } return $t_extension; } diff --git a/core/http_api.php b/core/http_api.php index 23be4fdc6a..8f4ba6e25f 100644 --- a/core/http_api.php +++ b/core/http_api.php @@ -22,6 +22,67 @@ * @link http://www.mantisbt.org */ +/** + * Check to see if the client is using Microsoft Internet Explorer so we can + * enable quirks and hacky non-standards-compliant workarounds. + * @return boolean True if Internet Explorer is detected as the user agent + */ +function is_browser_internet_explorer() { + $t_user_agent = isset( $_SERVER['HTTP_USER_AGENT'] ) ? $_SERVER['HTTP_USER_AGENT'] : 'none'; + + if ( strpos( $t_user_agent, 'MSIE' ) ) { + return true; + } + + return false; +} + +/** + * Checks to see if the client is using Google Chrome so we can enable quirks + * and hacky non-standards-compliant workarounds. + * @return boolean True if Chrome is detected as the user agent + */ +function is_browser_chrome() { + $t_user_agent = isset( $_SERVER['HTTP_USER_AGENT'] ) ? $_SERVER['HTTP_USER_AGENT'] : 'none'; + + if ( strpos( $t_user_agent, 'Chrome/' ) ) { + return true; + } + + return false; +} + +/** + * Send a Content-Disposition header. This is more complex than it sounds + * because only a few browsers properly support RFC2231. For those browsers + * which are behind the times or are otherwise broken, we need to use + * some hacky workarounds to get them to work 'nicely' with attachments and + * inline files. See http://greenbytes.de/tech/tc2231/ for full reasoning. + * @param string Filename + * @param boolean Display file inline (optional, default = treat as attachment) + */ +function http_content_disposition_header( $p_filename, $p_inline = false ) { + if ( !headers_sent() ) { + $t_encoded_filename = rawurlencode( $p_filename ); + $t_disposition = ''; + if ( !$p_inline ) { + $t_disposition = 'attachment;'; + } + if ( is_browser_internet_explorer() || is_browser_chrome() ) { + // Internet Explorer does not support RFC2231 however it does + // incorrectly decode URL encoded filenames and we can use this to + // get UTF8 filenames to work with the file download dialog. Chrome + // behaves in the same was as Internet Explorer in this respect. + // See http://greenbytes.de/tech/tc2231/#attwithfnrawpctenclong + header( 'Content-Disposition:' . $t_disposition . ' filename="' . $t_encoded_filename . '"' ); + } else { + // For most other browsers, we can use this technique: + // http://greenbytes.de/tech/tc2231/#attfnboth2 + header( 'Content-Disposition:' . $t_disposition . ' filename*=UTF-8\'\'' . $t_encoded_filename . '; filename="' . $t_encoded_filename . '"' ); + } + } +} + /** * Set caching headers that will allow or prevent browser caching. * @param boolean Allow caching @@ -29,25 +90,14 @@ function http_caching_headers( $p_allow_caching=false ) { global $g_allow_browser_cache; - // Basic browser detection - $t_user_agent = isset($_SERVER['HTTP_USER_AGENT']) ? $_SERVER['HTTP_USER_AGENT'] : 'none'; - - $t_browser_name = 'Normal'; - if ( strpos( $t_user_agent, 'MSIE' ) ) { - $t_browser_name = 'IE'; - } - // Headers to prevent caching - // with option to bypass if running from script + // with option to bypass if running from script if ( !headers_sent() ) { if ( $p_allow_caching || ( isset( $g_allow_browser_cache ) && ON == $g_allow_browser_cache ) ) { - switch ( $t_browser_name ) { - case 'IE': + if ( is_browser_internet_explorer() ) { header( 'Cache-Control: private, proxy-revalidate' ); - break; - default: + } else { header( 'Cache-Control: private, must-revalidate' ); - break; } } else { header( 'Cache-Control: no-store, no-cache, must-revalidate' ); diff --git a/file_download.php b/file_download.php index 4329c8efc5..d124d2b298 100644 --- a/file_download.php +++ b/file_download.php @@ -95,17 +95,13 @@ header( 'Pragma: public' ); $t_filename = file_get_display_name( $v_filename ); + $t_show_inline = false; $t_inline_files = explode(',', config_get('inline_file_exts', 'gif')); if ( in_array( utf8_strtolower( file_get_extension($t_filename) ), $t_inline_files ) ) { - $t_disposition = ''; //'inline;'; - } else { - $t_disposition = ' attachment;'; + $t_show_inline = true; } - # The following header has undefined behaviour but needs to be used to - # allow a fallback for old browsers that don't support RFC2231. - # See http://greenbytes.de/tech/tc2231/ for more information. - header( 'Content-Disposition:' . $t_disposition . ' filename*=UTF-8\'\'' . urlencode( $t_filename ) . '; filename="' . urlencode( $t_filename ) . '"' ); + http_content_disposition_header( $t_filename, $t_show_inline ); header( 'Last-Modified: ' . gmdate( 'D, d M Y H:i:s \G\M\T', $v_date_added ) ); @@ -113,7 +109,7 @@ # attached files via HTTPS, we disable the "Pragma: no-cache" # command when IE is used over HTTPS. global $g_allow_file_cache; - if ( ( isset( $_SERVER["HTTPS"] ) && ( "on" == utf8_strtolower( $_SERVER["HTTPS"] ) ) ) && preg_match( "/MSIE/", $_SERVER["HTTP_USER_AGENT"] ) ) { + if ( ( isset( $_SERVER["HTTPS"] ) && ( "on" == utf8_strtolower( $_SERVER["HTTPS"] ) ) ) && is_browser_internet_explorer() ) { # Suppress "Pragma: no-cache" header. } else { if ( !isset( $g_allow_file_cache ) ) { diff --git a/print_all_bug_page_word.php b/print_all_bug_page_word.php index b9b7b7efb6..d8582d3d93 100644 --- a/print_all_bug_page_word.php +++ b/print_all_bug_page_word.php @@ -49,17 +49,14 @@ if ( $f_type_page != 'html' ) { $t_export_title = helper_get_default_export_filename( '' ); $t_export_title = preg_replace( '/[\/:*?"<>|]/', '', $t_export_title ); + $t_export_title .= '.doc'; # Make sure that IE can download the attachments under https. header( 'Pragma: public' ); header( 'Content-Type: application/msword' ); - if ( preg_match( "/MSIE/", $_SERVER["HTTP_USER_AGENT"] ) ) { - header( 'Content-Disposition: attachment; filename="' . urlencode( $t_export_title ) . '.doc"' ); - } else { - header( 'Content-Disposition: attachment; filename="' . $t_export_title . '.doc"' ); - } + http_content_disposition_header( $t_export_title ); } # This is where we used to do the entire actual filter ourselves