001/* 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017package org.apache.wicket.markup.parser.filter; 018 019import org.apache.wicket.core.util.string.JavaScriptUtils; 020import org.apache.wicket.markup.ComponentTag; 021import org.apache.wicket.markup.Markup; 022import org.apache.wicket.markup.MarkupElement; 023import org.apache.wicket.markup.RawMarkup; 024import org.apache.wicket.markup.parser.AbstractMarkupFilter; 025import org.apache.wicket.markup.parser.XmlPullParser; 026import org.apache.wicket.util.lang.Args; 027 028import java.text.ParseException; 029import java.util.regex.Pattern; 030 031 032/** 033 * An IMarkupFilter that wraps the body of all <style> elements and <script> 034 * elements which are plain JavaScript in CDATA blocks. This allows the user application 035 * to use unescaped XML characters without caring that those may break Wicket's XML Ajax 036 * response. 037 * 038 * @author Juergen Donnerstag 039 */ 040public final class StyleAndScriptIdentifier extends AbstractMarkupFilter 041{ 042 /** 043 * Constructor. 044 */ 045 public StyleAndScriptIdentifier() 046 { 047 } 048 049 @Override 050 protected final MarkupElement onComponentTag(final ComponentTag tag) throws ParseException 051 { 052 if (tag.getNamespace() != null) 053 { 054 return tag; 055 } 056 057 String tagName = tag.getName(); 058 boolean isScript = XmlPullParser.SCRIPT.equalsIgnoreCase(tagName); 059 boolean isStyle = XmlPullParser.STYLE.equalsIgnoreCase(tagName); 060 if (isScript || isStyle) 061 { 062 if (tag.isOpen() && tag.getId() == null && ((isScript && tag.getAttribute("src") == null) || isStyle)) 063 { 064 // Not needed, but must not be null 065 tag.setId("_ScriptStyle"); 066 tag.setModified(true); 067 tag.setAutoComponentTag(true); 068 tag.setFlag(ComponentTag.RENDER_RAW, true); 069 } 070 071 tag.setUserData("STYLE_OR_SCRIPT", Boolean.TRUE); 072 } 073 074 return tag; 075 } 076 077 @Override 078 public void postProcess(Markup markup) 079 { 080 for (int i = 0; i < markup.size(); i++) 081 { 082 MarkupElement elem = markup.get(i); 083 if (elem instanceof ComponentTag) 084 { 085 ComponentTag open = (ComponentTag)elem; 086 087 if (shouldProcess(open)) 088 { 089 if (open.isOpen() && ((i + 2) < markup.size())) 090 { 091 MarkupElement body = markup.get(i + 1); 092 MarkupElement tag2 = markup.get(i + 2); 093 094 if ((body instanceof RawMarkup) && (tag2 instanceof ComponentTag)) 095 { 096 ComponentTag close = (ComponentTag)tag2; 097 if (close.closes(open)) 098 { 099 String text = body.toString().trim(); 100 if (shouldWrapInCdata(text)) 101 { 102 text = JavaScriptUtils.SCRIPT_CONTENT_PREFIX + body.toString() + 103 JavaScriptUtils.SCRIPT_CONTENT_SUFFIX; 104 markup.replace(i + 1, new RawMarkup(text)); 105 } 106 } 107 } 108 } 109 } 110 } 111 } 112 } 113 114 // OES == optional empty space 115 116 // OES<!--OES 117 private static final Pattern HTML_START_COMMENT = Pattern.compile("^\\s*<!--\\s*.*", Pattern.DOTALL); 118 119 // OES<![CDATA[OES 120 private static final Pattern CDATA_START_COMMENT = Pattern.compile("^\\s*<!\\[CDATA\\[\\s*.*", Pattern.DOTALL); 121 122 // OES/*OES<![CDATA[OES*/OES 123 private static final Pattern JS_CDATA_START_COMMENT = Pattern.compile("^\\s*\\/\\*\\s*<!\\[CDATA\\[\\s*\\*\\/\\s*.*", Pattern.DOTALL); 124 125 boolean shouldWrapInCdata(final String elementBody) 126 { 127 Args.notNull(elementBody, "elementBody"); 128 129 boolean shouldWrap = true; 130 131 if ( 132 HTML_START_COMMENT.matcher(elementBody).matches() || 133 CDATA_START_COMMENT.matcher(elementBody).matches() || 134 JS_CDATA_START_COMMENT.matcher(elementBody).matches() 135 ) 136 { 137 shouldWrap = false; 138 } 139 140 return shouldWrap; 141 } 142 143 private boolean shouldProcess(ComponentTag openTag) 144 { 145 // do not wrap in CDATA any <script> which has special MIME type. WICKET-4425 146 String typeAttribute = openTag.getAttribute("type"); 147 boolean shouldProcess = 148 // style elements should be processed 149 "style".equals(openTag.getName()) || 150 151 // script elements should be processed only if they have no type (HTML5 recommendation) 152 // or the type is "text/javascript" or "module" 153 (typeAttribute == null || "text/javascript".equalsIgnoreCase(typeAttribute) || 154 "module".equalsIgnoreCase(typeAttribute)); 155 156 return shouldProcess && openTag.getUserData("STYLE_OR_SCRIPT") != null; 157 } 158}