Skip to content

Commit fee6ddb

Browse files
authored
Merge pull request #521 from johnjaylward/CookieFlagSupport
Updates Cookie class to be more generic in attribute parsing and emit.
2 parents 5a32114 + 6029dec commit fee6ddb

File tree

2 files changed

+117
-48
lines changed

2 files changed

+117
-48
lines changed

src/main/java/org/json/Cookie.java

Lines changed: 91 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package org.json;
22

3+
import java.util.Locale;
4+
35
/*
46
Copyright (c) 2002 JSON.org
57
@@ -27,6 +29,7 @@ of this software and associated documentation files (the "Software"), to deal
2729
/**
2830
* Convert a web browser cookie specification to a JSONObject and back.
2931
* JSON and Cookies are both notations for name/value pairs.
32+
* See also: <a href="https://tools.ietf.org/html/rfc6265">https://tools.ietf.org/html/rfc6265</a>
3033
* @author JSON.org
3134
* @version 2015-12-09
3235
*/
@@ -65,77 +68,129 @@ public static String escape(String string) {
6568

6669
/**
6770
* Convert a cookie specification string into a JSONObject. The string
68-
* will contain a name value pair separated by '='. The name and the value
71+
* must contain a name value pair separated by '='. The name and the value
6972
* will be unescaped, possibly converting '+' and '%' sequences. The
7073
* cookie properties may follow, separated by ';', also represented as
71-
* name=value (except the secure property, which does not have a value).
74+
* name=value (except the Attribute properties like "Secure" or "HttpOnly",
75+
* which do not have a value. The value {@link Boolean#TRUE} will be used for these).
7276
* The name will be stored under the key "name", and the value will be
7377
* stored under the key "value". This method does not do checking or
7478
* validation of the parameters. It only converts the cookie string into
75-
* a JSONObject.
79+
* a JSONObject. All attribute names are converted to lower case keys in the
80+
* JSONObject (HttpOnly =&gt; httponly). If an attribute is specified more than
81+
* once, only the value found closer to the end of the cookie-string is kept.
7682
* @param string The cookie specification string.
7783
* @return A JSONObject containing "name", "value", and possibly other
7884
* members.
79-
* @throws JSONException if a called function fails or a syntax error
85+
* @throws JSONException If there is an error parsing the Cookie String.
86+
* Cookie strings must have at least one '=' character and the 'name'
87+
* portion of the cookie must not be blank.
8088
*/
81-
public static JSONObject toJSONObject(String string) throws JSONException {
89+
public static JSONObject toJSONObject(String string) {
90+
final JSONObject jo = new JSONObject();
8291
String name;
83-
JSONObject jo = new JSONObject();
8492
Object value;
93+
94+
8595
JSONTokener x = new JSONTokener(string);
86-
jo.put("name", x.nextTo('='));
96+
97+
name = unescape(x.nextTo('=').trim());
98+
//per RFC6265, if the name is blank, the cookie should be ignored.
99+
if("".equals(name)) {
100+
throw new JSONException("Cookies must have a 'name'");
101+
}
102+
jo.put("name", name);
103+
// per RFC6265, if there is no '=', the cookie should be ignored.
104+
// the 'next' call here throws an exception if the '=' is not found.
87105
x.next('=');
88-
jo.put("value", x.nextTo(';'));
106+
jo.put("value", unescape(x.nextTo(';')).trim());
107+
// discard the ';'
89108
x.next();
109+
// parse the remaining cookie attributes
90110
while (x.more()) {
91-
name = unescape(x.nextTo("=;"));
111+
name = unescape(x.nextTo("=;")).trim().toLowerCase(Locale.ROOT);
112+
// don't allow a cookies attributes to overwrite it's name or value.
113+
if("name".equalsIgnoreCase(name)) {
114+
throw new JSONException("Illegal attribute name: 'name'");
115+
}
116+
if("value".equalsIgnoreCase(name)) {
117+
throw new JSONException("Illegal attribute name: 'value'");
118+
}
119+
// check to see if it's a flag property
92120
if (x.next() != '=') {
93-
if (name.equals("secure")) {
94-
value = Boolean.TRUE;
95-
} else {
96-
throw x.syntaxError("Missing '=' in cookie parameter.");
97-
}
121+
value = Boolean.TRUE;
98122
} else {
99-
value = unescape(x.nextTo(';'));
123+
value = unescape(x.nextTo(';')).trim();
100124
x.next();
101125
}
102-
jo.put(name, value);
126+
// only store non-blank attributes
127+
if(!"".equals(name) && !"".equals(value)) {
128+
jo.put(name, value);
129+
}
103130
}
104131
return jo;
105132
}
106133

107134

108135
/**
109136
* Convert a JSONObject into a cookie specification string. The JSONObject
110-
* must contain "name" and "value" members.
111-
* If the JSONObject contains "expires", "domain", "path", or "secure"
112-
* members, they will be appended to the cookie specification string.
113-
* All other members are ignored.
137+
* must contain "name" and "value" members (case insensitive).
138+
* If the JSONObject contains other members, they will be appended to the cookie
139+
* specification string. User-Agents are instructed to ignore unknown attributes,
140+
* so ensure your JSONObject is using only known attributes.
141+
* See also: <a href="https://tools.ietf.org/html/rfc6265">https://tools.ietf.org/html/rfc6265</a>
114142
* @param jo A JSONObject
115143
* @return A cookie specification string
116-
* @throws JSONException if a called function fails
144+
* @throws JSONException thrown if the cookie has no name.
117145
*/
118146
public static String toString(JSONObject jo) throws JSONException {
119147
StringBuilder sb = new StringBuilder();
120-
121-
sb.append(escape(jo.getString("name")));
122-
sb.append("=");
123-
sb.append(escape(jo.getString("value")));
124-
if (jo.has("expires")) {
125-
sb.append(";expires=");
126-
sb.append(jo.getString("expires"));
148+
149+
String name = null;
150+
Object value = null;
151+
for(String key : jo.keySet()){
152+
if("name".equalsIgnoreCase(key)) {
153+
name = jo.getString(key).trim();
154+
}
155+
if("value".equalsIgnoreCase(key)) {
156+
value=jo.getString(key).trim();
157+
}
158+
if(name != null && value != null) {
159+
break;
160+
}
127161
}
128-
if (jo.has("domain")) {
129-
sb.append(";domain=");
130-
sb.append(escape(jo.getString("domain")));
162+
163+
if(name == null || "".equals(name.trim())) {
164+
throw new JSONException("Cookie does not have a name");
131165
}
132-
if (jo.has("path")) {
133-
sb.append(";path=");
134-
sb.append(escape(jo.getString("path")));
166+
if(value == null) {
167+
value = "";
135168
}
136-
if (jo.optBoolean("secure")) {
137-
sb.append(";secure");
169+
170+
sb.append(escape(name));
171+
sb.append("=");
172+
sb.append(escape((String)value));
173+
174+
for(String key : jo.keySet()){
175+
if("name".equalsIgnoreCase(key)
176+
|| "value".equalsIgnoreCase(key)) {
177+
// already processed above
178+
continue;
179+
}
180+
value = jo.opt(key);
181+
if(value instanceof Boolean) {
182+
if(Boolean.TRUE.equals(value)) {
183+
sb.append(';').append(escape(key));
184+
}
185+
// don't emit false values
186+
} else {
187+
sb.append(';')
188+
.append(escape(key))
189+
.append('=')
190+
.append(escape(value.toString()));
191+
}
138192
}
193+
139194
return sb.toString();
140195
}
141196

src/test/java/org/json/junit/CookieTest.java

Lines changed: 26 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -79,32 +79,46 @@ public void malFormedNameValueException() {
7979
* Expects a JSONException.
8080
*/
8181
@Test
82-
public void malFormedAttributeException() {
82+
public void booleanAttribute() {
8383
String cookieStr = "this=Cookie;myAttribute";
84+
JSONObject jo = Cookie.toJSONObject(cookieStr);
85+
assertTrue("has key 'name'", jo.has("name"));
86+
assertTrue("has key 'value'", jo.has("value"));
87+
assertTrue("has key 'myAttribute'", jo.has("myattribute"));
88+
}
89+
90+
/**
91+
* Attempts to create a JSONObject from an empty cookie string.<br>
92+
* Note: Cookie throws an exception, but CookieList does not.<br>
93+
* Expects a JSONException
94+
*/
95+
@Test
96+
public void emptyStringCookieException() {
97+
String cookieStr = "";
8498
try {
8599
Cookie.toJSONObject(cookieStr);
86100
fail("Expecting an exception");
87101
} catch (JSONException e) {
88102
assertEquals("Expecting an exception message",
89-
"Missing '=' in cookie parameter. at 23 [character 24 line 1]",
103+
"Cookies must have a 'name'",
90104
e.getMessage());
91105
}
92106
}
93-
94107
/**
95-
* Attempts to create a JSONObject from an empty cookie string.<br>
108+
*
109+
* Attempts to create a JSONObject from an cookie string where the name is blank.<br>
96110
* Note: Cookie throws an exception, but CookieList does not.<br>
97111
* Expects a JSONException
98112
*/
99113
@Test
100-
public void emptyStringCookieException() {
101-
String cookieStr = "";
114+
public void emptyNameCookieException() {
115+
String cookieStr = " = value ";
102116
try {
103117
Cookie.toJSONObject(cookieStr);
104118
fail("Expecting an exception");
105119
} catch (JSONException e) {
106120
assertEquals("Expecting an exception message",
107-
"Expected '=' and instead saw '' at 0 [character 1 line 1]",
121+
"Cookies must have a 'name'",
108122
e.getMessage());
109123
}
110124
}
@@ -149,8 +163,8 @@ public void multiPartCookie() {
149163
}
150164

151165
/**
152-
* Cookie.toString() will omit the non-standard "thiswont=beIncluded"
153-
* attribute, but the attribute is still stored in the JSONObject.
166+
* Cookie.toString() will emit the non-standard "thiswont=beIncluded"
167+
* attribute, and the attribute is still stored in the JSONObject.
154168
* This test confirms both behaviors.
155169
*/
156170
@Test
@@ -163,15 +177,15 @@ public void convertCookieToString() {
163177
"thisWont=beIncluded;"+
164178
"secure";
165179
String expectedCookieStr =
166-
"{\"path\":\"/\","+
180+
"{\"thiswont\":\"beIncluded\","+
181+
"\"path\":\"/\","+
167182
"\"expires\":\"Wed, 19-Mar-2014 17:53:53 GMT\","+
168183
"\"domain\":\".yahoo.com\","+
169184
"\"name\":\"PH\","+
170185
"\"secure\":true,"+
171186
"\"value\":\"deleted\"}";
172187
// Add the nonstandard attribute to the expected cookie string
173-
String expectedDirectCompareCookieStr =
174-
expectedCookieStr.replaceAll("\\{", "\\{\"thisWont\":\"beIncluded\",");
188+
String expectedDirectCompareCookieStr = expectedCookieStr;
175189
// convert all strings into JSONObjects
176190
JSONObject jsonObject = Cookie.toJSONObject(cookieStr);
177191
JSONObject expectedJsonObject = new JSONObject(expectedCookieStr);

0 commit comments

Comments
 (0)