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}