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