package votorola.g.web; // Copyright 2012, Michael Allan. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Votorola Software"), to deal in the Votorola Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicence, and/or sell copies of the Votorola Software, and to permit persons to whom the Votorola Software is furnished to do so, subject to the following conditions: The preceding copyright notice and this permission notice shall be included in all copies or substantial portions of the Votorola Software. THE VOTOROLA SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE VOTOROLA SOFTWARE OR THE USE OR OTHER DEALINGS IN THE VOTOROLA SOFTWARE. import java.io.*; import java.net.*; import javax.servlet.http.*; import votorola.g.lang.*; /** An extended cookie implementation that rejects illegal values. The superclass accepts * illegal values and depending on the servlet container may encode them by RFC 2965 * quoting, which is not supported by all browsers. So we do our own testing and * encoding here in compliance with both the original and existing standards. * * @see Persistent client state: HTTP cookies * @see RFC 2965 * @see Allowed characters in cookies */ public class CookieX extends Cookie { /** Constructs a CookieX without encoding the value. * * @see #getName() * @see #getValue() * * @throws IllegalArgumentException if either the name or value contains an * illegal character. * @see Cookie#Cookie(String,String) */ public CookieX( String name, String value ) { super( name, testedValue(value) ); } /** Constructs a CookieX. * * @see #getName() * @see #getValue() * @param toEncode whether to {@linkplain #encodedValue(String) encode the * value}. If * * @throws IllegalArgumentException if the name contains an illegal character, or * if the value contains an illegal character and assertions are enabled. * @see Cookie#Cookie(String,String) */ public CookieX( final String name, String value, final boolean toEncode ) { super( name, toEncode? value=encodedValue(value): value ); // testName( name ); /// superclass does that already assert test( value ); // test only when assertions enabled } // ------------------------------------------------------------------------------------ /** Decodes a previously {@link #encodedValue encodedValue} and returns the result. * * @param encodedValue the value to decode. Null values decode to null. * * @see #encodedValue(String) */ public static @ThreadSafe String decodedValue( final String encodedValue ) { final String decodedValue; if( encodedValue == null ) decodedValue = null; else { try { decodedValue = URLDecoder.decode( encodedValue, "UTF-8" ); } catch( UnsupportedEncodingException x ) { throw new RuntimeException( x ); } } return decodedValue; } /** A max-age value of roughly one year as measured in seconds. */ public static final int DURATION_YEAR_S = 86400/*s per day*/ * 365/*day*/; /** Encodes the value such that it has no illegal characters and returns the result. * This method uses URI "percent" escaping based on UTF-8 ordinals for any non-ASCII * characters. This is also the encoding method recommended for URIs. * * @param decodedValue the value to encode. Null values encode to null. * * @see #decodedValue(String) * @see Non-ASCII characters in URI attribute values */ public static @ThreadSafe String encodedValue( final String decodedValue ) { // encoded name would be similar, provided it encodes any leading '$' char final String encodedValue; if( decodedValue == null ) encodedValue = null; else { try { encodedValue = URLEncoder.encode( decodedValue, "UTF-8" ); } // encodes all illegal characters (tested JDK 1.7) catch( UnsupportedEncodingException x ) { throw new RuntimeException( x ); } } return encodedValue; } // - C o o k i e ---------------------------------------------------------------------- /** @throws IllegalArgumentException if the value contains an illegal character. */ public @Override final void setValue( final String value ) { testedValue( value ); super.setValue( value ); } /** @throws IllegalArgumentException if the value contains an illegal character * and assertions are enabled. */ public final void setValue( String value, final boolean toEncode ) { if( toEncode ) value = encodedValue( value ); assert test( value ); // test only when assertions enabled super.setValue( value ); } /** @throws IllegalArgumentException if the version is not zero. The argument * checking performed by this class is probably not necessary for later versions. */ public @Override final void setVersion( final int version ) { if( version != 0 ) throw new IllegalArgumentException( "non-zero version: " + version ); super.setVersion( version ); } //// P r i v a t e /////////////////////////////////////////////////////////////////////// private static boolean test( final String value ) { testedValue( value ); return true; } private static String testedValue( final String value ) { // Testing a name would be the same except that a leading '$' char is also illegal // per RFC 2965 3.2.2. But superclass constructor does its own name testing. if( value != null ) { final int cN = value.length(); if( cN == 0 ) throw new IllegalArgumentException( "empty cookie value" ); // API for #setValue says, "Empty values may not behave the same way on all browsers." for( int c = 0;; ) { char ch = value.charAt( c ); if( ch < '!' || ch > '~' // only ASCII [1] and no control characters [2] || ch == '"' // [2] || ch == ',' // [1] [2] || ch >= ':' && ch <= '@' // [2] : ; < = > ? @ || ch >= '[' && ch <= ']' // [2] [ \ ] || ch == '(' || ch == ')' // [2] || ch == '/' // [2] || ch == '{' || ch == '}' // [2] || Character.isWhitespace( ch )) // [1], and [2] disallows the ASCII space { throw new IllegalArgumentException( "illegal character in cookie value : '" + ch + "'" ); } ++c; if( c >= cN ) break; } } return value; // [1] Per Netscape. // [2] Per RFC 2965, which requires token form per RFC 2616. } }