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                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}