001    // Copyright 2009, 2010, 2011, 2012 The Apache Software Foundation
002    //
003    // Licensed under the Apache License, Version 2.0 (the "License");
004    // you may not use this file except in compliance with the License.
005    // You may obtain a copy of the License at
006    //
007    // http://www.apache.org/licenses/LICENSE-2.0
008    //
009    // Unless required by applicable law or agreed to in writing, software
010    // distributed under the License is distributed on an "AS IS" BASIS,
011    // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
012    // See the License for the specific language governing permissions and
013    // limitations under the License.
014    
015    package org.apache.tapestry5.internal.services;
016    
017    import org.apache.tapestry5.*;
018    import org.apache.tapestry5.internal.InternalConstants;
019    import org.apache.tapestry5.internal.TapestryInternalUtils;
020    import org.apache.tapestry5.ioc.annotations.Symbol;
021    import org.apache.tapestry5.ioc.internal.util.CollectionFactory;
022    import org.apache.tapestry5.ioc.internal.util.InternalUtils;
023    import org.apache.tapestry5.services.*;
024    import org.apache.tapestry5.services.security.ClientWhitelist;
025    
026    import java.util.List;
027    import java.util.Locale;
028    
029    public class ComponentEventLinkEncoderImpl implements ComponentEventLinkEncoder
030    {
031        private final ComponentClassResolver componentClassResolver;
032    
033        private final ContextPathEncoder contextPathEncoder;
034    
035        private final LocalizationSetter localizationSetter;
036    
037        private final Request request;
038    
039        private final Response response;
040    
041        private final RequestSecurityManager requestSecurityManager;
042    
043        private final BaseURLSource baseURLSource;
044    
045        private final PersistentLocale persistentLocale;
046    
047        private final boolean encodeLocaleIntoPath;
048    
049        private final MetaDataLocator metaDataLocator;
050    
051        private final ClientWhitelist clientWhitelist;
052    
053        private final String applicationFolder;
054    
055        private final String applicationFolderPrefix;
056    
057        private static final int BUFFER_SIZE = 100;
058    
059        private static final char SLASH = '/';
060    
061        public ComponentEventLinkEncoderImpl(ComponentClassResolver componentClassResolver,
062                                             ContextPathEncoder contextPathEncoder, LocalizationSetter localizationSetter, Request request,
063                                             Response response, RequestSecurityManager requestSecurityManager, BaseURLSource baseURLSource,
064                                             PersistentLocale persistentLocale, @Symbol(SymbolConstants.ENCODE_LOCALE_INTO_PATH)
065        boolean encodeLocaleIntoPath, @Symbol(SymbolConstants.APPLICATION_FOLDER) String applicationFolder, MetaDataLocator metaDataLocator, ClientWhitelist clientWhitelist)
066        {
067            this.componentClassResolver = componentClassResolver;
068            this.contextPathEncoder = contextPathEncoder;
069            this.localizationSetter = localizationSetter;
070            this.request = request;
071            this.response = response;
072            this.requestSecurityManager = requestSecurityManager;
073            this.baseURLSource = baseURLSource;
074            this.persistentLocale = persistentLocale;
075            this.encodeLocaleIntoPath = encodeLocaleIntoPath;
076            this.applicationFolder = applicationFolder;
077            this.metaDataLocator = metaDataLocator;
078            this.clientWhitelist = clientWhitelist;
079    
080            boolean hasAppFolder = applicationFolder.equals("");
081    
082            applicationFolderPrefix = hasAppFolder ? null : SLASH + applicationFolder;
083        }
084    
085        public Link createPageRenderLink(PageRenderRequestParameters parameters)
086        {
087            StringBuilder builder = new StringBuilder(BUFFER_SIZE);
088    
089            // Build up the absolute URI.
090    
091            String activePageName = parameters.getLogicalPageName();
092    
093            builder.append(request.getContextPath());
094    
095            encodeAppFolderAndLocale(builder);
096    
097            builder.append(SLASH);
098    
099            String encodedPageName = encodePageName(activePageName);
100    
101            builder.append(encodedPageName);
102    
103            appendContext(encodedPageName.length() > 0, parameters.getActivationContext(), builder);
104    
105            Link link = new LinkImpl(builder.toString(), false, requestSecurityManager.checkPageSecurity(activePageName),
106                    response, contextPathEncoder, baseURLSource);
107    
108            if (parameters.isLoopback())
109            {
110                link.addParameter(TapestryConstants.PAGE_LOOPBACK_PARAMETER_NAME, "t");
111            }
112    
113            return link;
114        }
115    
116        private void encodeAppFolderAndLocale(StringBuilder builder)
117        {
118            if (!applicationFolder.equals(""))
119            {
120                builder.append(SLASH).append(applicationFolder);
121            }
122    
123            if (encodeLocaleIntoPath)
124            {
125                Locale locale = persistentLocale.get();
126    
127                if (locale != null)
128                {
129                    builder.append(SLASH);
130                    builder.append(locale.toString());
131                }
132            }
133        }
134    
135        private String encodePageName(String pageName)
136        {
137            if (pageName.equalsIgnoreCase("index"))
138                return "";
139    
140            String encoded = pageName.toLowerCase();
141    
142            if (!encoded.endsWith("/index"))
143                return encoded;
144    
145            return encoded.substring(0, encoded.length() - 6);
146        }
147    
148        public Link createComponentEventLink(ComponentEventRequestParameters parameters, boolean forForm)
149        {
150            StringBuilder builder = new StringBuilder(BUFFER_SIZE);
151    
152            // Build up the absolute URI.
153    
154            String activePageName = parameters.getActivePageName();
155            String containingPageName = parameters.getContainingPageName();
156            String eventType = parameters.getEventType();
157    
158            String nestedComponentId = parameters.getNestedComponentId();
159            boolean hasComponentId = InternalUtils.isNonBlank(nestedComponentId);
160    
161            builder.append(request.getContextPath());
162    
163            encodeAppFolderAndLocale(builder);
164    
165            builder.append(SLASH);
166            builder.append(activePageName.toLowerCase());
167    
168            if (hasComponentId)
169            {
170                builder.append('.');
171                builder.append(nestedComponentId);
172            }
173    
174            if (!hasComponentId || !eventType.equals(EventConstants.ACTION))
175            {
176                builder.append(":");
177                builder.append(encodePageName(eventType));
178            }
179    
180            appendContext(true, parameters.getEventContext(), builder);
181    
182            Link result = new LinkImpl(builder.toString(), forForm,
183                    requestSecurityManager.checkPageSecurity(activePageName), response, contextPathEncoder, baseURLSource);
184    
185            EventContext pageActivationContext = parameters.getPageActivationContext();
186    
187            if (pageActivationContext.getCount() != 0)
188            {
189                // Reuse the builder
190                builder.setLength(0);
191                appendContext(true, pageActivationContext, builder);
192    
193                // Omit that first slash
194                result.addParameter(InternalConstants.PAGE_CONTEXT_NAME, builder.substring(1));
195            }
196    
197            // TAPESTRY-2044: Sometimes the active page drags in components from another page and we
198            // need to differentiate that.
199    
200            if (!containingPageName.equalsIgnoreCase(activePageName))
201                result.addParameter(InternalConstants.CONTAINER_PAGE_NAME, encodePageName(containingPageName));
202    
203            return result;
204        }
205    
206        /**
207         * Splits path at slashes into a <em>mutable</em> list of strings. Empty terms, including the
208         * expected leading term (paths start with a '/') are dropped.
209         *
210         * @param path
211         * @return mutable list of path elements
212         */
213        private List<String> splitPath(String path)
214        {
215            String[] split = TapestryInternalUtils.splitPath(path);
216    
217            List<String> result = CollectionFactory.newList();
218    
219            for (String name : split)
220            {
221                if (name.length() > 0)
222                {
223                    result.add(name);
224                }
225            }
226    
227            return result;
228        }
229    
230        private String joinPath(List<String> path)
231        {
232            if (path.isEmpty())
233            {
234                return "";
235            }
236    
237            StringBuilder builder = new StringBuilder(100);
238            String sep = "";
239    
240            for (String term : path)
241            {
242                builder.append(sep).append(term);
243                sep = "/";
244            }
245    
246            return builder.toString();
247        }
248    
249        private String peekFirst(List<String> path)
250        {
251            if (path.size() == 0)
252            {
253                return null;
254            }
255    
256            return path.get(0);
257        }
258    
259        public ComponentEventRequestParameters decodeComponentEventRequest(Request request)
260        {
261            String explicitLocale = null;
262    
263            // Split the path around slashes into a mutable list of terms, which will be consumed term by term.
264    
265            List<String> path = splitPath(request.getPath());
266    
267            if (this.applicationFolder.length() > 0)
268            {
269                // TODO: Should this be case insensitive
270    
271                String inPath = path.remove(0);
272    
273                if (!inPath.equals(this.applicationFolder))
274                {
275                    return null;
276                }
277            }
278    
279            if (path.isEmpty())
280            {
281                return null;
282            }
283    
284            // Next up: the locale (which is optional)
285    
286            String potentialLocale = path.get(0);
287    
288            if (localizationSetter.isSupportedLocaleName(potentialLocale))
289            {
290                explicitLocale = potentialLocale;
291                path.remove(0);
292            }
293    
294            StringBuilder pageName = new StringBuilder(100);
295            String sep = "";
296    
297            while (!path.isEmpty())
298            {
299                String name = path.remove(0);
300                String eventType = EventConstants.ACTION;
301                String nestedComponentId = "";
302    
303                boolean found = false;
304    
305                // First, look for an explicit action name.
306    
307                int colonx = name.lastIndexOf(':');
308    
309                if (colonx > 0)
310                {
311                    found = true;
312                    eventType = name.substring(colonx + 1);
313                    name = name.substring(0, colonx);
314                }
315    
316                int dotx = name.indexOf('.');
317    
318                if (dotx > 0)
319                {
320                    found = true;
321                    nestedComponentId = name.substring(dotx + 1);
322                    name = name.substring(0, dotx);
323                }
324    
325                pageName.append(sep).append(name);
326    
327                if (found)
328                {
329                    ComponentEventRequestParameters result = validateAndConstructComponentEventRequest(request, pageName.toString(), nestedComponentId, eventType, path);
330    
331                    if (result == null)
332                    {
333                        return result;
334                    }
335    
336                    if (explicitLocale == null)
337                    {
338                        setLocaleFromRequest(request);
339                    } else
340                    {
341                        localizationSetter.setLocaleFromLocaleName(explicitLocale);
342                    }
343    
344                    return result;
345                }
346    
347                // Continue on to the next name in the path
348                sep = "/";
349            }
350    
351            // Path empty before finding something that looks like a component id or event name, so
352            // it is not a component event request.
353    
354            return null;
355        }
356    
357        private ComponentEventRequestParameters validateAndConstructComponentEventRequest(Request request, String pageName, String nestedComponentId, String eventType, List<String> remainingPath)
358        {
359            if (!componentClassResolver.isPageName(pageName))
360            {
361                return null;
362            }
363    
364            String activePageName = componentClassResolver.canonicalizePageName(pageName);
365    
366            if (isWhitelistOnlyAndNotValid(activePageName))
367            {
368                return null;
369            }
370    
371            String value = request.getParameter(InternalConstants.CONTAINER_PAGE_NAME);
372    
373            String containingPageName = value == null
374                    ? activePageName
375                    : componentClassResolver.canonicalizePageName(value);
376    
377            EventContext eventContext = contextPathEncoder.decodePath(joinPath(remainingPath));
378            EventContext activationContext = contextPathEncoder.decodePath(request.getParameter(InternalConstants.PAGE_CONTEXT_NAME));
379    
380            return new ComponentEventRequestParameters(activePageName, containingPageName, nestedComponentId, eventType,
381                    activationContext, eventContext);
382        }
383    
384        private void setLocaleFromRequest(Request request)
385        {
386            Locale locale = request.getLocale();
387    
388            // And explicit locale will have invoked setLocaleFromLocaleName().
389    
390            localizationSetter.setNonPeristentLocaleFromLocaleName(locale.toString());
391        }
392    
393        public PageRenderRequestParameters decodePageRenderRequest(Request request)
394        {
395            boolean explicitLocale = false;
396    
397            // The extended name may include a page activation context. The trick is
398            // to figure out where the logical page name stops and where the
399            // activation context begins. Further, strip out the leading slash.
400    
401            String path = request.getPath();
402    
403            if (applicationFolderPrefix != null)
404            {
405                int prefixLength = applicationFolderPrefix.length();
406    
407                assert path.substring(0, prefixLength).equalsIgnoreCase(applicationFolderPrefix);
408    
409                // This checks that the character after the prefix is a slash ... the extra complexity
410                // only seems to occur in Selenium. There's some ambiguity about what to do with a request for
411                // the application folder that doesn't end with a slash. Manuyal with Chrome and IE 8 shows that such
412                // requests are passed through with a training slash,  automated testing with Selenium and FireFox
413                // can include requests for the folder without the trailing slash.
414    
415                assert path.length() <= prefixLength || path.charAt(prefixLength) == '/';
416    
417                // Strip off the folder prefix (i.e., "/foldername"), leaving the rest of the path (i.e., "/en/pagename").
418    
419                path = path.substring(prefixLength);
420            }
421    
422    
423            // TAPESTRY-1343: Sometimes path is the empty string (it should always be at least a slash,
424            // but Tomcat may return the empty string for a root context request).
425    
426            String extendedName = path.length() == 0 ? path : path.substring(1);
427    
428            // Ignore trailing slashes in the path.
429            while (extendedName.endsWith("/"))
430            {
431                extendedName = extendedName.substring(0, extendedName.length() - 1);
432            }
433    
434            int slashx = extendedName.indexOf('/');
435    
436            // So, what can we have left?
437            // 1. A page name
438            // 2. A locale followed by a page name
439            // 3. A page name followed by activation context
440            // 4. A locale name, page name, activation context
441            // 5. Just activation context (for root Index page)
442            // 6. A locale name followed by activation context
443    
444            String possibleLocaleName = slashx > 0 ? extendedName.substring(0, slashx) : extendedName;
445    
446            if (localizationSetter.setLocaleFromLocaleName(possibleLocaleName))
447            {
448                extendedName = slashx > 0 ? extendedName.substring(slashx + 1) : "";
449                explicitLocale = true;
450            }
451    
452            slashx = extendedName.length();
453            boolean atEnd = true;
454    
455            while (slashx > 0)
456            {
457                String pageName = extendedName.substring(0, slashx);
458                String pageActivationContext = atEnd ? "" : extendedName.substring(slashx + 1);
459    
460                PageRenderRequestParameters parameters = checkIfPage(request, pageName, pageActivationContext);
461    
462                if (parameters != null)
463                {
464                    return parameters;
465                }
466    
467                // Work backwards, splitting at the next slash.
468                slashx = extendedName.lastIndexOf('/', slashx - 1);
469    
470                atEnd = false;
471            }
472    
473            // OK, maybe its all page activation context for the root Index page.
474    
475            PageRenderRequestParameters result = checkIfPage(request, "", extendedName);
476    
477            if (result != null && !explicitLocale)
478            {
479                setLocaleFromRequest(request);
480            }
481    
482            return result;
483        }
484    
485        private PageRenderRequestParameters checkIfPage(Request request, String pageName, String pageActivationContext)
486        {
487            if (!componentClassResolver.isPageName(pageName))
488            {
489                return null;
490            }
491    
492            String canonicalized = componentClassResolver.canonicalizePageName(pageName);
493    
494            // If the page is only visible to the whitelist, but the request is not on the whitelist, then
495            // pretend the page doesn't exist!
496            if (isWhitelistOnlyAndNotValid(canonicalized))
497            {
498                return null;
499            }
500    
501            EventContext activationContext = contextPathEncoder.decodePath(pageActivationContext);
502    
503            boolean loopback = request.getParameter(TapestryConstants.PAGE_LOOPBACK_PARAMETER_NAME) != null;
504    
505            return new PageRenderRequestParameters(canonicalized, activationContext, loopback);
506        }
507    
508        private boolean isWhitelistOnlyAndNotValid(String canonicalized)
509        {
510            return metaDataLocator.findMeta(MetaDataConstants.WHITELIST_ONLY_PAGE, canonicalized, boolean.class) &&
511                    !clientWhitelist.isClientRequestOnWhitelist();
512        }
513    
514        public void appendContext(boolean seperatorRequired, EventContext context, StringBuilder builder)
515        {
516            String encoded = contextPathEncoder.encodeIntoPath(context);
517    
518            if (encoded.length() > 0)
519            {
520                if (seperatorRequired)
521                {
522                    builder.append(SLASH);
523                }
524    
525                builder.append(encoded);
526            }
527        }
528    }