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}