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 java.text.ParseException;
020import java.util.Iterator;
021
022import org.apache.wicket.Component;
023import org.apache.wicket.MarkupContainer;
024import org.apache.wicket.behavior.Behavior;
025import org.apache.wicket.markup.ComponentTag;
026import org.apache.wicket.markup.ComponentTag.IAutoComponentFactory;
027import org.apache.wicket.markup.Markup;
028import org.apache.wicket.markup.MarkupElement;
029import org.apache.wicket.markup.MarkupResourceStream;
030import org.apache.wicket.markup.MarkupStream;
031import org.apache.wicket.markup.WicketTag;
032import org.apache.wicket.markup.html.TransparentWebMarkupContainer;
033import org.apache.wicket.markup.parser.AbstractMarkupFilter;
034import org.apache.wicket.markup.resolver.IComponentResolver;
035import org.apache.wicket.request.UrlUtils;
036import org.apache.wicket.request.cycle.RequestCycle;
037import org.slf4j.Logger;
038import org.slf4j.LoggerFactory;
039
040/**
041 * The purpose of this filter is to make all "href", "src" and "background" attributes found in the
042 * markup which contain a relative URL like "myDir/myPage.gif" actually resolve in the output HTML,
043 * by prefixing them with with an appropriate path to make the link work properly, even if the
044 * current page is being displayed at a mounted URL or whatever. It is applied to all non wicket
045 * component tags, except for auto-linked tags.
046 * 
047 * It achieves this by being both an IMarkupFilter and IComponentResolver, and works similarly to
048 * the <wicket:message> code. For each tag, we look to see if the path in "href", "src" and
049 * "background" attributes is relative. If it is, we assume it's relative to the context path and we
050 * should prefix it appropriately so that it resolves correctly for the current request, even if
051 * that's for something that's not at the context root. This is done for ServletWebRequests by
052 * prepending with "../" tokens, for example.
053 * 
054 * 
055 * @author Al Maw
056 */
057public final class RelativePathPrefixHandler extends AbstractMarkupFilter
058        implements
059                IComponentResolver
060{
061        private static final long serialVersionUID = 1L;
062
063        /** Logging */
064        private static final Logger log = LoggerFactory.getLogger(RelativePathPrefixHandler.class);
065
066        /**
067         * The id automatically assigned to tags without an id which we need to prepend a relative path
068         * to.
069         */
070        public static final String WICKET_RELATIVE_PATH_PREFIX_CONTAINER_ID = "_relative_path_prefix_";
071
072        /** List of attribute names considered */
073        private static final String attributeNames[] = new String[] { "href", "src", "background",
074                        "action" };
075
076        /**
077         * Behavior that adds a prefix to src, href and background attributes to make them
078         * context-relative
079         */
080        public static final Behavior RELATIVE_PATH_BEHAVIOR = new Behavior()
081        {
082                private static final long serialVersionUID = 1L;
083
084                @Override
085                public void onComponentTag(Component component, ComponentTag tag)
086                {
087                        // Modify all relevant attributes
088                        for (String attrName : attributeNames)
089                        {
090                                String attrValue = tag.getAttributes().getString(attrName);
091
092                                if ((attrValue != null) && (attrValue.startsWith("/") == false)
093                                        && (!attrValue.contains(":")) && !(attrValue.startsWith("#")))
094                                {
095                                        tag.getAttributes().put(attrName,
096                                                UrlUtils.rewriteToContextRelative(attrValue, RequestCycle.get()));
097                                }
098                        }
099                }
100        };
101        
102        private static final IAutoComponentFactory FACTORY = new IAutoComponentFactory()
103        {
104                @Override
105                public Component newComponent(MarkupContainer container, ComponentTag tag)
106                {
107                        return new TransparentWebMarkupContainer(tag.getId());
108                }
109        };
110
111        /**
112         * Constructor for the IComponentResolver role.
113         */
114        public RelativePathPrefixHandler()
115        {
116                this(null);
117        }
118
119        /**
120         * Constructor for the IMarkupFilter role
121         * 
122         * @param markup
123         *            The markup created by reading the markup file
124         */
125        public RelativePathPrefixHandler(final MarkupResourceStream markup)
126        {
127                super(markup);
128        }
129
130        @Override
131        protected final MarkupElement onComponentTag(ComponentTag tag) throws ParseException
132        {
133                if (tag.isClose())
134                {
135                        return tag;
136                }
137
138                String wicketIdAttr = getWicketNamespace() + ":" + "id";
139
140                // Don't touch any wicket:id component and any auto-components
141                if ((tag instanceof WicketTag) || (tag.isAutolinkEnabled() == true)
142                        || (tag.getAttributes().get(wicketIdAttr) != null))
143                {
144                        return tag;
145                }
146                
147                // Work out whether we have any attributes that require us to add a
148                // behavior that prepends the relative path.
149                for (String attrName : attributeNames)
150                {
151                        String attrValue = tag.getAttributes().getString(attrName);
152                        if ((attrValue != null) && (attrValue.startsWith("/") == false)
153                                && (!attrValue.contains(":")) && !(attrValue.startsWith("#")))
154                        {
155                                if (tag.getId() == null)
156                                {
157                                        tag.setId(getWicketRelativePathPrefix(null)
158                                                + getRequestUniqueId());
159                                        tag.setAutoComponentTag(true);
160                                }
161
162                                tag.addBehavior(RELATIVE_PATH_BEHAVIOR);
163                                tag.setModified(true);
164                                
165                                break;
166                        }
167                }
168
169                return tag;
170        }
171                
172        @Override
173        public Component resolve(final MarkupContainer container, final MarkupStream markupStream,
174                final ComponentTag tag)
175        {
176                if ((tag != null) && (tag.getId().startsWith(getWicketRelativePathPrefix(markupStream))))
177                {
178                        // we do not want to mess with the hierarchy, so the container has to be
179                        // transparent as it may have wicket components inside. for example a raw anchor tag
180                        // that contains a label.
181                        return new TransparentWebMarkupContainer(tag.getId());
182                }
183                return null;
184        }
185        
186        @Override
187        public void postProcess(Markup markup)
188        {
189                /**
190                 * https://issues.apache.org/jira/browse/WICKET-5724
191                 * 
192                 * Transparent component inside page body must allow queued children components.
193                 */
194                Iterator<MarkupElement> markupIterator = markup.iterator();
195                while (markupIterator.hasNext())
196                {
197                        MarkupElement next = markupIterator.next();
198
199                        if (next instanceof ComponentTag)
200                        {
201                                ComponentTag componentTag = (ComponentTag)next;
202
203                                /**
204                                 * if component tag is for a transparent component and contains "wicket:id", must be
205                                 * queueable.
206                                 */
207                                if (componentTag.containsWicketId()
208                                        && componentTag.getId().startsWith(getWicketRelativePathPrefix(null)))
209                                {
210                                        componentTag.setAutoComponentFactory(FACTORY);
211                                }
212                        }
213                }
214        }
215        
216        private String getWicketRelativePathPrefix(final MarkupStream markupStream)
217        {
218                return getWicketNamespace(markupStream) + WICKET_RELATIVE_PATH_PREFIX_CONTAINER_ID;
219        }
220}