    001/*
    002    Licensed to the Apache Software Foundation (ASF) under one
    003    or more contributor license agreements.  See the NOTICE file
    004    distributed with this work for additional information
    005    regarding copyright ownership.  The ASF licenses this file
    006    to you under the Apache License, Version 2.0 (the
    007    "License"); you may not use this file except in compliance
    008    with the License.  You may obtain a copy of the License at
    009
    010       http://www.apache.org/licenses/LICENSE-2.0
    011
    012    Unless required by applicable law or agreed to in writing,
    013    software distributed under the License is distributed on an
    014    "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
    015    KIND, either express or implied.  See the License for the
    016    specific language governing permissions and limitations
    017    under the License.
    018 */
    019package org.apache.wiki.tags;
    020
    021import java.io.IOException;
    022import java.net.URLDecoder;
    023import java.net.URLEncoder;
    024import java.nio.charset.StandardCharsets;
    025import java.util.HashMap;
    026import java.util.Iterator;
    027import java.util.Map;
    028
    029import jakarta.servlet.http.Cookie;
    030import jakarta.servlet.http.HttpServletRequest;
    031import jakarta.servlet.http.HttpServletResponse;
    032import jakarta.servlet.jsp.PageContext;
    033import jakarta.servlet.jsp.tagext.TagSupport;
    034
    035import org.apache.logging.log4j.LogManager;
    036import org.apache.logging.log4j.Logger;
    037import org.apache.wiki.WikiEngine;
    038
    039
    040/**
    041 * Sets or gets Cookie values. This implementation makes the following
    042 * assumptions:
    043 * <ul>
    044 * <li>The cookie contains any number of name-value pairs
    045 * <li>Name-value pairs are separated by "&" in the encoded cookie value string
    046 * <li>An encoded name-value pair is compatible with JavaScript's
    047 * encodeURIComponent(). Notably, spaces are encoded as "%20".
    048 * <li>A decoded name-value pair separates the name and value with a "="
    049 * </ul>
    050 *
    051 * <p>The value of a cookie carrying values n1="v1" and n2="v2 with space"
    052 * would thus be
    053 * <pre>
    054 *   n1%3Dv1&n2%3Dv2%20with%20space
    055 * </pre>
    056 *
    057 * <p>Usage:
    058 *
    059 * <pre>
    060 * &lt;wiki:cookie name="cookiename" var="contextvariable" scope="page" /&gt;
    061 * </pre>
    062 * - Returns the value of the named cookie, or an empty string if not set.
    063 * If 'var' is specified, the value is set into a context variable of this name.
    064 * The 'scope' parameter may be added to specify the context: "session",
    065 * "page", "request". If var is omitted, the output is placed directly into
    066 * the JSP page.
    067 *
    068 * <pre>
    069 * &lt;wiki:cookie name="cookiename" value="encoded_value" /&gt;
    070 * </pre>
    071 * - Sets the named cookie to the given value. If the value string is empty,
    072 * the cookie value is set to empty; otherwise the cookie encoding rules of
    073 * this class must be followed for the value.
    074 *
    075 * <pre>
    076 * &lt;wiki:cookie name="cookiename" item="parameter_name" /&gt;
    077 * </pre>
    078 * - Assumes that the cookie contains URLEncoded name-value pairs,
    079 * with name and value separated by an equals sign, and returns the value
    080 * of the specified item.
    081 *
    082 * &lt;wiki:cookie name="cookiename" item="parameter_name" value="value" /&gt;
    083 * </pre>
    084 * - Sets the value of 'parameter_name' in the named cookie to 'value'.
    085 *
    086 * <pre>
    087 * &lt;wiki:cookie name="cookiename" clear="parameter_name" /&gt;
    088 * </pre>
    089 * - Removes the named parameter from the cookie.
    090 *
    091 * <pre>
    092 * &lt;wiki:cookie clear="cookiename" /&gt;
    093 * </pre>
    094 * - Removes the named cookie. Clear may be used at the same time as a value
    095 * is retrieved (or set, despite the dubious usefulness of that operation).
    096 */
    097public class CookieTag
    098    extends TagSupport
    099{
    100    private static final long serialVersionUID = 0L;
    101
    102    private static final Logger LOG = LogManager.getLogger( CookieTag.class );
    103
    104    /** Name of the cookie value. Required. */
    105    private String m_name;
    106    /** Name of the cookie nvp item. Optional. */
    107    private String m_item;
    108    /** A value to echo or set. Optional. */
    109    private String m_value;
    110    /** Name of a context variable to set result in. Optional, defaults to out.*/
    111    private String m_var;
    112    /** Scope of m_var: request, session, page. */
    113    private String m_scope;
    114    /** Name of a cookie or a cookie nvp to clear. */
    115    private String m_clear;
    116
    117    /**
    118     *  Set the "name" parameter.
    119     *
    120     *  @param s The name.
    121     */
    122    public void setName(final String s )
    123    {
    124        m_name = s;
    125    }
    126
    127    /**
    128     *  Set the "item" parameter.
    129     *
    130     *  @param s The item.
    131     */
    132    public void setItem(final String s )
    133    {
    134        m_item = s;
    135    }
    136
    137    /**
    138     *  Set the "value" parameter.
    139     *
    140     *  @param s The value.
    141     */
    142    public void setValue(final String s )
    143    {
    144        m_value = s;
    145    }
    146
    147    /**
    148     *  Set the "var" parameter.
    149     *
    150     *  @param s The parameter.
    151     */
    152    public void setVar(final String s )
    153    {
    154        m_scope = s;
    155    }
    156
    157    /**
    158     *  Set the "clear" parameter.
    159     *
    160     *  @param s The parameter.
    161     */
    162    public void setClear(final String s )
    163    {
    164        m_clear = s;
    165    }
    166
    167    /**
    168     *  Set the "scope" parameter.
    169     *
    170     *  @param s The scope.
    171     */
    172    public void setScope(final String s )
    173    {
    174        m_scope = s;
    175    }
    176
    177    /**
    178     *  {@inheritDoc}
    179     */
    180    @Override
    181    public void release()
    182    {
    183        m_name = m_item = m_var = m_value = m_clear = m_scope = null;
    184        super.release();
    185    }
    186
    187    /**
    188     * Examines the parameter and returns the corresponding scope identifier:
    189     * "request" maps to PageContext.REQUEST_SCOPE, and so on.
    190     * Possible values are "page", "session", "application", and "request",
    191     * which is the default return value.
    192     */
    193    private int getScope(final String s )
    194    {
    195        if( s == null )
    196        {
    197            return PageContext.REQUEST_SCOPE;
    198        }
    199        if( "page".equals( m_scope ) )
    200        {
    201            return PageContext.PAGE_SCOPE;
    202        }
    203        if( "session".equals( m_scope ) )
    204        {
    205            return PageContext.SESSION_SCOPE;
    206        }
    207        if( "application".equals( m_scope ) )
    208        {
    209            return PageContext.APPLICATION_SCOPE;
    210        }
    211
    212        return PageContext.REQUEST_SCOPE;
    213    }
    214
    215    /**
    216     *  {@inheritDoc}
    217     */
    218    @Override
    219    public int doEndTag()
    220    {
    221        String out = null;
    222        final Cookie cookie = findCookie( m_name );
    223        boolean changed = false;
    224
    225        if( m_value != null )
    226        {
    227            if( m_item != null )
    228            {
    229                setItemValue( cookie, m_item, m_value );
    230            }
    231            else
    232            {
    233                cookie.setValue( m_value );
    234            }
    235            changed = true;
    236        }
    237        else
    238        {
    239            if( m_item != null )
    240            {
    241                out = getItemValue( cookie, m_item );
    242            }
    243            else
    244            {
    245                out = cookie.getValue();
    246            }
    247        }
    248
    249        if( out != null )
    250        {
    251            if( m_var != null )
    252            {
    253                final int scope = getScope( m_scope );
    254                pageContext.setAttribute( m_var, out,  scope );
    255            }
    256            else
    257            {
    258                try
    259                {
    260                    pageContext.getOut().print( out );
    261                }
    262                catch( final IOException ioe )
    263                {
    264                    LOG.warn( "Failed to write to JSP page: " + ioe.getMessage(), ioe );
    265                }
    266            }
    267        }
    268
    269        Cookie cleared = null;
    270        if( m_clear != null )
    271        {
    272            cleared = findCookie( m_clear );
    273            if( m_item != null )
    274            {
    275                setItemValue( cookie, m_item, null );
    276            }
    277            else
    278            {
    279                cleared.setValue( null );
    280            }
    281        }
    282
    283        final HttpServletResponse res = (HttpServletResponse)pageContext.getResponse();
    284        if( changed )
    285        {
    286            if ("true".equalsIgnoreCase(
    287                    WikiEngine.getInstance(pageContext.getServletConfig()).
    288                            getWikiProperties().
    289                            getProperty("jspwiki.securecookie", "false"))) {
    290                cookie.setHttpOnly(true);
    291                cookie.setSecure(true);
    292            }
    293            res.addCookie( cookie );
    294        }
    295        if( cleared != null )
    296        {
    297             if ("true".equalsIgnoreCase(
    298                    WikiEngine.getInstance(pageContext.getServletConfig()).
    299                            getWikiProperties().
    300                            getProperty("jspwiki.securecookie", "false"))) {
    301                cookie.setHttpOnly(true);
    302                cookie.setSecure(true);
    303            }
    304            res.addCookie(cleared);
    305        }
    306
    307        return EVAL_PAGE;
    308    }
    309
    310    /**
    311     * Sets a single name-value pair in the given cookie.
    312     */
    313    private void setItemValue(final Cookie c, final String item, final String value )
    314    {
    315        if( c == null )
    316        {
    317            return;
    318        }
    319        final String in = c.getValue();
    320        final Map<String, String> values = parseCookieValues( in );
    321        values.put( item, value );
    322        final String cv = encodeValues( values );
    323        c.setValue( cv );
    324    }
    325
    326    /**
    327     * Returns the value of the given item in the cookie.
    328     */
    329    private String getItemValue(final Cookie c, final String item )
    330    {
    331        if( c == null || item == null ) {
    332            return null;
    333        }
    334        final String in = c.getValue();
    335        final Map< String, String > values = parseCookieValues( in );
    336        return values.get( item );
    337    }
    338
    339
    340    /**
    341     * Parses a cookie value, of format name1%3Fvalue1&name2%3Fvalue2...,
    342     * into a Map<String,String>.
    343     */
    344    private Map<String, String> parseCookieValues(final String s )
    345    {
    346        final Map< String, String > rval = new HashMap<>();
    347        if( s == null ) {
    348            return rval;
    349        }
    350        final String[] nvps = s.split( "&" );
    351        if( nvps.length == 0 ) {
    352            return rval;
    353        }
    354        for (final String value : nvps) {
    355            final String nvp = decode(value);
    356            final String[] nv = nvp.split("=");
    357            if (nv[0] != null && !nv[0].trim().isEmpty()) {
    358                rval.put(nv[0], nv[1]);
    359            }
    360        }
    361
    362        return rval;
    363    }
    364
    365    /**
    366     * Encodes name-value pairs in the map into a single string, in a format
    367     * understood by this class and JavaScript decodeURIComponent().
    368     */
    369    private String encodeValues(final Map<String, String> values )
    370    {
    371        final StringBuilder rval = new StringBuilder();
    372        if( values == null || values.isEmpty()) {
    373            return rval.toString();
    374        }
    375
    376        final Iterator< Map.Entry< String, String > > it = values.entrySet().iterator();
    377        while( it.hasNext() ) {
    378            final Map.Entry< String, String > e = it.next();
    379            final String n = e.getKey();
    380            final String v = e.getValue();
    381            if( v != null ) {
    382                final String nv = n + "=" + v;
    383                rval.append( encode( nv ) );
    384            }
    385        }
    386
    387        return rval.toString();
    388    }
    389
    390    /**
    391     * Converts a String to an encoding understood by JavaScript
    392     * decodeURIComponent.
    393     */
    394    private String encode(final String nvp )
    395    {
    396        final String coded = URLEncoder.encode( nvp, StandardCharsets.UTF_8 );
    397        return coded.replaceAll( "\\+", "%20" );
    398    }
    399
    400    /**
    401     * Converts a cookie value (set by this class, or by a JavaScript
    402     * encodeURIComponent call) into a plain string.
    403     */
    404    private String decode(final String envp )
    405    {
    406        final String rval;
    407        rval = URLDecoder.decode( envp , StandardCharsets.UTF_8);
    408        return rval;
    409    }
    410
    411    /**
    412     * Locates the named cookie in the request, or creates a new one if it
    413     * doesn't exist.
    414     */
    415    private Cookie findCookie(final String cname )
    416    {
    417        final HttpServletRequest req = (HttpServletRequest)pageContext.getRequest();
    418        if( req != null )
    419        {
    420            final Cookie[] cookies = req.getCookies();
    421            if( cookies != null )
    422            {
    423                for (final Cookie cookie : cookies) {
    424                    if (cookie.getName().equals(cname)) {
    425                        return cookie;
    426                    }
    427                }
    428            }
    429        }
    430
    431        return new Cookie( cname, null );
    432    }
    433
    434}