    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    }