001/* 002 * Copyright (c) 2009 The openGion Project. 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, 013 * either express or implied. See the License for the specific language 014 * governing permissions and limitations under the License. 015 */ 016package org.opengion.hayabusa.servlet.multipart; 017 018import org.opengion.fukurou.util.Closer ; 019 020import java.io.IOException; 021 022import java.util.List; 023import java.util.ArrayList; 024import java.util.Locale ; 025 026import javax.servlet.http.HttpServletRequest; 027import javax.servlet.ServletInputStream; 028 029/** 030 * ファイルアップロード時のマルチパート処理のパーサーです。 031 * 032 * @og.group その他機能 033 * 034 * @version 4.0 035 * @author Kazuhiko Hasegawa 036 * @since JDK5.0, 037 */ 038public class MultipartParser { 039 private final ServletInputStream in; 040 private final String boundary; 041 private FilePart lastFilePart; 042 private final byte[] buf = new byte[8 * 1024]; 043 private static final String DEFAULT_ENCODING = "MS932"; 044 private String encoding = DEFAULT_ENCODING; 045 046 /** 047 * マルチパート処理のパーサーオブジェクトを構築する、コンストラクター 048 * 049 * @og.rev 5.3.7.0 (2011/07/01) 最大容量オーバー時のエラーメッセージ変更 050 * @og.rev 5.5.2.6 (2012/05/25) maxSize で、0,またはマイナスで無制限 051 * @og.rev 5.10.31.0 (2021/04/29) boundaryの取り方修正 052 * 053 * @param req HttpServletRequestオブジェクト 054 * @param maxSize 最大容量(0,またはマイナスで無制限) 055 * @throws IOException 入出力エラーが発生したとき 056 */ 057 public MultipartParser( final HttpServletRequest req, final int maxSize ) throws IOException { 058 String type = null; 059 String type1 = req.getHeader("Content-Type"); 060 String type2 = req.getContentType(); 061 062 if(type1 == null && type2 != null) { 063 type = type2; 064 } 065 else if(type2 == null && type1 != null) { 066 type = type1; 067 } 068 069 else if(type1 != null && type2 != null) { 070 type = (type1.length() > type2.length() ? type1 : type2); 071 } 072 073 if(type == null || 074 !type.toLowerCase(Locale.JAPAN).startsWith("multipart/form-data")) { 075 throw new IOException("Posted content type isn't multipart/form-data"); 076 } 077 078 int length = req.getContentLength(); 079 // 5.5.2.6 (2012/05/25) maxSize で、0,またはマイナスで無制限 080// if(length > maxSize) { 081 if( maxSize > 0 && length > maxSize ) { 082// throw new IOException("Posted content length of " + length + 083// " exceeds limit of " + maxSize); 084 throw new IOException("登録したファイルサイズが上限(" + ( maxSize / 1024 / 1024 ) + "MB)を越えています。" 085 + " 登録ファイル=" + ( length / 1024 / 1024 ) + "MB" ); // 5.3.7.0 (2011/07/01) 086 } 087 088 // 4.0.0 (2005/01/31) The local variable "boundary" shadows an accessible field with the same name and compatible type in class org.opengion.hayabusa.servlet.multipart.MultipartParser 089 String bound = extractBoundary(type); 090 // if(bound == null) { 091 // throw new IOException("Separation boundary was not specified"); 092 // } 093 094 this.in = req.getInputStream(); 095 //this.boundary = bound; 096 097 String line = readLine(); 098 if(line == null) { 099 throw new IOException("Corrupt form data: premature ending"); 100 } 101 else if( bound == null && line.contains( "WebKitFormBoundary" ) ) { // 5.10. 102 bound = line; 103 } 104 105 if(bound == null) { 106 throw new IOException("Separation boundary was not specified"); 107 } 108 109 this.boundary = bound; 110 111 if(!line.startsWith(boundary)) { 112 throw new IOException("Corrupt form data: no leading boundary: " + 113 line + " != " + boundary); 114 } 115 } 116 117 /** 118 * エンコードを設定します。 119 * 120 * @param encoding エンコード 121 */ 122 public void setEncoding( final String encoding ) { 123 this.encoding = encoding; 124 } 125 126 /** 127 * 次のパートを読み取ります。 128 * 129 * @og.rev 3.5.6.2 (2004/07/05) 文字列の連結にStringBuilderを使用します。 130 * 131 * @return 次のパート 132 * @throws IOException 入出力エラーが発生したとき 133 */ 134 public Part readNextPart() throws IOException { 135 if(lastFilePart != null) { 136 Closer.ioClose( lastFilePart.getInputStream() ); // 4.0.0 (2006/01/31) close 処理時の IOException を無視 137 lastFilePart = null; 138 } 139 140 List<String> headers = new ArrayList<String>(); 141 142 String line = readLine(); 143 if(line == null) { 144 return null; 145 } 146 else if(line.length() == 0) { 147 return null; 148 } 149 150 while (line != null && line.length() > 0) { 151 String nextLine = null; 152 boolean getNextLine = true; 153 StringBuilder buf = new StringBuilder( 100 ); 154 buf.append( line ); 155 while (getNextLine) { 156 nextLine = readLine(); 157 if(nextLine != null 158 && (nextLine.startsWith(" ") 159 || nextLine.startsWith("\t"))) { 160 buf.append( nextLine ); 161 } 162 else { 163 getNextLine = false; 164 } 165 } 166 167 headers.add(buf.toString()); 168 line = nextLine; 169 } 170 171 if(line == null) { 172 return null; 173 } 174 175 String name = null; 176 String filename = null; 177 String origname = null; 178 String contentType = "text/plain"; 179 180 for( String headerline : headers ) { 181 if(headerline.toLowerCase(Locale.JAPAN).startsWith("content-disposition:")) { 182 String[] dispInfo = extractDispositionInfo(headerline); 183 184 name = dispInfo[1]; 185 filename = dispInfo[2]; 186 origname = dispInfo[3]; 187 } 188 else if(headerline.toLowerCase(Locale.JAPAN).startsWith("content-type:")) { 189 String type = extractContentType(headerline); 190 if(type != null) { 191 contentType = type; 192 } 193 } 194 } 195 196 if(filename == null) { 197 return new ParamPart(name, in, boundary, encoding); 198 } 199 else { 200 if( "".equals( filename ) ) { 201 filename = null; 202 } 203 lastFilePart = new FilePart(name,in,boundary,contentType,filename,origname); 204 return lastFilePart; 205 } 206 } 207 208 /** 209 * ローカル変数「境界」アクセス可能なフィールドを返します。 210 * 211 * @param line 1行 212 * 213 * @return 境界文字列 214 * @see org.opengion.hayabusa.servlet.multipart.MultipartParser 215 */ 216 private String extractBoundary( final String line ) { 217 // 4.0.0 (2005/01/31) The local variable "boundary" shadows an accessible field with the same name and compatible type in class org.opengion.hayabusa.servlet.multipart.MultipartParser 218 int index = line.lastIndexOf("boundary="); 219 if(index == -1) { 220 return null; 221 } 222 String bound = line.substring(index + 9); 223 if(bound.charAt(0) == '"') { 224 index = bound.lastIndexOf('"'); 225 bound = bound.substring(1, index); 226 } 227 228 bound = "--" + bound; 229 230 return bound; 231 } 232 233 /** 234 * コンテンツの情報を返します。 235 * 236 * @param origline 元の行 237 * 238 * @return コンテンツの情報配列 239 * @throws IOException 入出力エラーが発生したとき 240 */ 241 private String[] extractDispositionInfo( final String origline ) throws IOException { 242 String[] retval = new String[4]; 243 244 String line = origline.toLowerCase(Locale.JAPAN); 245 246 int start = line.indexOf("content-disposition: "); 247 int end = line.indexOf(';'); 248 if(start == -1 || end == -1) { 249 throw new IOException("Content disposition corrupt: " + origline); 250 } 251 String disposition = line.substring(start + 21, end); 252 if(!"form-data".equals(disposition)) { 253 throw new IOException("Invalid content disposition: " + disposition); 254 } 255 256 start = line.indexOf("name=\"", end); // start at last semicolon 257 end = line.indexOf("\"", start + 7); // skip name=\" 258 if(start == -1 || end == -1) { 259 throw new IOException("Content disposition corrupt: " + origline); 260 } 261 String name = origline.substring(start + 6, end); 262 263 String filename = null; 264 String origname = null; 265 start = line.indexOf("filename=\"", end + 2); // start after name 266 end = line.indexOf("\"", start + 10); // skip filename=\" 267 if(start != -1 && end != -1) { // note the != 268 filename = origline.substring(start + 10, end); 269 origname = filename; 270 int slash = 271 Math.max(filename.lastIndexOf('/'), filename.lastIndexOf('\\')); 272 if(slash > -1) { 273 filename = filename.substring(slash + 1); // past last slash 274 } 275 } 276 277 retval[0] = disposition; 278 retval[1] = name; 279 retval[2] = filename; 280 retval[3] = origname; 281 return retval; 282 } 283 284 /** 285 * コンテンツタイプの情報を返します。 286 * 287 * @param origline 元の行 288 * 289 * @return コンテンツタイプの情報 290 * @throws IOException 入出力エラーが発生したとき 291 */ 292 private String extractContentType( final String origline ) throws IOException { 293 String contentType = null; 294 295 String line = origline.toLowerCase(Locale.JAPAN); 296 297 if(line.startsWith("content-type")) { 298 int start = line.indexOf(' '); 299 if(start == -1) { 300 throw new IOException("Content type corrupt: " + origline); 301 } 302 contentType = line.substring(start + 1); 303 } 304 else if(line.length() > 0) { // no content type, so should be empty 305 throw new IOException("Malformed line after disposition: " + origline); 306 } 307 308 return contentType; 309 } 310 311 /** 312 * 行を読み取ります。 313 * 314 * @return 読み取られた1行分 315 * @throws IOException 入出力エラーが発生したとき 316 */ 317 private String readLine() throws IOException { 318 StringBuilder sbuf = new StringBuilder(); 319 int result; 320 321 do { 322 result = in.readLine(buf, 0, buf.length); 323 if(result != -1) { 324 sbuf.append(new String(buf, 0, result, encoding)); 325 } 326 } while (result == buf.length); 327 328 if(sbuf.length() == 0) { 329 return null; 330 } 331 332 // 4.0.0 (2005/01/31) The method StringBuilder.setLength() should be avoided in favor of creating a new StringBuilder. 333 String rtn = sbuf.toString(); 334 int len = sbuf.length(); 335 if(len >= 2 && sbuf.charAt(len - 2) == '\r') { 336 rtn = rtn.substring(0,len - 2); 337 } 338 else if(len >= 1 && sbuf.charAt(len - 1) == '\n') { 339 rtn = rtn.substring(0,len - 1); 340 } 341 return rtn ; 342 } 343}