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.io.UnsupportedEncodingException;
023import java.net.URLDecoder;
024import java.net.URLEncoder;
025import java.nio.charset.StandardCharsets;
026import java.util.HashMap;
027import java.util.Iterator;
028import java.util.Map;
029
030import javax.servlet.http.Cookie;
031import javax.servlet.http.HttpServletRequest;
032import javax.servlet.http.HttpServletResponse;
033import javax.servlet.jsp.PageContext;
034import javax.servlet.jsp.tagext.TagSupport;
035
036import org.apache.logging.log4j.LogManager;
037import org.apache.logging.log4j.Logger;
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    public void release()
181    {
182        m_name = m_item = m_var = m_value = m_clear = m_scope = null;
183        super.release();
184    }
185
186    /**
187     * Examines the parameter and returns the corresponding scope identifier:
188     * "request" maps to PageContext.REQUEST_SCOPE, and so on.
189     * Possible values are "page", "session", "application", and "request",
190     * which is the default return value.
191     */
192    private int getScope(final String s )
193    {
194        if( s == null )
195        {
196            return PageContext.REQUEST_SCOPE;
197        }
198        if( "page".equals( m_scope ) )
199        {
200            return PageContext.PAGE_SCOPE;
201        }
202        if( "session".equals( m_scope ) )
203        {
204            return PageContext.SESSION_SCOPE;
205        }
206        if( "application".equals( m_scope ) )
207        {
208            return PageContext.APPLICATION_SCOPE;
209        }
210
211        return PageContext.REQUEST_SCOPE;
212    }
213
214    /**
215     *  {@inheritDoc}
216     */
217    public int doEndTag()
218    {
219        String out = null;
220        final Cookie cookie = findCookie( m_name );
221        boolean changed = false;
222
223        if( m_value != null )
224        {
225            if( m_item != null )
226            {
227                setItemValue( cookie, m_item, m_value );
228            }
229            else
230            {
231                cookie.setValue( m_value );
232            }
233            changed = true;
234        }
235        else
236        {
237            if( m_item != null )
238            {
239                out = getItemValue( cookie, m_item );
240            }
241            else
242            {
243                out = cookie.getValue();
244            }
245        }
246
247        if( out != null )
248        {
249            if( m_var != null )
250            {
251                final int scope = getScope( m_scope );
252                pageContext.setAttribute( m_var, out,  scope );
253            }
254            else
255            {
256                try
257                {
258                    pageContext.getOut().print( out );
259                }
260                catch( final IOException ioe )
261                {
262                    log.warn( "Failed to write to JSP page: " + ioe.getMessage(), ioe );
263                }
264            }
265        }
266
267        Cookie cleared = null;
268        if( m_clear != null )
269        {
270            cleared = findCookie( m_clear );
271            if( m_item != null )
272            {
273                setItemValue( cookie, m_item, null );
274            }
275            else
276            {
277                cleared.setValue( null );
278            }
279        }
280
281        final HttpServletResponse res = (HttpServletResponse)pageContext.getResponse();
282        if( changed )
283        {
284            res.addCookie( cookie );
285        }
286        if( cleared != null )
287        {
288            res.addCookie( cleared );
289        }
290
291        return EVAL_PAGE;
292    }
293
294    /**
295     * Sets a single name-value pair in the given cookie.
296     */
297    private void setItemValue(final Cookie c, final String item, final String value )
298    {
299        if( c == null )
300        {
301            return;
302        }
303        final String in = c.getValue();
304        final Map<String, String> values = parseCookieValues( in );
305        values.put( item, value );
306        final String cv = encodeValues( values );
307        c.setValue( cv );
308    }
309
310    /**
311     * Returns the value of the given item in the cookie.
312     */
313    private String getItemValue(final Cookie c, final String item )
314    {
315        if( c == null || item == null ) {
316            return null;
317        }
318        final String in = c.getValue();
319        final Map< String, String > values = parseCookieValues( in );
320        return values.get( item );
321    }
322
323
324    /**
325     * Parses a cookie value, of format name1%3Fvalue1&name2%3Fvalue2...,
326     * into a Map<String,String>.
327     */
328    private Map<String, String> parseCookieValues(final String s )
329    {
330        final Map< String, String > rval = new HashMap< String, String >();
331        if( s == null ) {
332            return rval;
333        }
334        final String[] nvps = s.split( "&" );
335        if( nvps.length == 0 ) {
336            return rval;
337        }
338        for( int i = 0; i < nvps.length; i++ ) {
339            final String nvp = decode( nvps[i] );
340            final String[] nv = nvp.split( "=" );
341            if( nv[0] != null && !nv[0].trim().isEmpty() )
342            {
343                rval.put( nv[0], nv[1] );
344            }
345        }
346
347        return rval;
348    }
349
350    /**
351     * Encodes name-value pairs in the map into a single string, in a format
352     * understood by this class and JavaScript decodeURIComponent().
353     */
354    private String encodeValues(final Map<String, String> values )
355    {
356        final StringBuilder rval = new StringBuilder();
357        if( values == null || values.size() == 0 ) {
358            return rval.toString();
359        }
360
361        final Iterator< Map.Entry< String, String > > it = values.entrySet().iterator();
362        while( it.hasNext() ) {
363            final Map.Entry< String, String > e = it.next();
364            final String n = e.getKey();
365            final String v = e.getValue();
366            if( v != null ) {
367                final String nv = n + "=" + v;
368                rval.append( encode( nv ) );
369            }
370        }
371
372        return rval.toString();
373    }
374
375    /**
376     * Converts a String to an encoding understood by JavaScript
377     * decodeURIComponent.
378     */
379    private String encode(final String nvp )
380    {
381        String coded = "";
382        try
383        {
384            coded = URLEncoder.encode( nvp, StandardCharsets.UTF_8.name() );
385        }
386        catch( final UnsupportedEncodingException e )
387        {
388            /* never happens */
389            log.info( "Failed to encode UTF-8", e );
390        }
391        return coded.replaceAll( "\\+", "%20" );
392    }
393
394    /**
395     * Converts a cookie value (set by this class, or by a JavaScript
396     * encodeURIComponent call) into a plain string.
397     */
398    private String decode(final String envp )
399    {
400        final String rval;
401        try
402        {
403            rval = URLDecoder.decode( envp , StandardCharsets.UTF_8.name() );
404            return rval;
405        }
406        catch( final UnsupportedEncodingException e )
407        {
408            log.error( "Failed to decode cookie", e );
409            return envp;
410        }
411    }
412
413    /**
414     * Locates the named cookie in the request, or creates a new one if it
415     * doesn't exist.
416     */
417    private Cookie findCookie(final String cname )
418    {
419        final HttpServletRequest req = (HttpServletRequest)pageContext.getRequest();
420        if( req != null )
421        {
422            final Cookie[] cookies = req.getCookies();
423            if( cookies != null )
424            {
425                for( int i = 0; i < cookies.length; i++ )
426                {
427                    if( cookies[i].getName().equals( cname ) )
428                    {
429                        return cookies[i];
430                    }
431                }
432            }
433        }
434
435        return new Cookie( cname, null );
436    }
437
438}