package railo.runtime.tag; import java.io.BufferedInputStream; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.PrintWriter; import java.util.Enumeration; import java.util.zip.GZIPOutputStream; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import railo.commons.io.IOUtil; import railo.commons.io.res.Resource; import railo.commons.io.res.util.ResourceUtil; import railo.commons.lang.StringUtil; import railo.commons.lang.SystemOut; import railo.commons.net.HTTPUtil; import railo.runtime.PageContextImpl; import railo.runtime.exp.ApplicationException; import railo.runtime.exp.PageException; import railo.runtime.exp.PostContentAbort; import railo.runtime.exp.TemplateException; import railo.runtime.ext.tag.BodyTagImpl; import railo.runtime.net.http.ReqRspUtil; import railo.runtime.op.Caster; import railo.runtime.type.util.ListUtil; /** * Defines the MIME type returned by the current page. Optionally, lets you specify the name of a file * to be returned with the page. **/ public final class Content extends BodyTagImpl { private static final int RANGE_NONE = 0; private static final int RANGE_YES = 1; private static final int RANGE_NO = 2; /** Defines the File/ MIME content type returned by the current page. */ private String type; /** The name of the file being retrieved */ private String strFile; /** 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 ** and file attributes are mutually exclusive. If you specify a file, the reset attribute has no effect. */ private boolean reset=true; private int _range=RANGE_NONE; /** Yes or No. Yes deletes the file after the download operation. Defaults to No. ** This attribute applies only if you specify a file with the file attribute. */ private boolean deletefile=false; private byte[] content; @Override public void release() { super.release(); type=null; strFile=null; reset=true; deletefile=false; content=null; _range=RANGE_NONE; } /** set the value type * Defines the File/ MIME content type returned by the current page. * @param type value to set **/ public void setType(String type) { this.type=type.trim(); } public void setRange(boolean range) { this._range=range?RANGE_YES:RANGE_NO; } /** set the value file * The name of the file being retrieved * @param file value to set **/ public void setFile(String file) { this.strFile=file; } /** * the content to output as binary * @param content value to set * @deprecated replaced with <code>{@link #setVariable(Object)}</code> **/ public void setContent(byte[] content) { this.content=content; } public void setVariable(Object variable) throws PageException { if(variable instanceof String) this.content=Caster.toBinary(pageContext.getVariable((String)variable)); else this.content=Caster.toBinary(variable); } /** set the value reset * 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 * and file attributes are mutually exclusive. If you specify a file, the reset attribute has no effect. * @param reset value to set **/ public void setReset(boolean reset) { this.reset=reset; } /** set the value deletefile * Yes or No. Yes deletes the file after the download operation. Defaults to No. * This attribute applies only if you specify a file with the file attribute. * @param deletefile value to set **/ public void setDeletefile(boolean deletefile) { this.deletefile=deletefile; } @Override public int doStartTag() throws PageException { //try { return _doStartTag(); /*} catch (IOException e) { throw Caster.toPageException(e); }*/ } private int _doStartTag() throws PageException { // check the file before doing anyrhing else Resource file=null; if(content==null && !StringUtil.isEmpty(strFile)) file = ResourceUtil.toResourceExisting(pageContext,strFile); // get response object HttpServletResponse rsp = pageContext. getHttpServletResponse(); // check committed if(rsp.isCommitted()) throw new ApplicationException("content is already flushed","you can't rewrite head of response after part of the page is flushed"); // set type if(!StringUtil.isEmpty(type,true)) { type=type.trim(); rsp.setContentType(type); // TODO more dynamic implementation, configuration in admin? if(!HTTPUtil.isTextMimeType(type)) { ((PageContextImpl)pageContext).getRootOut().setAllowCompression(false); } } Range[] ranges=getRanges(); boolean hasRanges=ranges!=null && ranges.length>0; if(_range==RANGE_YES || hasRanges){ rsp.setHeader("Accept-Ranges", "bytes"); } else if(_range==RANGE_NO) { rsp.setHeader("Accept-Ranges", "none"); hasRanges=false; } // set content if(this.content!=null || file!=null) { pageContext.clear(); InputStream is=null; OutputStream os=null; long totalLength,contentLength; try { os=getOutputStream(); if(content!=null) { //ReqRspUtil.setContentLength(rsp,content.length); contentLength=content.length; totalLength=content.length; is=new BufferedInputStream(new ByteArrayInputStream(content)); } else { //ReqRspUtil.setContentLength(rsp,file.length()); pageContext.getConfig().getSecurityManager().checkFileLocation(file); contentLength=totalLength=file.length(); is=IOUtil.toBufferedInputStream(file.getInputStream()); } // write if(!hasRanges) IOUtil.copy(is,os,false,false); else { contentLength=0; long off,len,to; for(int i=0;i<ranges.length;i++) { off=ranges[i].from; if(ranges[i].to==-1) { len=-1; to=totalLength-1; } else { to=ranges[i].to; if(to>=totalLength)to=totalLength-1; len=to-ranges[i].from+1; } rsp.addHeader("Content-Range", "bytes "+off+"-"+to+"/"+Caster.toString(totalLength)); rsp.setStatus(206); //print.e("Content-Range: bytes "+off+"-"+to+"/"+Caster.toString(totalLength)); contentLength+=to-off+1L; // ReqRspUtil.setContentLength(rsp,len); IOUtil.copy(is, os,off,len); } } if(!(os instanceof GZIPOutputStream)) ReqRspUtil.setContentLength(rsp,contentLength); } catch(IOException ioe) {} finally { IOUtil.flushEL(os); IOUtil.closeEL(is,os); if(deletefile && file!=null) ResourceUtil.removeEL(file, true); ((PageContextImpl)pageContext).getRootOut().setClosed(true); } throw new PostContentAbort(); } // clear current content else if(reset)pageContext.clear(); return EVAL_BODY_INCLUDE;//EVAL_PAGE; } private OutputStream getOutputStream() throws PageException, IOException { try { return pageContext.getResponseStream(); } catch(IllegalStateException ise) { throw new TemplateException("content is already send to user, flush"); } } @Override public int doEndTag() { return strFile == null ? EVAL_PAGE : SKIP_PAGE; } /** * sets if tag has a body or not * @param hasBody */ public void hasBody(boolean hasBody) { } private Range[] getRanges() { HttpServletRequest req = pageContext.getHttpServletRequest(); Enumeration names = req.getHeaderNames(); if(names==null) return null; String name; Range[] range; while(names.hasMoreElements()) { name=(String) names.nextElement(); if("range".equalsIgnoreCase(name)){ range = getRanges(name,req.getHeader(name)); if(range!=null) return range; } } return null; } private Range[] getRanges(String name,String range) { if(StringUtil.isEmpty(range, true)) return null; range=StringUtil.removeWhiteSpace(range); if(range.indexOf("bytes=")==0) range=range.substring(6); String[] arr=null; try { arr = ListUtil.toStringArray(ListUtil.listToArrayRemoveEmpty(range, ',')); } catch (PageException e) { failRange(name,range); return null; } String item; int index; long from,to; Range[] ranges=new Range[arr.length]; for(int i=0;i<ranges.length;i++) { item=arr[i].trim(); index=item.indexOf('-'); if(index!=-1) { from = Caster.toLongValue(item.substring(0,index),0); to = Caster.toLongValue(item.substring(index+1),-1); if(to!=-1 && from>to){ failRange(name,range); return null; //throw new ExpressionException("invalid range definition, from have to bigger than to ("+from+"-"+to+")"); } } else { from = Caster.toLongValue(item,0); to=-1; } ranges[i]=new Range(from,to); if(i>0 && ranges[i-1].to>=from){ PrintWriter err = pageContext.getConfig().getErrWriter(); SystemOut.printDate(err,"there is a overlapping of 2 ranges ("+ranges[i-1]+","+ranges[i]+")"); //throw new ExpressionException("there is a overlapping of 2 ranges ("+ranges[i-1]+","+ranges[i]+")"); return null; } } return ranges; } private void failRange(String name, String range) { PrintWriter err = pageContext.getConfig().getErrWriter(); SystemOut.printDate(err,"failed to parse the header field ["+name+":"+range+"]"); } } class Range { long from; long to; public Range(long from, long len) { this.from = from; this.to = len; } public String toString() { return from+"-"+to; } }