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