001 package railo.runtime.writer; 002 003 import java.io.IOException; 004 005 import javax.servlet.http.HttpServletRequest; 006 import javax.servlet.http.HttpServletResponse; 007 008 /** 009 * JSP Writer that Remove WhiteSpace from given content while preserving pre-formatted spaces 010 * in Tags like <CODE> <PRE> and <TEXTAREA> 011 */ 012 public final class CFMLWriterWSPref extends CFMLWriterImpl implements WhiteSpaceWriter { 013 014 public static final char CHAR_NL = '\n'; 015 public static final char CHAR_RETURN = '\r'; 016 public static final char CHAR_SPACE = ' '; 017 018 private static final char CHAR_GT = '>'; 019 private static final char CHAR_LT = '<'; 020 private static final char CHAR_SL = '/'; 021 private static final String[] EXCLUDE_TAGS = { "code", "pre", "textarea" }; 022 023 private static int minTagLen = 64; 024 025 private static final boolean doChangeWsToSpace = true; // can help fight XSS attacks with characters like Vertical Tab 026 027 private int[] depths; 028 private int depthSum = 0; 029 private char lastChar = 0; 030 private boolean isFirstChar = true; 031 private StringBuilder sb = new StringBuilder(); 032 033 034 static { 035 036 // TODO: set EXCLUDE_TAGS (and perhaps doChangeWsToSpace) to values from WebConfigImpl 037 038 for ( String s : EXCLUDE_TAGS ) 039 if ( s.length() < minTagLen ) 040 minTagLen = s.length(); 041 042 minTagLen++; // add 1 for LessThan symbol 043 } 044 045 046 /** 047 * constructor of the class 048 * @param rsp 049 * @param bufferSize 050 * @param autoFlush 051 */ 052 public CFMLWriterWSPref(HttpServletRequest req, HttpServletResponse rsp, int bufferSize, boolean autoFlush, boolean closeConn, 053 boolean showVersion, boolean contentLength, boolean allowCompression) { 054 super(req,rsp, bufferSize, autoFlush,closeConn,showVersion,contentLength,allowCompression); 055 depths = new int[ EXCLUDE_TAGS.length ]; 056 } 057 058 059 /** 060 * prints the characters from the buffer and resets it 061 * 062 * TODO: make sure that printBuffer() is called at the end of the stream in case we have some characters there! (flush() ?) 063 */ 064 synchronized void printBuffer() throws IOException { // TODO: is synchronized really needed here? 065 int len = sb.length(); 066 if ( len > 0 ) { 067 char[] chars = new char[ len ]; 068 sb.getChars( 0, len, chars, 0 ); 069 sb.setLength( 0 ); 070 super.write( chars, 0, chars.length ); 071 } 072 } 073 074 void printBufferEL() { 075 if( sb.length() > 0 ) { 076 try { 077 printBuffer(); 078 } 079 catch (IOException e) {} 080 } 081 } 082 083 /** 084 * checks if a character is part of an open html tag or close html tag, and if so adds it to the buffer, otherwise returns false. 085 * 086 * @param c 087 * @return true if the char was added to the buffer, false otherwise 088 */ 089 boolean addToBuffer( char c ) throws IOException { 090 int len = sb.length(); 091 if ( len == 0 && c != CHAR_LT ) 092 return false; // buffer must starts with '<' 093 094 sb.append( c ); // if we reached this point then we will return true 095 if ( ++len >= minTagLen ) { // increment len as it was sampled before we appended c 096 boolean isClosingTag = ( len >= 2 && sb.charAt( 1 ) == CHAR_SL ); 097 String substr; 098 if ( isClosingTag ) 099 substr = sb.substring( 2 ); // we know that the 1st two chars are "</" 100 else 101 substr = sb.substring( 1 ); // we know that the 1st char is "<" 102 for ( int i=0; i<EXCLUDE_TAGS.length; i++ ) { // loop thru list of WS-preserving tags 103 if ( substr.equalsIgnoreCase( EXCLUDE_TAGS[ i ] ) ) { // we have a match 104 if ( isClosingTag ) { 105 depthDec( i ); // decrement the depth at i and calc depthSum 106 printBuffer(); 107 lastChar = 0; // needed to allow WS after buffer was printed 108 } else { 109 depthInc( i ); // increment the depth at i and calc depthSum 110 } 111 } 112 } 113 } 114 return true; 115 } 116 117 /** 118 * decrement the depth at index and calc the new depthSum 119 * @param index 120 */ 121 private void depthDec( int index ) { 122 if ( --depths[ index ] < 0 ) 123 depths[ index ] = 0; 124 depthCalc(); 125 } 126 127 /** 128 * increment the depth at index and calc the new depthSum 129 * @param index 130 */ 131 private void depthInc( int index ) { 132 depths[ index ]++; 133 depthCalc(); 134 } 135 136 /** 137 * calc the new depthSum 138 */ 139 private void depthCalc() { 140 int sum = 0; 141 for ( int d : depths ) 142 sum += d; 143 depthSum = sum; 144 } 145 146 147 /** 148 * sends a character to output stream if it is not a consecutive white-space unless we're inside a PRE or TEXTAREA tag. 149 * 150 * @param c 151 * @throws IOException 152 */ 153 @Override 154 public void print( char c ) throws IOException { 155 boolean isWS = Character.isWhitespace( c ); 156 if ( isWS ) { 157 if ( isFirstChar ) // ignore all WS before non-WS content 158 return; 159 if ( c == CHAR_RETURN ) // ignore Carriage-Return chars 160 return; 161 if ( sb.length() > 0 ) { 162 printBuffer(); // buffer should never contain WS so flush it 163 lastChar = c == CHAR_NL ? CHAR_NL : ( doChangeWsToSpace ? CHAR_SPACE : c ); 164 super.print( lastChar ); 165 return; 166 } 167 } 168 169 isFirstChar = false; 170 if ( c == CHAR_GT && sb.length() > 0 ) 171 printBuffer(); // buffer should never contain ">" so flush it 172 173 if ( isWS || !addToBuffer( c ) ) { 174 if ( depthSum == 0 ) { // we're not in a WS-preserving tag; suppress whitespace 175 if ( isWS ) { // this char is WS 176 if ( lastChar == CHAR_NL ) // lastChar was NL; discard this WS char 177 return; 178 if ( c != CHAR_NL ) { // this WS char is not NL 179 if ( Character.isWhitespace( lastChar ) ) 180 return; // lastChar was WS but Not NL; discard this WS char 181 if ( doChangeWsToSpace ) 182 c = CHAR_SPACE; // this char is WS and not NL; change it to a regular space 183 } 184 } 185 } 186 lastChar = c; // remember c as lastChar and write it to output stream 187 super.print( c ); 188 } 189 } 190 191 192 /** 193 * @see railo.runtime.writer.CFMLWriter#writeRaw(java.lang.String) 194 */ 195 public void writeRaw(String str) throws IOException { 196 printBuffer(); 197 super.write(str); 198 } 199 200 /** 201 * just a wrapper function for ACF 202 * @throws IOException 203 */ 204 public void initHeaderBuffer() throws IOException{ 205 resetHTMLHead(); 206 } 207 208 209 210 211 212 213 214 215 216 217 218 219 /** 220 * @see railo.runtime.writer.CFMLWriterImpl#clear() 221 */ 222 public final void clear() throws IOException { 223 printBuffer(); 224 super.clear(); 225 } 226 227 /** 228 * @see railo.runtime.writer.CFMLWriterImpl#clearBuffer() 229 */ 230 public final void clearBuffer() { 231 printBufferEL(); 232 super.clearBuffer(); 233 } 234 235 /** 236 * @see railo.runtime.writer.CFMLWriterImpl#close() 237 */ 238 public final void close() throws IOException { 239 printBuffer(); 240 super.close(); 241 } 242 243 /** 244 * @see railo.runtime.writer.CFMLWriterImpl#flush() 245 */ 246 public final void flush() throws IOException { 247 printBuffer(); 248 super.flush(); 249 } 250 251 /** 252 * @see railo.runtime.writer.CFMLWriterImpl#getRemaining() 253 */ 254 public final int getRemaining() { 255 printBufferEL(); 256 return super.getRemaining(); 257 } 258 259 /** 260 * @see railo.runtime.writer.CFMLWriterImpl#newLine() 261 */ 262 public final void newLine() throws IOException { 263 print(CHAR_NL); 264 } 265 266 /** 267 * @see railo.runtime.writer.CFMLWriterImpl#print(boolean) 268 */ 269 public final void print(boolean b) throws IOException { 270 printBuffer(); 271 super.print(b); 272 } 273 274 /** 275 * @see railo.runtime.writer.CFMLWriterImpl#print(char[]) 276 */ 277 public final void print(char[] chars) throws IOException { 278 write(chars,0,chars.length); 279 } 280 281 /** 282 * @see railo.runtime.writer.CFMLWriterImpl#print(double) 283 */ 284 public final void print(double d) throws IOException { 285 printBuffer(); 286 super.print(d); 287 } 288 289 /** 290 * @see railo.runtime.writer.CFMLWriterImpl#print(float) 291 */ 292 public final void print(float f) throws IOException { 293 printBuffer(); 294 super.print(f); 295 } 296 297 /** 298 * @see railo.runtime.writer.CFMLWriterImpl#print(int) 299 */ 300 public final void print(int i) throws IOException { 301 printBuffer(); 302 super.print(i); 303 } 304 305 /** 306 * @see railo.runtime.writer.CFMLWriterImpl#print(long) 307 */ 308 public final void print(long l) throws IOException { 309 printBuffer(); 310 super.print(l); 311 } 312 313 /** 314 * @see railo.runtime.writer.CFMLWriterImpl#print(java.lang.Object) 315 */ 316 public final void print(Object obj) throws IOException { 317 print(obj.toString()); 318 } 319 320 /** 321 * @see railo.runtime.writer.CFMLWriterImpl#print(java.lang.String) 322 */ 323 public final void print(String str) throws IOException { 324 write(str.toCharArray(),0,str.length()); 325 } 326 327 /** 328 * @see railo.runtime.writer.CFMLWriterImpl#println() 329 */ 330 public final void println() throws IOException { 331 print(CHAR_NL); 332 } 333 334 /** 335 * @see railo.runtime.writer.CFMLWriterImpl#println(boolean) 336 */ 337 public final void println(boolean b) throws IOException { 338 printBuffer(); 339 super.print(b); 340 print(CHAR_NL); 341 } 342 343 /** 344 * @see railo.runtime.writer.CFMLWriterImpl#println(char) 345 */ 346 public final void println(char c) throws IOException { 347 print(c); 348 print(CHAR_NL); 349 } 350 351 /** 352 * @see railo.runtime.writer.CFMLWriterImpl#println(char[]) 353 */ 354 public final void println(char[] chars) throws IOException { 355 write(chars,0,chars.length); 356 print(CHAR_NL); 357 } 358 359 /** 360 * @see railo.runtime.writer.CFMLWriterImpl#println(double) 361 */ 362 public final void println(double d) throws IOException { 363 printBuffer(); 364 super.print(d); 365 print(CHAR_NL); 366 } 367 368 /** 369 * @see railo.runtime.writer.CFMLWriterImpl#println(float) 370 */ 371 public final void println(float f) throws IOException { 372 printBuffer(); 373 super.print(f); 374 print(CHAR_NL); 375 } 376 377 /** 378 * @see railo.runtime.writer.CFMLWriterImpl#println(int) 379 */ 380 public final void println(int i) throws IOException { 381 printBuffer(); 382 super.print(i); 383 print(CHAR_NL); 384 } 385 386 /** 387 * @see railo.runtime.writer.CFMLWriterImpl#println(long) 388 */ 389 public final void println(long l) throws IOException { 390 printBuffer(); 391 super.print(l); 392 print(CHAR_NL); 393 } 394 395 /** 396 * @see railo.runtime.writer.CFMLWriterImpl#println(java.lang.Object) 397 */ 398 public final void println(Object obj) throws IOException { 399 println(obj.toString()); 400 } 401 402 /** 403 * @see railo.runtime.writer.CFMLWriterImpl#println(java.lang.String) 404 */ 405 public final void println(String str) throws IOException { 406 print(str); 407 print(CHAR_NL); 408 409 } 410 411 /** 412 * @see railo.runtime.writer.CFMLWriterImpl#write(char[], int, int) 413 */ 414 public final void write(char[] chars, int off, int len) throws IOException { 415 for(int i=off;i<len;i++) { 416 print(chars[i]); 417 } 418 } 419 420 /** 421 * @see railo.runtime.writer.CFMLWriterImpl#write(java.lang.String, int, int) 422 */ 423 public final void write(String str, int off, int len) throws IOException { 424 write(str.toCharArray(),off,len); 425 } 426 427 428 /** 429 * @see railo.runtime.writer.CFMLWriterImpl#write(char[]) 430 */ 431 public final void write(char[] chars) throws IOException { 432 write(chars,0,chars.length); 433 } 434 435 /** 436 * @see railo.runtime.writer.CFMLWriterImpl#write(int) 437 */ 438 public final void write(int i) throws IOException { 439 print(i); 440 } 441 442 /** 443 * @see railo.runtime.writer.CFMLWriterImpl#write(java.lang.String) 444 */ 445 public final void write(String str) throws IOException { 446 write(str.toCharArray(),0,str.length()); 447 } 448 }