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.FileOutputStream;
21 import java.io.IOException;
22 import java.io.OutputStream;
23 import java.io.UnsupportedEncodingException;
24 import java.util.ArrayList;
25 import java.util.Collections;
26 import java.util.Enumeration;
27 import java.util.Iterator;
28 import java.util.List;
29 import java.util.Map;
30 import java.util.StringTokenizer;
31
32 import javax.servlet.Filter;
33 import javax.servlet.FilterChain;
34 import javax.servlet.FilterConfig;
35 import javax.servlet.ServletContext;
36 import javax.servlet.ServletException;
37 import javax.servlet.ServletRequest;
38 import javax.servlet.ServletResponse;
39 import javax.servlet.http.HttpServletRequest;
40 import javax.servlet.http.HttpServletRequestWrapper;
41
42 import org.mortbay.util.LazyList;
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
107 MultiMap params = new MultiMap();
108 for (Iterator i = request.getParameterMap().entrySet().iterator();i.hasNext();)
109 {
110 Map.Entry entry=(Map.Entry)i.next();
111 Object value=entry.getValue();
112 if (value instanceof String[])
113 params.addValues(entry.getKey(),(String[])value);
114 else
115 params.add(entry.getKey(),value);
116 }
117
118 try
119 {
120 // Get first boundary
121 byte[] bytes=TypeUtil.readLine(in);
122 String line=bytes==null?null:new String(bytes,"UTF-8");
123 if(line==null || !line.equals(boundary))
124 {
125 throw new IOException("Missing initial multi part boundary");
126 }
127
128 // Read each part
129 boolean lastPart=false;
130 String content_disposition=null;
131 outer:while(!lastPart)
132 {
133 while(true)
134 {
135 bytes=TypeUtil.readLine(in);
136 // If blank line, end of part headers
137 if(bytes==null)
138 break outer;
139 if (bytes.length==0)
140 break;
141 line=new String(bytes,"UTF-8");
142
143 // place part header key and value in map
144 int c=line.indexOf(':',0);
145 if(c>0)
146 {
147 String key=line.substring(0,c).trim().toLowerCase();
148 String value=line.substring(c+1,line.length()).trim();
149 if(key.equals("content-disposition"))
150 content_disposition=value;
151 }
152 }
153 // Extract content-disposition
154 boolean form_data=false;
155 if(content_disposition==null)
156 {
157 throw new IOException("Missing content-disposition");
158 }
159
160 StringTokenizer tok=new StringTokenizer(content_disposition,";");
161 String name=null;
162 String filename=null;
163 while(tok.hasMoreTokens())
164 {
165 String t=tok.nextToken().trim();
166 String tl=t.toLowerCase();
167 if(t.startsWith("form-data"))
168 form_data=true;
169 else if(tl.startsWith("name="))
170 name=value(t);
171 else if(tl.startsWith("filename="))
172 filename=value(t);
173 }
174
175 // Check disposition
176 if(!form_data)
177 {
178 continue;
179 }
180
181 //It is valid for reset and submit buttons to have an empty name.
182 //If no name is supplied, the browser skips sending the info for that field.
183 //However, if you supply the empty string as the name, the browser sends the
184 //field, with name as the empty string. So, only continue this loop if we
185 //have not yet seen a name field.
186 if(name==null)
187 {
188 continue;
189 }
190
191 OutputStream out=null;
192 File file=null;
193 try
194 {
195 if (filename!=null && filename.length()>0)
196 {
197 file = File.createTempFile("MultiPart", "", tempdir);
198 out = new FileOutputStream(file);
199 if(_fileOutputBuffer>0)
200 out = new BufferedOutputStream(out, _fileOutputBuffer);
201 request.setAttribute(name,file);
202 params.add(name, filename);
203
204 if (_deleteFiles)
205 {
206 file.deleteOnExit();
207 ArrayList files = (ArrayList)request.getAttribute(FILES);
208 if (files==null)
209 {
210 files=new ArrayList();
211 request.setAttribute(FILES,files);
212 }
213 files.add(file);
214 }
215
216 }
217 else
218 out=new ByteArrayOutputStream();
219
220 int state=-2;
221 int c;
222 boolean cr=false;
223 boolean lf=false;
224
225 // loop for all lines`
226 while(true)
227 {
228 int b=0;
229 while((c=(state!=-2)?state:in.read())!=-1)
230 {
231 state=-2;
232 // look for CR and/or LF
233 if(c==13||c==10)
234 {
235 if(c==13)
236 state=in.read();
237 break;
238 }
239 // look for boundary
240 if(b>=0&&b<byteBoundary.length&&c==byteBoundary[b])
241 b++;
242 else
243 {
244 // this is not a boundary
245 if(cr)
246 out.write(13);
247 if(lf)
248 out.write(10);
249 cr=lf=false;
250 if(b>0)
251 out.write(byteBoundary,0,b);
252 b=-1;
253 out.write(c);
254 }
255 }
256 // check partial boundary
257 if((b>0&&b<byteBoundary.length-2)||(b==byteBoundary.length-1))
258 {
259 if(cr)
260 out.write(13);
261 if(lf)
262 out.write(10);
263 cr=lf=false;
264 out.write(byteBoundary,0,b);
265 b=-1;
266 }
267 // boundary match
268 if(b>0||c==-1)
269 {
270 if(b==byteBoundary.length)
271 lastPart=true;
272 if(state==10)
273 state=-2;
274 break;
275 }
276 // handle CR LF
277 if(cr)
278 out.write(13);
279 if(lf)
280 out.write(10);
281 cr=(c==13);
282 lf=(c==10||state==10);
283 if(state==10)
284 state=-2;
285 }
286 }
287 finally
288 {
289 out.close();
290 }
291
292 if (file==null)
293 {
294 bytes = ((ByteArrayOutputStream)out).toByteArray();
295 params.add(name,bytes);
296 }
297 }
298
299 // handle request
300 chain.doFilter(new Wrapper(srequest,params),response);
301 }
302 finally
303 {
304 deleteFiles(request);
305 }
306 }
307
308 private void deleteFiles(ServletRequest request)
309 {
310 ArrayList files = (ArrayList)request.getAttribute(FILES);
311 if (files!=null)
312 {
313 Iterator iter = files.iterator();
314 while (iter.hasNext())
315 {
316 File file=(File)iter.next();
317 try
318 {
319 file.delete();
320 }
321 catch(Exception e)
322 {
323 _context.log("failed to delete "+file,e);
324 }
325 }
326 }
327 }
328 /* ------------------------------------------------------------ */
329 private String value(String nameEqualsValue)
330 {
331 String value=nameEqualsValue.substring(nameEqualsValue.indexOf('=')+1).trim();
332 int i=value.indexOf(';');
333 if(i>0)
334 value=value.substring(0,i);
335 if(value.startsWith("\""))
336 {
337 value=value.substring(1,value.indexOf('"',1));
338 }
339 else
340 {
341 i=value.indexOf(' ');
342 if(i>0)
343 value=value.substring(0,i);
344 }
345 return value;
346 }
347
348 /* ------------------------------------------------------------------------------- */
349 /**
350 * @see javax.servlet.Filter#destroy()
351 */
352 public void destroy()
353 {
354 }
355
356 private static class Wrapper extends HttpServletRequestWrapper
357 {
358 String encoding="UTF-8";
359 MultiMap map;
360
361 /* ------------------------------------------------------------------------------- */
362 /** Constructor.
363 * @param request
364 */
365 public Wrapper(HttpServletRequest request, MultiMap map)
366 {
367 super(request);
368 this.map=map;
369 }
370
371 /* ------------------------------------------------------------------------------- */
372 /**
373 * @see javax.servlet.ServletRequest#getContentLength()
374 */
375 public int getContentLength()
376 {
377 return 0;
378 }
379
380 /* ------------------------------------------------------------------------------- */
381 /**
382 * @see javax.servlet.ServletRequest#getParameter(java.lang.String)
383 */
384 public String getParameter(String name)
385 {
386 Object o=map.get(name);
387 if (!(o instanceof byte[]) && LazyList.size(o)>0)
388 o=LazyList.get(o,0);
389
390 if (o instanceof byte[])
391 {
392 try
393 {
394 String s=new String((byte[])o,encoding);
395 return s;
396 }
397 catch(Exception e)
398 {
399 e.printStackTrace();
400 }
401 }
402 else if (o!=null)
403 return String.valueOf(o);
404 return null;
405 }
406
407 /* ------------------------------------------------------------------------------- */
408 /**
409 * @see javax.servlet.ServletRequest#getParameterMap()
410 */
411 public Map getParameterMap()
412 {
413 return Collections.unmodifiableMap(map.toStringArrayMap());
414 }
415
416 /* ------------------------------------------------------------------------------- */
417 /**
418 * @see javax.servlet.ServletRequest#getParameterNames()
419 */
420 public Enumeration getParameterNames()
421 {
422 return Collections.enumeration(map.keySet());
423 }
424
425 /* ------------------------------------------------------------------------------- */
426 /**
427 * @see javax.servlet.ServletRequest#getParameterValues(java.lang.String)
428 */
429 public String[] getParameterValues(String name)
430 {
431 List l=map.getValues(name);
432 if (l==null || l.size()==0)
433 return new String[0];
434 String[] v = new String[l.size()];
435 for (int i=0;i<l.size();i++)
436 {
437 Object o=l.get(i);
438 if (o instanceof byte[])
439 {
440 try
441 {
442 v[i]=new String((byte[])o,encoding);
443 }
444 catch(Exception e)
445 {
446 e.printStackTrace();
447 }
448 }
449 else if (o instanceof String)
450 v[i]=(String)o;
451 }
452 return v;
453 }
454
455 /* ------------------------------------------------------------------------------- */
456 /**
457 * @see javax.servlet.ServletRequest#setCharacterEncoding(java.lang.String)
458 */
459 public void setCharacterEncoding(String enc)
460 throws UnsupportedEncodingException
461 {
462 encoding=enc;
463 }
464 }
465 }