001/**
002 *
003 * Copyright (c) 2014, the Railo Company Ltd. All rights reserved.
004 *
005 * This library is free software; you can redistribute it and/or
006 * modify it under the terms of the GNU Lesser General Public
007 * License as published by the Free Software Foundation; either 
008 * version 2.1 of the License, or (at your option) any later version.
009 * 
010 * This library is distributed in the hope that it will be useful,
011 * but WITHOUT ANY WARRANTY; without even the implied warranty of
012 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
013 * Lesser General Public License for more details.
014 * 
015 * You should have received a copy of the GNU Lesser General Public 
016 * License along with this library.  If not, see <http://www.gnu.org/licenses/>.
017 * 
018 **/
019package lucee.runtime.tag;
020
021import java.io.BufferedInputStream;
022import java.io.ByteArrayInputStream;
023import java.io.IOException;
024import java.io.InputStream;
025import java.io.OutputStream;
026import java.io.PrintWriter;
027import java.util.Enumeration;
028import java.util.zip.GZIPOutputStream;
029
030import javax.servlet.http.HttpServletRequest;
031import javax.servlet.http.HttpServletResponse;
032
033import lucee.commons.io.IOUtil;
034import lucee.commons.io.res.Resource;
035import lucee.commons.io.res.util.ResourceUtil;
036import lucee.commons.lang.StringUtil;
037import lucee.commons.lang.SystemOut;
038import lucee.commons.net.HTTPUtil;
039import lucee.runtime.PageContextImpl;
040import lucee.runtime.exp.ApplicationException;
041import lucee.runtime.exp.PageException;
042import lucee.runtime.exp.PostContentAbort;
043import lucee.runtime.exp.TemplateException;
044import lucee.runtime.ext.tag.BodyTagImpl;
045import lucee.runtime.net.http.ReqRspUtil;
046import lucee.runtime.op.Caster;
047import lucee.runtime.type.util.ListUtil;
048
049/**
050* Defines the MIME type returned by the current page. Optionally, lets you specify the name of a file
051*   to be returned with the page.
052**/
053public final class Content extends BodyTagImpl {
054
055        private static final int RANGE_NONE = 0;
056        private static final int RANGE_YES = 1;
057        private static final int RANGE_NO = 2;
058
059        /** Defines the File/ MIME content type returned by the current page. */
060        private String type;
061
062        /** The name of the file being retrieved */
063        private String strFile;
064
065        /** Yes or No. Yes discards output that precedes the call to cfcontent. No preserves the output that precedes the call. Defaults to Yes. The reset 
066        **              and file attributes are mutually exclusive. If you specify a file, the reset attribute has no effect. */
067        private boolean reset=true;
068        
069        private int _range=RANGE_NONE;
070
071        /** Yes or No. Yes deletes the file after the download operation. Defaults to No. 
072        **      This attribute applies only if you specify a file with the file attribute. */
073        private boolean deletefile=false;
074
075    private byte[] content;
076
077
078    @Override
079    public void release()   {
080        super.release();
081        type=null;
082        strFile=null;
083        reset=true;
084        deletefile=false;
085        content=null;
086        _range=RANGE_NONE;
087    }
088
089        /** set the value type
090        *  Defines the File/ MIME content type returned by the current page.
091        * @param type value to set
092        **/
093        public void setType(String type)        {
094                this.type=type.trim();
095        }
096        
097        public void setRange(boolean range)     {
098                this._range=range?RANGE_YES:RANGE_NO;
099        }
100
101    /** set the value file
102    *  The name of the file being retrieved
103    * @param file value to set
104    **/
105    public void setFile(String file)    {
106        this.strFile=file;
107    }
108
109    /** 
110    * the content to output as binary
111    * @param content value to set
112    * @deprecated replaced with <code>{@link #setVariable(Object)}</code>
113    **/
114    public void setContent(byte[] content)    {
115        this.content=content;
116    }
117    
118    public void setVariable(Object variable) throws PageException    {
119        if(variable instanceof String)
120                this.content=Caster.toBinary(pageContext.getVariable((String)variable));
121        else
122                this.content=Caster.toBinary(variable);
123    }
124
125        /** set the value reset
126        *  Yes or No. Yes discards output that precedes the call to cfcontent. No preserves the output that precedes the call. Defaults to Yes. The reset 
127        *               and file attributes are mutually exclusive. If you specify a file, the reset attribute has no effect.
128        * @param reset value to set
129        **/
130        public void setReset(boolean reset)     {
131                this.reset=reset;
132        }
133
134        /** set the value deletefile
135        *  Yes or No. Yes deletes the file after the download operation. Defaults to No. 
136        *       This attribute applies only if you specify a file with the file attribute.
137        * @param deletefile value to set
138        **/
139        public void setDeletefile(boolean deletefile)   {
140                this.deletefile=deletefile;
141        }
142
143
144        @Override
145    public int doStartTag() throws PageException   {
146        //try {
147            return _doStartTag();
148        /*} 
149        catch (IOException e) {
150            throw Caster.toPageException(e);
151        }*/
152    }
153
154
155    private int _doStartTag() throws PageException   {
156        // check the file before doing anyrhing else
157        Resource file=null;
158                if(content==null && !StringUtil.isEmpty(strFile)) 
159                file = ResourceUtil.toResourceExisting(pageContext,strFile);
160
161                // get response object
162                HttpServletResponse rsp = pageContext. getHttpServletResponse();
163            
164        // check committed
165        if(rsp.isCommitted())
166            throw new ApplicationException("content is already flushed","you can't rewrite head of response after part of the page is flushed");
167        
168        // set type
169        if(!StringUtil.isEmpty(type,true)) {
170                type=type.trim();
171                rsp.setContentType(type);
172                
173                // TODO more dynamic implementation, configuration in admin?
174                if(!HTTPUtil.isTextMimeType(type)) {
175                        ((PageContextImpl)pageContext).getRootOut().setAllowCompression(false);
176                }
177        }
178        
179        Range[] ranges=getRanges();
180        boolean hasRanges=ranges!=null && ranges.length>0;
181        if(_range==RANGE_YES || hasRanges){
182            rsp.setHeader("Accept-Ranges", "bytes");
183        }
184        else if(_range==RANGE_NO) {
185            rsp.setHeader("Accept-Ranges", "none");
186            hasRanges=false;
187        }
188        
189        // set content
190        if(this.content!=null || file!=null) {
191            pageContext.clear();
192                InputStream is=null;
193            OutputStream os=null;
194            long totalLength,contentLength;
195            try {
196                os=getOutputStream();
197                
198                if(content!=null) {
199                        //ReqRspUtil.setContentLength(rsp,content.length);
200                        contentLength=content.length;
201                        totalLength=content.length;
202                     is=new BufferedInputStream(new ByteArrayInputStream(content));  
203                }
204                else {
205                    //ReqRspUtil.setContentLength(rsp,file.length());
206                    pageContext.getConfig().getSecurityManager().checkFileLocation(file);
207                    contentLength=totalLength=file.length();
208                    is=IOUtil.toBufferedInputStream(file.getInputStream());
209                }
210                
211                // write
212                if(!hasRanges)
213                        IOUtil.copy(is,os,false,false);
214                else {
215                        contentLength=0;
216                        long off,len,to;
217                        for(int i=0;i<ranges.length;i++) {
218                                off=ranges[i].from;
219                                if(ranges[i].to==-1) {
220                                        len=-1;
221                                        to=totalLength-1;
222                                }
223                                else {
224                                        to=ranges[i].to;
225                                        if(to>=totalLength)to=totalLength-1;
226                                        len=to-ranges[i].from+1;
227                                }
228                                rsp.addHeader("Content-Range", "bytes "+off+"-"+to+"/"+Caster.toString(totalLength));
229                                rsp.setStatus(206);
230                                //print.e("Content-Range: bytes "+off+"-"+to+"/"+Caster.toString(totalLength));
231                                contentLength+=to-off+1L;
232                        // ReqRspUtil.setContentLength(rsp,len);
233                                IOUtil.copy(is, os,off,len);
234                        }
235                }
236                if(!(os instanceof GZIPOutputStream))
237                        ReqRspUtil.setContentLength(rsp,contentLength);
238            } 
239            catch(IOException ioe) {}
240            finally {
241                IOUtil.flushEL(os);
242                IOUtil.closeEL(is,os);
243                if(deletefile && file!=null) ResourceUtil.removeEL(file, true);
244                ((PageContextImpl)pageContext).getRootOut().setClosed(true);
245            }
246            throw new PostContentAbort();
247        }
248        // clear current content
249        else if(reset)pageContext.clear();
250        
251        return EVAL_BODY_INCLUDE;//EVAL_PAGE;
252        }
253
254        private OutputStream getOutputStream() throws PageException, IOException {
255        try {
256                return pageContext.getResponseStream();
257        } 
258        catch(IllegalStateException ise) {
259            throw new TemplateException("content is already send to user, flush");
260        }
261    }
262
263    @Override
264        public int doEndTag()   {
265                return strFile == null ? EVAL_PAGE : SKIP_PAGE;
266        }
267
268    /**
269     * sets if tag has a body or not
270     * @param hasBody
271     */
272    public void hasBody(boolean hasBody) {
273    }
274
275
276        private Range[] getRanges() {
277                HttpServletRequest req = pageContext.getHttpServletRequest();
278                Enumeration names = req.getHeaderNames();
279                
280                if(names==null) return null;
281                String name;
282                Range[] range;
283                while(names.hasMoreElements()) {
284                        name=(String) names.nextElement();
285                        
286                        if("range".equalsIgnoreCase(name)){
287                                range = getRanges(name,req.getHeader(name));
288                                if(range!=null) return range;
289                        }
290                }
291                return null;
292        }
293
294        private Range[] getRanges(String name,String range) {
295                if(StringUtil.isEmpty(range, true)) return null;
296                range=StringUtil.removeWhiteSpace(range);
297                if(range.indexOf("bytes=")==0) range=range.substring(6);
298                String[] arr=null;
299                try {
300                        arr = ListUtil.toStringArray(ListUtil.listToArrayRemoveEmpty(range, ','));
301                } catch (PageException e) {
302                        failRange(name,range);
303                        return null;
304                }
305                String item;
306                int index;
307                long from,to;
308                
309                Range[] ranges=new Range[arr.length];
310                for(int i=0;i<ranges.length;i++) {
311                        item=arr[i].trim();
312                        index=item.indexOf('-');
313                        if(index!=-1) {
314                                from = Caster.toLongValue(item.substring(0,index),0);
315                                to = Caster.toLongValue(item.substring(index+1),-1);
316                                if(to!=-1 && from>to){
317                                        failRange(name,range);
318                                        return null;
319                                        //throw new ExpressionException("invalid range definition, from have to bigger than to ("+from+"-"+to+")");
320                                }
321                        }
322                        else {
323                                from = Caster.toLongValue(item,0);
324                                to=-1;
325                        }
326                        ranges[i]=new Range(from,to);
327                        
328                        if(i>0 && ranges[i-1].to>=from){
329                                PrintWriter err = pageContext.getConfig().getErrWriter();
330                                SystemOut.printDate(err,"there is a overlapping of 2 ranges ("+ranges[i-1]+","+ranges[i]+")");
331                                //throw new ExpressionException("there is a overlapping of 2 ranges ("+ranges[i-1]+","+ranges[i]+")");
332                                return null;
333                        }
334                        
335                }
336                return ranges;
337        }
338
339        private void failRange(String name, String range) {
340                PrintWriter err = pageContext.getConfig().getErrWriter();
341                SystemOut.printDate(err,"failed to parse the header field ["+name+":"+range+"]");
342        }
343}
344
345class Range {
346
347        long from;
348        long to;
349
350        public Range(long from, long len) {
351                this.from = from;
352                this.to = len;
353        }
354
355        public String toString() {
356                return from+"-"+to;
357        }
358}