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