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 * <wiki:cookie name="cookiename" var="contextvariable" scope="page" /> 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 * <wiki:cookie name="cookiename" value="encoded_value" /> 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 * <wiki:cookie name="cookiename" item="parameter_name" /> 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 * <wiki:cookie name="cookiename" item="parameter_name" value="value" /> 083 * </pre> 084 * - Sets the value of 'parameter_name' in the named cookie to 'value'. 085 * 086 * <pre> 087 * <wiki:cookie name="cookiename" clear="parameter_name" /> 088 * </pre> 089 * - Removes the named parameter from the cookie. 090 * 091 * <pre> 092 * <wiki:cookie clear="cookiename" /> 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}