/* This file belongs to the Servoy development and deployment environment, Copyright (C) 1997-2010 Servoy BV This program is free software; you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation; either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program; if not, see http://www.gnu.org/licenses or write to the Free Software Foundation,Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 */ package com.servoy.j2db.server.headlessclient.dataui; import java.io.ByteArrayInputStream; import java.text.ParseException; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Locale; import java.util.Set; import org.apache.wicket.Application; import org.apache.wicket.Component; import org.apache.wicket.IResourceListener; import org.apache.wicket.Request; import org.apache.wicket.RequestCycle; import org.apache.wicket.ResourceReference; import org.apache.wicket.markup.html.form.FormComponent; import org.apache.wicket.markup.html.form.IFormSubmittingComponent; import org.apache.wicket.markup.parser.XmlPullParser; import org.apache.wicket.markup.parser.XmlTag; import org.apache.wicket.protocol.http.WicketURLEncoder; import org.apache.wicket.util.convert.IConverter; import org.apache.wicket.util.crypt.ICrypt; import org.apache.wicket.util.string.Strings; import org.apache.wicket.util.value.IValueMap; import com.servoy.j2db.FlattenedSolution; import com.servoy.j2db.MediaURLStreamHandler; import com.servoy.j2db.server.headlessclient.ServoyForm; import com.servoy.j2db.server.headlessclient.TabIndexHelper; import com.servoy.j2db.ui.ILabel; import com.servoy.j2db.util.Debug; import com.servoy.j2db.util.Utils; /** * Converts full html text, to html that can be inlined in the main html. * Has support for tranfering styles,javascripts and javascript urls to the head tag of the main html in the browser. * * @author jcompagner */ public class StripHTMLTagsConverter implements IConverter { public static final String BLOB_LOADER_PARAM = "sb"; //$NON-NLS-1$ /** * @author jcompagner * */ public static class StrippedText { private CharSequence bodyTxt; private final List<CharSequence> javascriptUrls; private final List<CharSequence> javascriptScripts; private final List<CharSequence> linkTags; private IValueMap bodyAttributes; private final List<CharSequence> styles; StrippedText() { javascriptUrls = new ArrayList<CharSequence>(); javascriptScripts = new ArrayList<CharSequence>(); styles = new ArrayList<CharSequence>(); linkTags = new ArrayList<CharSequence>(); } public final CharSequence getBodyTxt() { return bodyTxt; } public final void setBodyTxt(CharSequence bodyTxt) { this.bodyTxt = bodyTxt; } public final List<CharSequence> getJavascriptUrls() { return javascriptUrls; } public final List<CharSequence> getJavascriptScripts() { return javascriptScripts; } public final List<CharSequence> getLinkTags() { return linkTags; } /** * @param attributes */ public void addBodyAttributes(IValueMap attributes) { this.bodyAttributes = attributes; } /** * @return the bodyAttributes */ public IValueMap getBodyAttributes() { return bodyAttributes; } /** * @return */ public List<CharSequence> getStyles() { return styles; } } private static final long serialVersionUID = 1L; public static final IConverter htmlStripper = new StripHTMLTagsConverter(); public static final String[] scanTags = new String[] { "src", "href", "background", "onsubmit", "onreset", "onselect", "onclick", "ondblclick", "onfocus", "onblur", "onchange", "onkeydown", "onkeypress", "onkeyup", "onmousedown", "onmousemove", "onmouseout", "onmouseover", "onmouseup" }; //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$ //$NON-NLS-5$ //$NON-NLS-6$ //$NON-NLS-7$ //$NON-NLS-8$ //$NON-NLS-9$ //$NON-NLS-10$ //$NON-NLS-11$ //$NON-NLS-12$ //$NON-NLS-13$ //$NON-NLS-14$ //$NON-NLS-15$ //$NON-NLS-16$ //$NON-NLS-17$ //$NON-NLS-18$ //$NON-NLS-19$ public static final Set<String> ignoreTags; static { // scanTags = new HashMap(); // scanTags.put("img", new String[] { "src" }); // scanTags.put("a", new String[] { "href", "onclick" }); // scanTags.put("input", new String[] { "onclick" }); ignoreTags = new HashSet<String>(); ignoreTags.add("html"); //$NON-NLS-1$ ignoreTags.add("body"); //$NON-NLS-1$ ignoreTags.add("form"); //$NON-NLS-1$ ignoreTags.add("head"); //$NON-NLS-1$ ignoreTags.add("title"); //$NON-NLS-1$ } /** * @param bodyText * @param solution * @return */ @SuppressWarnings("nls") public static StrippedText convertBodyText(Component component, CharSequence bodyText, FlattenedSolution solutionRoot) { StrippedText st = new StrippedText(); if (RequestCycle.get() == null) { st.setBodyTxt(bodyText); return st; } ResourceReference rr = new ResourceReference("media"); //$NON-NLS-1$ String solutionName = solutionRoot.getSolution().getName(); StringBuffer bodyTxt = new StringBuffer(bodyText.length()); XmlPullParser parser = new XmlPullParser(); ICrypt urlCrypt = null; if (Application.exists()) urlCrypt = Application.get().getSecuritySettings().getCryptFactory().newCrypt(); try { parser.parse(new ByteArrayInputStream(bodyText.toString().getBytes("UTF8")), "UTF8"); //$NON-NLS-1$ //$NON-NLS-2$ XmlTag me = (XmlTag)parser.nextTag(); while (me != null) { CharSequence tmp = parser.getInputFromPositionMarker(me.getPos()); if (tmp.toString().trim().length() > 0) bodyTxt.append(tmp); parser.setPositionMarker(); String currentTagName = me.getName().toLowerCase(); if (currentTagName.equals("script")) //$NON-NLS-1$ { if (!me.isClose()) { String srcUrl = (String)me.getAttributes().get("src"); //$NON-NLS-1$ if (srcUrl == null) srcUrl = (String)me.getAttributes().get("SRC"); //$NON-NLS-1$ me = (XmlTag)parser.nextTag(); if (srcUrl != null) { st.getJavascriptUrls().add(convertMediaReferences(srcUrl, solutionName, rr, "", true).toString()); } else { if (me != null) { st.getJavascriptScripts().add(parser.getInputFromPositionMarker(me.getPos())); parser.setPositionMarker(); } } } else { me = (XmlTag)parser.nextTag(); } continue; } else if (currentTagName.equals("style")) { if (me.isOpen()) { me = (XmlTag)parser.nextTag(); List<CharSequence> styles = st.getStyles(); String style = parser.getInputFromPositionMarker(me.getPos()).toString().trim(); if (!"".equals(style) && !styles.contains(style)) { styles.add(convertMediaReferences(style, solutionName, rr, "", false)); } parser.setPositionMarker(); } else { me = (XmlTag)parser.nextTag(); } continue; } else if (currentTagName.equals("link")) { if (me.isOpen() || me.isOpenClose()) { String end = "\n"; if (me.isOpen()) end = "</link>\n"; st.getLinkTags().add(convertMediaReferences(me.toXmlString(null) + end, solutionName, rr, "", false)); } me = (XmlTag)parser.nextTag(); continue; } if (ignoreTags.contains(currentTagName)) { if (currentTagName.equals("body") && (me.isOpen() || me.isOpenClose())) { if (me.getAttributes().size() > 0) { st.addBodyAttributes(me.getAttributes()); } me = (XmlTag)parser.nextTag(); } else { me = (XmlTag)parser.nextTag(); } continue; } if (currentTagName.equals("img") && component instanceof ILabel) { ILabel label = (ILabel)component; String onload = "Servoy.Utils.setLabelChildHeight('" + component.getMarkupId() + "', " + label.getVerticalAlignment() + ");"; onload = me.getAttributes().containsKey("onload") ? me.getAttributes().getString("onload") + ";" + onload : onload; me.getAttributes().put("onload", onload); } boolean ignoreOnclick = false; IValueMap attributeMap = me.getAttributes(); // first transfer over the tabindex to anchor tags if (currentTagName.equals("a")) { int tabIndex = TabIndexHelper.getTabIndex(component); if (tabIndex != -1) attributeMap.put("tabindex", Integer.valueOf(tabIndex)); } // TODO attributes with casing? // now they have to be lowercase. (that is a xhtml requirement) for (String attribute : scanTags) { if (ignoreOnclick && attribute.equals("onclick")) continue; //$NON-NLS-1$ String src = attributeMap.getString(attribute); if (src == null) { continue; } String lowercase = src.toLowerCase(); if (lowercase.startsWith(MediaURLStreamHandler.MEDIA_URL_DEF)) { String name = src.substring(MediaURLStreamHandler.MEDIA_URL_DEF.length()); if (name.startsWith(MediaURLStreamHandler.MEDIA_URL_BLOBLOADER)) { String url = generateBlobloaderUrl(component, urlCrypt, name); me.getAttributes().put(attribute, url); } else { String translatedUrl = MediaURLStreamHandler.getTranslatedMediaURL(solutionRoot, lowercase); if (translatedUrl != null) { me.getAttributes().put(attribute, translatedUrl); } } } else if (component instanceof ISupportScriptCallback && lowercase.startsWith("javascript:")) { String script = src; if (script.length() > 13) { String scriptName = script.substring(11); if ("href".equals(attribute)) { if (attributeMap.containsKey("externalcall")) { attributeMap.remove("externalcall"); } else { me.getAttributes().put("href", "#"); me.getAttributes().put("onclick", ((ISupportScriptCallback)component).getCallBackUrl(scriptName, true)); ignoreOnclick = true; } } else { me.getAttributes().put(attribute, ((ISupportScriptCallback)component).getCallBackUrl(scriptName, "onclick".equals(attribute))); } } } else if (component instanceof FormComponent< ? > && lowercase.startsWith("javascript:")) { String script = src; if (script.length() > 13) { String scriptName = script.substring(11); if ("href".equals(attribute)) { me.getAttributes().put("href", "#"); me.getAttributes().put("onclick", getTriggerJavaScript((FormComponent< ? >)component, scriptName)); ignoreOnclick = true; } else { me.getAttributes().put(attribute, getTriggerJavaScript((FormComponent< ? >)component, scriptName)); } } } } bodyTxt.append(me.toString()); me = (XmlTag)parser.nextTag(); } bodyTxt.append(parser.getInputFromPositionMarker(-1)); st.setBodyTxt(convertMediaReferences(convertBlobLoaderReferences(bodyTxt, component), solutionName, rr, "", false)); //$NON-NLS-1$ } catch (ParseException ex) { Debug.error(ex); bodyTxt.append("<span style=\"color : #ff0000;\">"); //$NON-NLS-1$ bodyTxt.append(ex.getMessage()); bodyTxt.append(bodyText.subSequence(ex.getErrorOffset(), Math.min(ex.getErrorOffset() + 100, bodyText.length()))); bodyTxt.append("</span></body></html>"); //$NON-NLS-1$ st.setBodyTxt(bodyTxt); } catch (Exception ex) { Debug.error(ex); bodyTxt.append("<span style=\"color : #ff0000;\">"); //$NON-NLS-1$ bodyTxt.append(ex.getMessage()); bodyTxt.append("</span></body></html>"); //$NON-NLS-1$ st.setBodyTxt(bodyTxt); } return st; } /** * @param component * @param urlCrypt * @param name * @return */ public static String generateBlobloaderUrl(Component component, ICrypt urlCrypt, String name) { String mediaUrlPart = name.substring((MediaURLStreamHandler.MEDIA_URL_BLOBLOADER + '?').length()); if (urlCrypt != null) { mediaUrlPart = WicketURLEncoder.QUERY_INSTANCE.encode(urlCrypt.encryptUrlSafe(mediaUrlPart)); } else { // if no url crypt then the old way mediaUrlPart = "true&" + mediaUrlPart; //$NON-NLS-1$ } return RequestCycle.get().urlFor(component, IResourceListener.INTERFACE).toString() + '&' + BLOB_LOADER_PARAM + '=' + mediaUrlPart; } public static String getBlobLoaderUrlPart(Request request) { String url = request.getParameter(BLOB_LOADER_PARAM); if (url != null) { // old url if (url.equals("true")) //$NON-NLS-1$ { url = request.getURL(); } else { // encrypted if (Application.exists()) { try { ICrypt urlCrypt = Application.get().getSecuritySettings().getCryptFactory().newCrypt(); url = urlCrypt.decryptUrlSafe(url); url = url.replace("&", "&"); //$NON-NLS-1$ //$NON-NLS-2$ } catch (Exception e) { Debug.error("Error decrypting blobloader url: " + url, e); } } } } return url; } @SuppressWarnings("nls") public static CharSequence convertBlobLoaderReferences(CharSequence text, Component component) { if (text != null) { String txt = text.toString(); int index = txt.indexOf("media:///servoy_blobloader?"); if (index == -1) return txt; ICrypt urlCrypt = null; if (Application.exists()) urlCrypt = Application.get().getSecuritySettings().getCryptFactory().newCrypt(); if (urlCrypt != null) { while (index != -1) { // just try to search for the ending quote int index2 = Utils.firstIndexOf(txt, new char[] { '\'', '"', ' ', '\t', ')' }, index); // if ending can't be resolved don't encrypt it. if (index2 == -1) return Strings.replaceAll(text, "media:///servoy_blobloader?", RequestCycle.get().urlFor(component, IResourceListener.INTERFACE) + "&" + BLOB_LOADER_PARAM + "=true&"); String bloburl = generateBlobloaderUrl(component, urlCrypt, txt.substring(index + "media:///".length(), index2)); txt = txt.substring(0, index) + bloburl + txt.substring(index2); index = txt.indexOf("media:///servoy_blobloader?", index + 1); } return txt; } } if (RequestCycle.get() != null) return Strings.replaceAll(text, "media:///servoy_blobloader?", RequestCycle.get().urlFor(component, IResourceListener.INTERFACE) + "&" + BLOB_LOADER_PARAM + "=true&"); return text; } public static CharSequence convertMediaReferences(CharSequence text, String solutionName, ResourceReference media, String prefix, boolean quoteSpecialHTMLChars) // TODO quoteSpecialHTMLChars - shouldn't this always be true? (currently in most places it is false) { if (RequestCycle.get() != null) return Strings.replaceAll(text, "media:///", prefix + RequestCycle.get().urlFor(media) + "?s=" + solutionName + (quoteSpecialHTMLChars ? "&" : "&") + "id="); //$NON-NLS-1$//$NON-NLS-2$//$NON-NLS-3$ return text; } public static String getTriggerJavaScript(FormComponent< ? > component, String value) { ServoyForm form = (ServoyForm)component.getForm(); StringBuffer sb = new StringBuffer(100); sb.append("javascript:document.getElementById('"); //$NON-NLS-1$ sb.append(form.getHiddenField()); sb.append("').name=\'"); //$NON-NLS-1$ sb.append(component.getInputName()); sb.append("';"); //$NON-NLS-1$ sb.append("document.getElementById('"); //$NON-NLS-1$ sb.append(form.getHiddenField()); sb.append("').value=\'"); //$NON-NLS-1$ sb.append(Utils.stringReplace(value, "\'", "\\\'")); //$NON-NLS-1$ //$NON-NLS-2$ sb.append("';"); //$NON-NLS-1$ sb.append("var f=document.getElementById('"); //$NON-NLS-1$ sb.append(form.getJavascriptCssId()); sb.append("');"); //$NON-NLS-1$ sb.append("if (f.onsubmit != undefined) { if (f.onsubmit()==false) return false; }"); //$NON-NLS-1$ sb.append("f.submit();return false;"); //$NON-NLS-1$ return sb.toString(); } public static String getTriggerJavaScript(IFormSubmittingComponent component, String value) { ServoyForm form = (ServoyForm)component.getForm(); StringBuffer sb = new StringBuffer(100); sb.append("javascript:document.getElementById('"); //$NON-NLS-1$ sb.append(form.getHiddenField()); sb.append("').name=\'"); //$NON-NLS-1$ sb.append(component.getInputName()); sb.append("';"); //$NON-NLS-1$ sb.append("document.getElementById('"); //$NON-NLS-1$ sb.append(form.getHiddenField()); sb.append("').value=\'"); //$NON-NLS-1$ sb.append(Utils.stringReplace(value, "\'", "\\\'")); //$NON-NLS-1$ //$NON-NLS-2$ sb.append("';"); //$NON-NLS-1$ sb.append("var f=document.getElementById('"); //$NON-NLS-1$ sb.append(form.getJavascriptCssId()); sb.append("');"); //$NON-NLS-1$ sb.append("if (f.onsubmit != undefined) { if (f.onsubmit()==false) return false; }"); //$NON-NLS-1$ sb.append("f.submit();return false;"); //$NON-NLS-1$ return sb.toString(); } /** * @see wicket.util.convert.IConverter#convertToObject(java.lang.String, java.util.Locale) */ public Object convertToObject(String value, Locale locale) { return value; } /** * @see wicket.util.convert.IConverter#convertToString(java.lang.Object, java.util.Locale) */ public String convertToString(Object value, Locale locale) { if (value == null) return null; return TemplateGenerator.getSafeText(value.toString()); } }