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