diff --git a/src/main/java/org/json/Cookie.java b/src/main/java/org/json/Cookie.java index 5da423a87..a43d1eddd 100644 --- a/src/main/java/org/json/Cookie.java +++ b/src/main/java/org/json/Cookie.java @@ -1,5 +1,7 @@ package org.json; +import java.util.Locale; + /* Copyright (c) 2002 JSON.org @@ -27,6 +29,7 @@ of this software and associated documentation files (the "Software"), to deal /** * Convert a web browser cookie specification to a JSONObject and back. * JSON and Cookies are both notations for name/value pairs. + * See also: https://tools.ietf.org/html/rfc6265 * @author JSON.org * @version 2015-12-09 */ @@ -65,41 +68,65 @@ public static String escape(String string) { /** * Convert a cookie specification string into a JSONObject. The string - * will contain a name value pair separated by '='. The name and the value + * must contain a name value pair separated by '='. The name and the value * will be unescaped, possibly converting '+' and '%' sequences. The * cookie properties may follow, separated by ';', also represented as - * name=value (except the secure property, which does not have a value). + * name=value (except the Attribute properties like "Secure" or "HttpOnly", + * which do not have a value. The value {@link Boolean#TRUE} will be used for these). * The name will be stored under the key "name", and the value will be * stored under the key "value". This method does not do checking or * validation of the parameters. It only converts the cookie string into - * a JSONObject. + * a JSONObject. All attribute names are converted to lower case keys in the + * JSONObject (HttpOnly => httponly). If an attribute is specified more than + * once, only the value found closer to the end of the cookie-string is kept. * @param string The cookie specification string. * @return A JSONObject containing "name", "value", and possibly other * members. - * @throws JSONException if a called function fails or a syntax error + * @throws JSONException If there is an error parsing the Cookie String. + * Cookie strings must have at least one '=' character and the 'name' + * portion of the cookie must not be blank. */ - public static JSONObject toJSONObject(String string) throws JSONException { + public static JSONObject toJSONObject(String string) { + final JSONObject jo = new JSONObject(); String name; - JSONObject jo = new JSONObject(); Object value; + + JSONTokener x = new JSONTokener(string); - jo.put("name", x.nextTo('=')); + + name = unescape(x.nextTo('=').trim()); + //per RFC6265, if the name is blank, the cookie should be ignored. + if("".equals(name)) { + throw new JSONException("Cookies must have a 'name'"); + } + jo.put("name", name); + // per RFC6265, if there is no '=', the cookie should be ignored. + // the 'next' call here throws an exception if the '=' is not found. x.next('='); - jo.put("value", x.nextTo(';')); + jo.put("value", unescape(x.nextTo(';')).trim()); + // discard the ';' x.next(); + // parse the remaining cookie attributes while (x.more()) { - name = unescape(x.nextTo("=;")); + name = unescape(x.nextTo("=;")).trim().toLowerCase(Locale.ROOT); + // don't allow a cookies attributes to overwrite it's name or value. + if("name".equalsIgnoreCase(name)) { + throw new JSONException("Illegal attribute name: 'name'"); + } + if("value".equalsIgnoreCase(name)) { + throw new JSONException("Illegal attribute name: 'value'"); + } + // check to see if it's a flag property if (x.next() != '=') { - if (name.equals("secure")) { - value = Boolean.TRUE; - } else { - throw x.syntaxError("Missing '=' in cookie parameter."); - } + value = Boolean.TRUE; } else { - value = unescape(x.nextTo(';')); + value = unescape(x.nextTo(';')).trim(); x.next(); } - jo.put(name, value); + // only store non-blank attributes + if(!"".equals(name) && !"".equals(value)) { + jo.put(name, value); + } } return jo; } @@ -107,35 +134,63 @@ public static JSONObject toJSONObject(String string) throws JSONException { /** * Convert a JSONObject into a cookie specification string. The JSONObject - * must contain "name" and "value" members. - * If the JSONObject contains "expires", "domain", "path", or "secure" - * members, they will be appended to the cookie specification string. - * All other members are ignored. + * must contain "name" and "value" members (case insensitive). + * If the JSONObject contains other members, they will be appended to the cookie + * specification string. User-Agents are instructed to ignore unknown attributes, + * so ensure your JSONObject is using only known attributes. + * See also: https://tools.ietf.org/html/rfc6265 * @param jo A JSONObject * @return A cookie specification string - * @throws JSONException if a called function fails + * @throws JSONException thrown if the cookie has no name. */ public static String toString(JSONObject jo) throws JSONException { StringBuilder sb = new StringBuilder(); - - sb.append(escape(jo.getString("name"))); - sb.append("="); - sb.append(escape(jo.getString("value"))); - if (jo.has("expires")) { - sb.append(";expires="); - sb.append(jo.getString("expires")); + + String name = null; + Object value = null; + for(String key : jo.keySet()){ + if("name".equalsIgnoreCase(key)) { + name = jo.getString(key).trim(); + } + if("value".equalsIgnoreCase(key)) { + value=jo.getString(key).trim(); + } + if(name != null && value != null) { + break; + } } - if (jo.has("domain")) { - sb.append(";domain="); - sb.append(escape(jo.getString("domain"))); + + if(name == null || "".equals(name.trim())) { + throw new JSONException("Cookie does not have a name"); } - if (jo.has("path")) { - sb.append(";path="); - sb.append(escape(jo.getString("path"))); + if(value == null) { + value = ""; } - if (jo.optBoolean("secure")) { - sb.append(";secure"); + + sb.append(escape(name)); + sb.append("="); + sb.append(escape((String)value)); + + for(String key : jo.keySet()){ + if("name".equalsIgnoreCase(key) + || "value".equalsIgnoreCase(key)) { + // already processed above + continue; + } + value = jo.opt(key); + if(value instanceof Boolean) { + if(Boolean.TRUE.equals(value)) { + sb.append(';').append(escape(key)); + } + // don't emit false values + } else { + sb.append(';') + .append(escape(key)) + .append('=') + .append(escape(value.toString())); + } } + return sb.toString(); } diff --git a/src/test/java/org/json/junit/CookieTest.java b/src/test/java/org/json/junit/CookieTest.java index 74756aadd..7e7b62b45 100644 --- a/src/test/java/org/json/junit/CookieTest.java +++ b/src/test/java/org/json/junit/CookieTest.java @@ -79,32 +79,46 @@ public void malFormedNameValueException() { * Expects a JSONException. */ @Test - public void malFormedAttributeException() { + public void booleanAttribute() { String cookieStr = "this=Cookie;myAttribute"; + JSONObject jo = Cookie.toJSONObject(cookieStr); + assertTrue("has key 'name'", jo.has("name")); + assertTrue("has key 'value'", jo.has("value")); + assertTrue("has key 'myAttribute'", jo.has("myattribute")); + } + + /** + * Attempts to create a JSONObject from an empty cookie string.
+ * Note: Cookie throws an exception, but CookieList does not.
+ * Expects a JSONException + */ + @Test + public void emptyStringCookieException() { + String cookieStr = ""; try { Cookie.toJSONObject(cookieStr); fail("Expecting an exception"); } catch (JSONException e) { assertEquals("Expecting an exception message", - "Missing '=' in cookie parameter. at 23 [character 24 line 1]", + "Cookies must have a 'name'", e.getMessage()); } } - /** - * Attempts to create a JSONObject from an empty cookie string.
+ * + * Attempts to create a JSONObject from an cookie string where the name is blank.
* Note: Cookie throws an exception, but CookieList does not.
* Expects a JSONException */ @Test - public void emptyStringCookieException() { - String cookieStr = ""; + public void emptyNameCookieException() { + String cookieStr = " = value "; try { Cookie.toJSONObject(cookieStr); fail("Expecting an exception"); } catch (JSONException e) { assertEquals("Expecting an exception message", - "Expected '=' and instead saw '' at 0 [character 1 line 1]", + "Cookies must have a 'name'", e.getMessage()); } } @@ -149,8 +163,8 @@ public void multiPartCookie() { } /** - * Cookie.toString() will omit the non-standard "thiswont=beIncluded" - * attribute, but the attribute is still stored in the JSONObject. + * Cookie.toString() will emit the non-standard "thiswont=beIncluded" + * attribute, and the attribute is still stored in the JSONObject. * This test confirms both behaviors. */ @Test @@ -163,15 +177,15 @@ public void convertCookieToString() { "thisWont=beIncluded;"+ "secure"; String expectedCookieStr = - "{\"path\":\"/\","+ + "{\"thiswont\":\"beIncluded\","+ + "\"path\":\"/\","+ "\"expires\":\"Wed, 19-Mar-2014 17:53:53 GMT\","+ "\"domain\":\".yahoo.com\","+ "\"name\":\"PH\","+ "\"secure\":true,"+ "\"value\":\"deleted\"}"; // Add the nonstandard attribute to the expected cookie string - String expectedDirectCompareCookieStr = - expectedCookieStr.replaceAll("\\{", "\\{\"thisWont\":\"beIncluded\","); + String expectedDirectCompareCookieStr = expectedCookieStr; // convert all strings into JSONObjects JSONObject jsonObject = Cookie.toJSONObject(cookieStr); JSONObject expectedJsonObject = new JSONObject(expectedCookieStr);