1 // ========================================================================
2 // Copyright 1996-2005 Mort Bay Consulting Pty. Ltd.
3 // ------------------------------------------------------------------------
4 // Licensed under the Apache License, Version 2.0 (the "License");
5 // you may not use this file except in compliance with the License.
6 // You may obtain a copy of the License at
7 // http://www.apache.org/licenses/LICENSE-2.0
8 // Unless required by applicable law or agreed to in writing, software
9 // distributed under the License is distributed on an "AS IS" BASIS,
10 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11 // See the License for the specific language governing permissions and
12 // limitations under the License.
13 // ========================================================================
14 package org.mortbay.servlet;
15
16 import java.io.BufferedInputStream;
17 import java.io.BufferedOutputStream;
18 import java.io.ByteArrayOutputStream;
19 import java.io.File;
20 import java.io.FileNotFoundException;
21 import java.io.FileOutputStream;
22 import java.io.IOException;
23 import java.io.OutputStream;
24 import java.io.UnsupportedEncodingException;
25 import java.util.ArrayList;
26 import java.util.Collections;
27 import java.util.Enumeration;
28 import java.util.Iterator;
29 import java.util.List;
30 import java.util.Map;
31 import java.util.StringTokenizer;
32
33 import javax.servlet.Filter;
34 import javax.servlet.FilterChain;
35 import javax.servlet.FilterConfig;
36 import javax.servlet.ServletContext;
37 import javax.servlet.ServletException;
38 import javax.servlet.ServletRequest;
39 import javax.servlet.ServletResponse;
40 import javax.servlet.http.HttpServletRequest;
41 import javax.servlet.http.HttpServletRequestWrapper;
42
43 import org.mortbay.util.MultiMap;
44 import org.mortbay.util.StringUtil;
45 import org.mortbay.util.TypeUtil;
46
47 /* ------------------------------------------------------------ */
48 /**
49 * Multipart Form Data Filter.
50 * <p>
51 * This class decodes the multipart/form-data stream sent by a HTML form that uses a file input
52 * item. Any files sent are stored to a tempary file and a File object added to the request
53 * as an attribute. All other values are made available via the normal getParameter API and
54 * the setCharacterEncoding mechanism is respected when converting bytes to Strings.
55 *
56 * If the init paramter "delete" is set to "true", any files created will be deleted when the
57 * current request returns.
58 *
59 * @author Greg Wilkins
60 * @author Jim Crossley
61 */
62 public class MultiPartFilter implements Filter
63 {
64 private final static String FILES ="org.mortbay.servlet.MultiPartFilter.files";
65 private File tempdir;
66 private boolean _deleteFiles;
67 private ServletContext _context;
68 private int _fileOutputBuffer = 0;
69
70 /* ------------------------------------------------------------------------------- */
71 /**
72 * @see javax.servlet.Filter#init(javax.servlet.FilterConfig)
73 */
74 public void init(FilterConfig filterConfig) throws ServletException
75 {
76 tempdir=(File)filterConfig.getServletContext().getAttribute("javax.servlet.context.tempdir");
77 _deleteFiles="true".equals(filterConfig.getInitParameter("deleteFiles"));
78 String fileOutputBuffer = filterConfig.getInitParameter("fileOutputBuffer");
79 if(fileOutputBuffer!=null)
80 _fileOutputBuffer = Integer.parseInt(fileOutputBuffer);
81 _context=filterConfig.getServletContext();
82 }
83
84 /* ------------------------------------------------------------------------------- */
85 /**
86 * @see javax.servlet.Filter#doFilter(javax.servlet.ServletRequest,
87 * javax.servlet.ServletResponse, javax.servlet.FilterChain)
88 */
89 public void doFilter(ServletRequest request,ServletResponse response,FilterChain chain)
90 throws IOException, ServletException
91 {
92 HttpServletRequest srequest=(HttpServletRequest)request;
93 if(srequest.getContentType()==null||!srequest.getContentType().startsWith("multipart/form-data"))
94 {
95 chain.doFilter(request,response);
96 return;
97 }
98
99 BufferedInputStream in = new BufferedInputStream(request.getInputStream());
100 String content_type=srequest.getContentType();
101
102 // TODO - handle encodings
103
104 String boundary="--"+value(content_type.substring(content_type.indexOf("boundary=")));
105 byte[] byteBoundary=(boundary+"--").getBytes(StringUtil.__ISO_8859_1);
106 // cross-container
107 MultiMap params = new MultiMap(request.getParameterMap());
108
109 // jetty-specific but more efficient
110 /*MultiMap params = new MultiMap();
111 if(srequest instanceof org.mortbay.jetty.Request)
112 {
113 org.mortbay.jetty.Request req = ((org.mortbay.jetty.Request)srequest);
114 req.getUri().decodeQueryTo(params, req.getQueryEncoding());
115 }*/
116
117 try
118 {
119 // Get first boundary
120 byte[] bytes=TypeUtil.readLine(in);
121 String line=bytes==null?null:new String(bytes,"UTF-8");
122 if(line==null || !line.equals(boundary))
123 {
124 throw new IOException("Missing initial multi part boundary");
125 }
126
127 // Read each part
128 boolean lastPart=false;
129 String content_disposition=null;
130 while(!lastPart)
131 {
132 while(true)
133 {
134 bytes=TypeUtil.readLine(in);
135 // If blank line, end of part headers
136 if(bytes==null || bytes.length==0)
137 break;
138 line=new String(bytes,"UTF-8");
139
140 // place part header key and value in map
141 int c=line.indexOf(':',0);
142 if(c>0)
143 {
144 String key=line.substring(0,c).trim().toLowerCase();
145 String value=line.substring(c+1,line.length()).trim();
146 if(key.equals("content-disposition"))
147 content_disposition=value;
148 }
149 }
150 // Extract content-disposition
151 boolean form_data=false;
152 if(content_disposition==null)
153 {
154 throw new IOException("Missing content-disposition");
155 }
156
157 StringTokenizer tok=new StringTokenizer(content_disposition,";");
158 String name=null;
159 String filename=null;
160 while(tok.hasMoreTokens())
161 {
162 String t=tok.nextToken().trim();
163 String tl=t.toLowerCase();
164 if(t.startsWith("form-data"))
165 form_data=true;
166 else if(tl.startsWith("name="))
167 name=value(t);
168 else if(tl.startsWith("filename="))
169 filename=value(t);
170 }
171
172 // Check disposition
173 if(!form_data)
174 {
175 continue;
176 }
177
178 //It is valid for reset and submit buttons to have an empty name.
179 //If no name is supplied, the browser skips sending the info for that field.
180 //However, if you supply the empty string as the name, the browser sends the
181 //field, with name as the empty string. So, only continue this loop if we
182 //have not yet seen a name field.
183 if(name==null)
184 {
185 continue;
186 }
187
188 OutputStream out=null;
189 File file=null;
190 try
191 {
192 if (filename!=null && filename.length()>0)
193 {
194 file = File.createTempFile("MultiPart", "", tempdir);
195 out = new FileOutputStream(file);
196 if(_fileOutputBuffer>0)
197 out = new BufferedOutputStream(out, _fileOutputBuffer);
198 request.setAttribute(name,file);
199 params.put(name, filename);
200
201 if (_deleteFiles)
202 {
203 file.deleteOnExit();
204 ArrayList files = (ArrayList)request.getAttribute(FILES);
205 if (files==null)
206 {
207 files=new ArrayList();
208 request.setAttribute(FILES,files);
209 }
210 files.add(file);
211 }
212
213 }
214 else
215 out=new ByteArrayOutputStream();
216
217 int state=-2;
218 int c;
219 boolean cr=false;
220 boolean lf=false;
221
222 // loop for all lines`
223 while(true)
224 {
225 int b=0;
226 while((c=(state!=-2)?state:in.read())!=-1)
227 {
228 state=-2;
229 // look for CR and/or LF
230 if(c==13||c==10)
231 {
232 if(c==13)
233 state=in.read();
234 break;
235 }
236 // look for boundary
237 if(b>=0&&b<byteBoundary.length&&c==byteBoundary[b])
238 b++;
239 else
240 {
241 // this is not a boundary
242 if(cr)
243 out.write(13);
244 if(lf)
245 out.write(10);
246 cr=lf=false;
247 if(b>0)
248 out.write(byteBoundary,0,b);
249 b=-1;
250 out.write(c);
251 }
252 }
253 // check partial boundary
254 if((b>0&&b<byteBoundary.length-2)||(b==byteBoundary.length-1))
255 {
256 if(cr)
257 out.write(13);
258 if(lf)
259 out.write(10);
260 cr=lf=false;
261 out.write(byteBoundary,0,b);
262 b=-1;
263 }
264 // boundary match
265 if(b>0||c==-1)
266 {
267 if(b==byteBoundary.length)
268 lastPart=true;
269 if(state==10)
270 state=-2;
271 break;
272 }
273 // handle CR LF
274 if(cr)
275 out.write(13);
276 if(lf)
277 out.write(10);
278 cr=(c==13);
279 lf=(c==10||state==10);
280 if(state==10)
281 state=-2;
282 }
283 }
284 finally
285 {
286 out.close();
287 }
288
289 if (file==null)
290 {
291 bytes = ((ByteArrayOutputStream)out).toByteArray();
292 params.add(name,bytes);
293 }
294 }
295
296 // handle request
297 chain.doFilter(new Wrapper(srequest,params),response);
298 }
299 finally
300 {
301 deleteFiles(request);
302 }
303 }
304
305 private void deleteFiles(ServletRequest request)
306 {
307 ArrayList files = (ArrayList)request.getAttribute(FILES);
308 if (files!=null)
309 {
310 Iterator iter = files.iterator();
311 while (iter.hasNext())
312 {
313 File file=(File)iter.next();
314 try
315 {
316 file.delete();
317 }
318 catch(Exception e)
319 {
320 _context.log("failed to delete "+file,e);
321 }
322 }
323 }
324 }
325 /* ------------------------------------------------------------ */
326 private String value(String nameEqualsValue)
327 {
328 String value=nameEqualsValue.substring(nameEqualsValue.indexOf('=')+1).trim();
329 int i=value.indexOf(';');
330 if(i>0)
331 value=value.substring(0,i);
332 if(value.startsWith("\""))
333 {
334 value=value.substring(1,value.indexOf('"',1));
335 }
336 else
337 {
338 i=value.indexOf(' ');
339 if(i>0)
340 value=value.substring(0,i);
341 }
342 return value;
343 }
344
345 /* ------------------------------------------------------------------------------- */
346 /**
347 * @see javax.servlet.Filter#destroy()
348 */
349 public void destroy()
350 {
351 }
352
353 private static class Wrapper extends HttpServletRequestWrapper
354 {
355 String encoding="UTF-8";
356 MultiMap map;
357
358 /* ------------------------------------------------------------------------------- */
359 /** Constructor.
360 * @param request
361 */
362 public Wrapper(HttpServletRequest request, MultiMap map)
363 {
364 super(request);
365 this.map=map;
366 }
367
368 /* ------------------------------------------------------------------------------- */
369 /**
370 * @see javax.servlet.ServletRequest#getContentLength()
371 */
372 public int getContentLength()
373 {
374 return 0;
375 }
376
377 /* ------------------------------------------------------------------------------- */
378 /**
379 * @see javax.servlet.ServletRequest#getParameter(java.lang.String)
380 */
381 public String getParameter(String name)
382 {
383 Object o=map.get(name);
384 if (o instanceof byte[])
385 {
386 try
387 {
388 String s=new String((byte[])o,encoding);
389 return s;
390 }
391 catch(Exception e)
392 {
393 e.printStackTrace();
394 }
395 }
396 else if (o instanceof String)
397 return (String)o;
398 else if (o instanceof String[])
399 {
400 String[] s = (String[])o;
401 return s.length>0 ? s[0] : null;
402 }
403 return null;
404 }
405
406 /* ------------------------------------------------------------------------------- */
407 /**
408 * @see javax.servlet.ServletRequest#getParameterMap()
409 */
410 public Map getParameterMap()
411 {
412 return map;
413 }
414
415 /* ------------------------------------------------------------------------------- */
416 /**
417 * @see javax.servlet.ServletRequest#getParameterNames()
418 */
419 public Enumeration getParameterNames()
420 {
421 return Collections.enumeration(map.keySet());
422 }
423
424 /* ------------------------------------------------------------------------------- */
425 /**
426 * @see javax.servlet.ServletRequest#getParameterValues(java.lang.String)
427 */
428 public String[] getParameterValues(String name)
429 {
430 List l=map.getValues(name);
431 if (l==null || l.size()==0)
432 return new String[0];
433 String[] v = new String[l.size()];
434 for (int i=0;i<l.size();i++)
435 {
436 Object o=l.get(i);
437 if (o instanceof byte[])
438 {
439 try
440 {
441 v[i]=new String((byte[])o,encoding);
442 }
443 catch(Exception e)
444 {
445 e.printStackTrace();
446 }
447 }
448 else if (o instanceof String)
449 v[i]=(String)o;
450 }
451 return v;
452 }
453
454 /* ------------------------------------------------------------------------------- */
455 /**
456 * @see javax.servlet.ServletRequest#setCharacterEncoding(java.lang.String)
457 */
458 public void setCharacterEncoding(String enc)
459 throws UnsupportedEncodingException
460 {
461 encoding=enc;
462 }
463 }
464 }