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 * <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 @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}