001: /**
002: *
003: */package clime.messadmin.filter;
004:
005: import java.io.IOException;
006: import java.io.PrintWriter;
007: import java.io.Writer;
008: import java.lang.reflect.InvocationTargetException;
009: import java.lang.reflect.Method;
010: import java.net.InetAddress;
011: import java.net.UnknownHostException;
012:
013: import javax.servlet.ServletOutputStream;
014: import javax.servlet.ServletResponse;
015: import javax.servlet.http.HttpServletResponse;
016: import javax.servlet.http.HttpServletResponseWrapper;
017:
018: import clime.messadmin.core.MessAdmin;
019:
020: /**
021: * Pass-trough response wrapper, looking up for message injection.
022: * IMPLEMENTATION NOTE: do not buffer response here: it can be quite large...
023: * @author Cédrik LIME
024: * TODO: for version 2, try to put the script at an even more suitable location (immediatly after <body> or immediatly before </body> or </head>)
025: */
026: public class MessAdminResponseWrapper extends
027: HttpServletResponseWrapper {
028: private static final boolean DEBUG = false;
029: private static transient Method getContentType = null;
030:
031: private static String[] SCRIPT_BEGIN = new String[] {
032: "<script language='JavaScript' type='text/javascript'>",
033: "<!--",
034: " var width = 600; // screen.availWidth - 10",
035: " var height = 400; // screen.availHeight - 20",
036: " var screenX = Math.floor((screen.width)/2) - Math.floor(width/2);",
037: " var screenY = Math.floor((screen.height)/2) - Math.floor(height/2) - 20;",
038: "",
039: " var features =",
040: " 'toolbar=no,' +",
041: " 'scrollbars=yes,' +",
042: " 'status=no,' +",
043: " 'location=no,' +",
044: " 'directories=no,' +",
045: " 'menubar=no,' +",
046: " 'resizable=yes,' +",
047: " 'width=' + width + ',' +",
048: " 'height=' + height + ',' +",
049: " 'top=' + screenY + ',' +",
050: " 'left=' + screenX + ',' +",
051: "",
052: " // NS only",
053: " 'screenX=' + screenX + ',' +",
054: " 'screenY=' + screenY + ',' +",
055: " 'alwaysRaised=yes,' +",
056: " 'dependent=yes,' +",
057: " 'hotkeys=no,' +",
058: " 'modal=yes';",
059: "",
060: " // IE only",
061: " var ieFeatures =",
062: " 'center=yes,' +",
063: " 'dialogHeight=' + height + ',' +",
064: " 'dialogWidth=' + width + ',' +",
065: " 'dialogTop=' + screenY + ',' +",
066: " 'dialogLeft=' + screenX + ',' +",
067: " 'resizable=yes,' +",
068: " 'help=no';",
069: "",
070: // " //URL - string containing the URL of the document to open in the new window. If no URL is specified, an empty window will be created.",
071: // " //name - string containing the name of the new window. This can be used as the 'target' attribute of a <form> or <a> tag to point to the new window.",
072: // " //features - optional string that contains details of which of the standard window features are to be used with the new window.",
073: " var messadminpopup;",
074: " //if (document.all && window.print) { // IE5",
075: // Do not confuse Modeless windows (showModelessWindow) of IE5 with Modal Windows (showModalDialog) of IE4.
076: // While both remain the active window until closed, the later is somewhat a "pest" in that the rest of the
077: // page is "hung up" until the window is dismissed. We like modeless windows better.
078: // Note that links inside modeless windows are always launched in a new page.
079: " // messadminpopup = window.showModelessWindow('', 'MessAdminPopUpPage', ieFeatures);",
080: " //} else {",
081: " messadminpopup = window.open('', 'MessAdminPopUpPage', features);",
082: " if (!messadminpopup.closed) { messadminpopup.close(); }",
083: " messadminpopup = window.open('', 'MessAdminPopUpPage', features);",
084: " //}",
085: " if (!messadminpopup.opener) { messadminpopup.opener = self; }",
086: " messadminpopup.document.writeln('<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01 Transitional//EN\" \"http://www.w3.org/TR/html4/loose.dtd\">');",
087: " messadminpopup.document.writeln('<html>');",
088: " messadminpopup.document.writeln('<head><title>Administrative message</title></head>');",
089: " messadminpopup.document.writeln('<body onload=\"window.focus()\" onblur=\"self.focus()\">');",
090: " messadminpopup.document.writeln('" };
091: private static final String[] SCRIPT_END = new String[] { "');",
092: " messadminpopup.document.writeln('</body></html>');",
093: " //self.blur();",
094: " setTimeout('messadminpopup.focus()', 500);",
095: " messadminpopup.focus();",
096: //" document.body.getAttributeNode('onunload').value = 'messadminpopup.close();' + document.body.getAttributeNode('onunload').value;",
097: "// -->", "</script>" };
098:
099: private static final short JS_SCRIPT_SIZE;//bytes
100:
101: static {
102: short scriptSize = 0;
103: for (int i = 0; i < SCRIPT_BEGIN.length; ++i) {
104: String line = SCRIPT_BEGIN[i];
105: scriptSize += 1 + line.length();//FIXME: depends on encoding!
106: }
107: for (int i = 0; i < SCRIPT_END.length; ++i) {
108: String line = SCRIPT_END[i];
109: scriptSize += 1 + line.length();//FIXME: depends on encoding!
110: }
111: JS_SCRIPT_SIZE = scriptSize;
112:
113: // @since 2.4
114: try {
115: getContentType = ServletResponse.class.getMethod(
116: "getContentType", null);//$NON-NLS-1$
117: } catch (SecurityException e) {
118: } catch (NoSuchMethodException e) {
119: }
120: }
121:
122: protected String injectedMessageHTML = null;
123: protected boolean shouldInject = false;
124: protected boolean messageInjected = false;
125: private HttpServletResponse httpResponse;
126: private int contentLength = -1;
127: private String contentType = null;
128: private PrintWriter writer = null;
129: private ServletOutputStream stream = null;
130: protected long responseLength = -1;
131: protected int status;
132:
133: /**
134: * @param response
135: */
136: public MessAdminResponseWrapper(HttpServletResponse response) {
137: super (response);
138: this .httpResponse = response;
139: if (getContentType != null) {
140: //contentType = response.getContentType();
141: try {
142: contentType = (String) getContentType.invoke(response,
143: null);
144: setShouldInject();
145: } catch (IllegalArgumentException iae) {
146: } catch (IllegalAccessException iae) {
147: } catch (InvocationTargetException ite) {
148: }
149: }
150: }
151:
152: public int getContentLength() {
153: return contentLength;
154: }
155:
156: /** {@inheritDoc} */
157: // @Override
158: public void setContentLength(int len) {
159: int realLength = len;
160: if (shouldInject) {
161: realLength += JS_SCRIPT_SIZE + injectedMessageHTML.length();//FIXME: depends on encoding!
162: }
163: super .setContentLength(realLength);
164: contentLength = realLength;
165: }
166:
167: /**
168: * Will only be valid at end of response cycle, when reponse is flush'ed.
169: * @return byte count of response
170: */
171: public long getResponseLength() {
172: return (responseLength == -1) ? responseLength
173: : responseLength + 1;
174: }
175:
176: /** {@inheritDoc} */
177: // @Override
178: public void setContentType(String type) {
179: super .setContentType(type);
180: contentType = type;
181: setShouldInject();
182: }
183:
184: /** {@inheritDoc} */
185: public void setStatus(int sc) {
186: super .setStatus(sc);
187: status = sc;
188: }
189:
190: public int getStatus() {
191: return status;
192: }
193:
194: protected void setWarning() {
195: setShouldInject();
196: if (shouldInject && !httpResponse.isCommitted()) {
197: String warnAgent = null;
198: try {
199: warnAgent = InetAddress.getLocalHost().toString();
200: } catch (Exception e) {
201: warnAgent = "MessAdmin/" + MessAdmin.getVersion();
202: }
203: httpResponse
204: .addHeader(
205: "Warning",
206: "199 "
207: + warnAgent
208: + " MessAdmin injected a popup message in this message.");
209: // 199 Miscellaneous warning The warning text MAY include arbitrary information to be presented to a human user, or logged.
210: // A system receiving this warning MUST NOT take any automated action, besides presenting the warning to the user.
211: }
212: }
213:
214: // @Override
215: public synchronized PrintWriter getWriter() throws IOException {
216: setWarning();
217: if (writer == null) {
218: writer = new CountingInjectorPrintWriter(super .getWriter());
219: }
220: return writer;
221: }
222:
223: /** BUG: if <head> has attributes, it won't be catched. FIXME! Look for </head> instead (more difficult: buffering) or knowledge of spaces+attributes */
224: protected class CountingInjectorPrintWriter extends PrintWriter {
225: private final char[] search = new char[] { '<', 'h', 'e', 'a',
226: 'd', '>' };
227: private byte pos = 0;
228:
229: public CountingInjectorPrintWriter(PrintWriter outWriter) {
230: super (outWriter);
231: }
232:
233: /** {@inheritDoc} */
234: public void write(int c) {
235: if (shouldInject) {
236: // look for <head> and inject
237: if (Character.toLowerCase((char) c) == search[pos]) {
238: ++pos;
239: } else {
240: pos = 0;
241: }
242: if (pos >= search.length) {
243: try {
244: super .write(c);
245: ++responseLength;
246: // inject message immediatly after '<head>'
247: injectMessage((PrintWriter) out);
248: } catch (IOException e) {
249: setError();
250: }
251: shouldInject = false;//should not be required, but more prudent...
252: pos = 0;
253: return;
254: }
255: }
256: super .write(c);
257: ++responseLength;
258: }
259:
260: /** {@inheritDoc} */
261: public void write(char[] cbuf, int off, int len) {
262: if (shouldInject) {
263: // look for <head> and inject
264: for (int i = off; i < off + len; ++i) {
265: if (Character.toLowerCase(cbuf[i]) == search[pos]) {
266: ++pos;
267: } else {
268: pos = 0;
269: }
270: if (pos >= search.length) {
271: try {
272: ++i;//we can because of return at end
273: super .write(cbuf, off, i - off);
274: // inject message immediatly after '<head>'
275: injectMessage((PrintWriter) out);
276: super .write(cbuf, off + i, len - (i - off));
277: responseLength += len - off;
278: } catch (IOException e) {
279: setError();
280: }
281: shouldInject = false;//should not be required, but more prudent...
282: pos = 0;
283: return;
284: }
285: }
286: }
287: super .write(cbuf, off, len);
288: responseLength += len - off;
289: }
290:
291: /** {@inheritDoc} */
292: public void write(String s, int off, int len) {
293: if (shouldInject) {
294: // look for <head> and inject
295: for (int i = off; i < off + len; ++i) {
296: if (Character.toLowerCase(s.charAt(i)) == search[pos]) {
297: ++pos;
298: } else {
299: pos = 0;
300: }
301: if (pos >= search.length) {
302: try {
303: ++i;//we can because of return at end
304: super .write(s, off, i - off);
305: // inject message immediatly after '<head>'
306: injectMessage((PrintWriter) out);
307: super .write(s, off + i, len - (i - off));
308: responseLength += len - off;//FIXME: depends on encoding!
309: } catch (IOException e) {
310: setError();
311: }
312: shouldInject = false;//should not be required, but more prudent...
313: pos = 0;
314: return;
315: }
316: }
317: }
318: super .write(s, off, len);
319: responseLength += len - off;//FIXME: depends on encoding!
320: }
321:
322: }
323:
324: /** {@inheritDoc} */
325: // @Override
326: public synchronized ServletOutputStream getOutputStream()
327: throws IOException {
328: setWarning();
329: if (stream == null) {
330: stream = new CountingServletOutputStream(super
331: .getOutputStream());
332: }
333: return stream;
334: }
335:
336: protected class CountingServletOutputStream extends
337: ServletOutputStream {
338: protected ServletOutputStream out;
339:
340: public CountingServletOutputStream(ServletOutputStream out) {
341: this .out = out;
342: }
343:
344: /** {@inheritDoc} */
345: public void write(int b) throws IOException {
346: out.write(b);
347: ++responseLength;
348: }
349: }
350:
351: /**
352: * Injects code in output stream
353: * mess up with response encoding (convert message from UTF to XYZ)? No: handled by the PrintWriter
354: * @return true if message was injected, false otherwise
355: * @throws IOException
356: */
357: public boolean finish() throws IOException {
358: try {
359: PrintWriter out = httpResponse.getWriter();
360: return injectMessage(out);
361: } catch (IllegalStateException ise) {
362: // abort, don't do anything: getOutputStream() was previously called
363: if (DEBUG) {
364: System.err
365: .println(this .getClass().getName()
366: + ": can't inject: " + ise.getLocalizedMessage());//$NON-NLS-1$
367: }
368: }
369: return false;
370: }
371:
372: protected boolean injectMessage(PrintWriter out) throws IOException {
373: setShouldInject();
374: if (!shouldInject) {
375: // no message to inject
376: return false;
377: }
378: out.println();
379: for (int i = 0; i < SCRIPT_BEGIN.length - 1; ++i) {
380: String line = SCRIPT_BEGIN[i];
381: out.println(line);
382: }
383: out.print(SCRIPT_BEGIN[SCRIPT_BEGIN.length - 1]);
384: escapeJavaStyleString(out, injectedMessageHTML, true);
385: for (int i = 0; i < SCRIPT_END.length; ++i) {
386: String line = SCRIPT_END[i];
387: out.println(line);
388: }
389: if (DEBUG) {
390: System.out.println(this .getClass().getName()
391: + ": injected message");//$NON-NLS-1$
392: }
393: responseLength += JS_SCRIPT_SIZE + injectedMessageHTML.length();//FIXME: depends on encoding!
394: shouldInject = false;
395: injectedMessageHTML = null; // message is gone; information used by MessAdminFilter
396: messageInjected = true;
397: return true;
398: }
399:
400: public String getInjectedMessageHTML() {
401: return injectedMessageHTML;
402: }
403:
404: public void setInjectedMessageHTML(String in_injectedMessage) {
405: injectedMessageHTML = in_injectedMessage;
406: setShouldInject();
407: }
408:
409: private void setShouldInject() {
410: // is there a message to inject?
411: shouldInject = (injectedMessageHTML != null
412: && injectedMessageHTML.length() != 0 && !"".equals(injectedMessageHTML.trim()));//$NON-NLS-1$
413: // does the contentType allow HTML injection?
414: final String l_contentType = (contentType != null) ? contentType
415: .toLowerCase()
416: : "";//$NON-NLS-1$
417: if (shouldInject
418: && (l_contentType.indexOf("text/html") == -1) && (l_contentType.indexOf("application/xhtml+xml") == -1)) {//$NON-NLS-1$ //$NON-NLS-2$
419: // don't inject anything in non-html stuff or if no message to inject!
420: shouldInject = false;
421: if (DEBUG) {
422: System.out.println(this .getClass().getName()
423: + ": no injection (not html)");//$NON-NLS-1$
424: }
425: }
426: }
427:
428: public boolean isMessageInjected() {
429: return messageInjected;
430: }
431:
432: /**
433: * COPIED FROM COMMONS-LANG 2.1 org.apache.commons.lang.StringEscapeUtils
434: *
435: * <p>Escapes the characters in a <code>String</code> using JavaScript String rules
436: * to a <code>Writer</code>.</p>
437: *
438: * <p>Escapes any values it finds into their JavaScript String form.
439: * Deals correctly with quotes and control-chars (tab, backslash, cr, ff, etc.) </p>
440: *
441: * <p>So a tab becomes the characters <code>'\\'</code> and
442: * <code>'t'</code>.</p>
443: *
444: * <p>The only difference between Java strings and JavaScript strings
445: * is that in JavaScript, a single quote must be escaped.</p>
446: *
447: * <p>Example:
448: * <pre>
449: * input string: He didn't say, "Stop!"
450: * output string: He didn\'t say, \"Stop!\"
451: * </pre>
452: * </p>
453: *
454: * <p>A <code>null</code> string input has no effect.</p>
455: *
456: * @param out Writer to write escaped string into
457: * @param str String to escape values in, may be null
458: * @throws IllegalArgumentException if the Writer is <code>null</code>
459: * @throws IOException if error occurs on undelying Writer
460: **/
461: private static void escapeJavaStyleString(Writer out, String str,
462: boolean escapeSingleQuote) throws IOException {
463: if (out == null) {
464: throw new IllegalArgumentException(
465: "The Writer must not be null");//$NON-NLS-1$
466: }
467: if (str == null) {
468: return;
469: }
470: int sz;
471: sz = str.length();
472: for (int i = 0; i < sz; ++i) {
473: char ch = str.charAt(i);
474:
475: // handle unicode
476: if (ch > 0xfff) {
477: out.write("\\u" + hex(ch));//$NON-NLS-1$
478: } else if (ch > 0xff) {
479: out.write("\\u0" + hex(ch));//$NON-NLS-1$
480: } else if (ch > 0x7f) {
481: out.write("\\u00" + hex(ch));//$NON-NLS-1$
482: } else if (ch < 32) {
483: switch (ch) {
484: case '\b':
485: out.write('\\');
486: out.write('b');
487: break;
488: case '\n':
489: out.write('\\');
490: out.write('n');
491: break;
492: case '\t':
493: out.write('\\');
494: out.write('t');
495: break;
496: case '\f':
497: out.write('\\');
498: out.write('f');
499: break;
500: case '\r':
501: out.write('\\');
502: out.write('r');
503: break;
504: default:
505: if (ch > 0xf) {
506: out.write("\\u00" + hex(ch));//$NON-NLS-1$
507: } else {
508: out.write("\\u000" + hex(ch));//$NON-NLS-1$
509: }
510: break;
511: }
512: } else {
513: switch (ch) {
514: case '\'':
515: if (escapeSingleQuote) {
516: out.write('\\');
517: }
518: out.write('\'');
519: break;
520: case '"':
521: out.write('\\');
522: out.write('"');
523: break;
524: case '\\':
525: out.write('\\');
526: out.write('\\');
527: break;
528: default:
529: out.write(ch);
530: break;
531: }
532: }
533: }
534: }
535:
536: /**
537: * COPIED FROM COMMONS-LANG 2.1 org.apache.commons.lang.StringEscapeUtils
538: *
539: * <p>Returns an upper case hexadecimal <code>String</code> for the given
540: * character.</p>
541: *
542: * @param ch The character to convert.
543: * @return An upper case hexadecimal <code>String</code>
544: */
545: private static String hex(char ch) {
546: return Integer.toHexString(ch).toUpperCase();
547: }
548:
549: }
|