    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    １行
    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      読み取られた１行分
    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}