1+ // Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
2+ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+ using Microsoft . AspNet . Http ;
5+ using System ;
6+ using System . Text ;
7+ using System . Threading . Tasks ;
8+
9+ namespace Microsoft . AspNet . Mvc
10+ {
11+ public abstract class FileResult : ActionResult
12+ {
13+ private string _fileDownloadName ;
14+
15+ protected FileResult ( [ NotNull ] string contentType )
16+ {
17+ ContentType = contentType ;
18+ }
19+
20+ public string ContentType { get ; private set ; }
21+
22+ public string FileDownloadName
23+ {
24+ get { return _fileDownloadName ?? string . Empty ; }
25+ set { _fileDownloadName = value ; }
26+ }
27+
28+ public async override Task ExecuteResultAsync ( [ NotNull ] ActionContext context )
29+ {
30+ var response = context . HttpContext . Response ;
31+ response . ContentType = ContentType ;
32+
33+ if ( ! string . IsNullOrEmpty ( FileDownloadName ) )
34+ {
35+ // From RFC 2183, Sec. 2.3:
36+ // The sender may want to suggest a filename to be used if the entity is
37+ // detached and stored in a separate file. If the receiving MUA writes
38+ // the entity to a file, the suggested filename should be used as a
39+ // basis for the actual filename, where possible.
40+ string headerValue = ContentDispositionUtil . GetHeaderValue ( FileDownloadName ) ;
41+ context . HttpContext . Response . Headers . Set ( "Content-Disposition" , headerValue ) ;
42+ }
43+
44+ await WriteFileAsync ( response ) ;
45+ }
46+
47+ protected abstract Task WriteFileAsync ( HttpResponse response ) ;
48+
49+ internal static class ContentDispositionUtil
50+ {
51+ private const string HexDigits = "0123456789ABCDEF" ;
52+
53+ private static void AddByteToStringBuilder ( byte b , StringBuilder builder )
54+ {
55+ builder . Append ( '%' ) ;
56+
57+ int i = b ;
58+ AddHexDigitToStringBuilder ( i >> 4 , builder ) ;
59+ AddHexDigitToStringBuilder ( i % 16 , builder ) ;
60+ }
61+
62+ private static void AddHexDigitToStringBuilder ( int digit , StringBuilder builder )
63+ {
64+ builder . Append ( HexDigits [ digit ] ) ;
65+ }
66+
67+ private static string CreateRfc2231HeaderValue ( string filename )
68+ {
69+ StringBuilder builder = new StringBuilder ( "attachment; filename*=UTF-8''" ) ;
70+
71+ byte [ ] filenameBytes = Encoding . UTF8 . GetBytes ( filename ) ;
72+ foreach ( byte b in filenameBytes )
73+ {
74+ if ( IsByteValidHeaderValueCharacter ( b ) )
75+ {
76+ builder . Append ( ( char ) b ) ;
77+ }
78+ else
79+ {
80+ AddByteToStringBuilder ( b , builder ) ;
81+ }
82+ }
83+
84+ return builder . ToString ( ) ;
85+ }
86+
87+ public static string GetHeaderValue ( string fileName )
88+ {
89+ // If fileName contains any Unicode characters, encode according
90+ // to RFC 2231 (with clarifications from RFC 5987)
91+ foreach ( char c in fileName )
92+ {
93+ if ( ( int ) c > 127 )
94+ {
95+ return CreateRfc2231HeaderValue ( fileName ) ;
96+ }
97+ }
98+
99+ return CreateNonUnicodeHeaderValue ( fileName ) ;
100+ }
101+
102+ private static string CreateNonUnicodeHeaderValue ( string fileName )
103+ {
104+ string escapedFileName = EscapeFileName ( fileName ) ;
105+ return string . Format ( "attachment; filename={0}" , escapedFileName ) ;
106+ }
107+
108+ private static string EscapeFileName ( string fileName )
109+ {
110+ var hasToBeQuoted = false ;
111+ for ( int i = 0 ; i < fileName . Length ; i ++ )
112+ {
113+ if ( fileName [ i ] == '\n ' )
114+ {
115+ // See RFC 2047 for more details
116+ return GetRfc2047Base64EncodedWord ( fileName ) ;
117+ }
118+
119+ // Control characters = (octets 0 - 31) and DEL (127)
120+ if ( char . IsControl ( fileName [ i ] ) )
121+ {
122+ hasToBeQuoted = true ;
123+ }
124+
125+ switch ( fileName [ i ] )
126+ {
127+ case '(' :
128+ case ')' :
129+ case '<' :
130+ case '>' :
131+ case '@' :
132+ case ',' :
133+ case ';' :
134+ case ':' :
135+ case '\\ ' :
136+ case '/' :
137+ case '[' :
138+ case ']' :
139+ case '?' :
140+ case '=' :
141+ case '{' :
142+ case '}' :
143+ case ' ' :
144+ case '\t ' :
145+ case '"' :
146+ hasToBeQuoted = true ;
147+ break ;
148+ default :
149+ break ;
150+ }
151+ }
152+
153+ return hasToBeQuoted ? QuoteFileName ( fileName ) : fileName ;
154+ }
155+
156+ private static string QuoteFileName ( string fileName )
157+ {
158+ StringBuilder builder = new StringBuilder ( ) ;
159+ builder . Append ( "\" " ) ;
160+
161+ for ( var i = 0 ; i < fileName . Length ; i ++ )
162+ {
163+ switch ( fileName [ i ] )
164+ {
165+ case '\\ ' :
166+ builder . Append ( "\\ \\ " ) ;
167+ break ;
168+ case '"' :
169+ builder . Append ( "\\ \" " ) ;
170+ break ;
171+ default :
172+ builder . Append ( fileName [ i ] ) ;
173+ break ;
174+ }
175+ }
176+
177+ builder . Append ( "\" " ) ;
178+ return builder . ToString ( ) ;
179+ }
180+
181+ private static string GetRfc2047Base64EncodedWord ( string fileName )
182+ {
183+ // See RFC 2047 for details. Section 8 for examples.
184+ const string charset = "utf-8" ;
185+ const string encoding = "B" ;
186+
187+ var base64EncodedFileName = Convert . ToBase64String ( Encoding . UTF8 . GetBytes ( fileName ) ) ;
188+
189+ return string . Format ( "\" =?{0}?{1}?{2}?=\" " , charset , encoding , base64EncodedFileName ) ;
190+ }
191+
192+
193+ // Application of RFC 2231 Encoding to Hypertext Transfer Protocol (HTTP) Header Fields, sec. 3.2
194+ // http://greenbytes.de/tech/webdav/draft-reschke-rfc2231-in-http-latest.html
195+ private static bool IsByteValidHeaderValueCharacter ( byte b )
196+ {
197+ if ( ( byte ) '0' <= b && b <= ( byte ) '9' )
198+ {
199+ return true ; // is digit
200+ }
201+ if ( ( byte ) 'a' <= b && b <= ( byte ) 'z' )
202+ {
203+ return true ; // lowercase letter
204+ }
205+ if ( ( byte ) 'A' <= b && b <= ( byte ) 'Z' )
206+ {
207+ return true ; // uppercase letter
208+ }
209+
210+ switch ( b )
211+ {
212+ case ( byte ) '-' :
213+ case ( byte ) '.' :
214+ case ( byte ) '_' :
215+ case ( byte ) '~' :
216+ case ( byte ) ':' :
217+ case ( byte ) '!' :
218+ case ( byte ) '$' :
219+ case ( byte ) '&' :
220+ case ( byte ) '+' :
221+ return true ;
222+ }
223+
224+ return false ;
225+ }
226+ }
227+ }
228+ }
0 commit comments