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}