|
1 | 1 | package org.json; |
2 | 2 |
|
| 3 | +import java.util.Locale; |
| 4 | + |
3 | 5 | /* |
4 | 6 | Copyright (c) 2002 JSON.org |
5 | 7 |
|
@@ -27,6 +29,7 @@ of this software and associated documentation files (the "Software"), to deal |
27 | 29 | /** |
28 | 30 | * Convert a web browser cookie specification to a JSONObject and back. |
29 | 31 | * 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> |
30 | 33 | * @author JSON.org |
31 | 34 | * @version 2015-12-09 |
32 | 35 | */ |
@@ -65,77 +68,129 @@ public static String escape(String string) { |
65 | 68 |
|
66 | 69 | /** |
67 | 70 | * 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 |
69 | 72 | * will be unescaped, possibly converting '+' and '%' sequences. The |
70 | 73 | * 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). |
72 | 76 | * The name will be stored under the key "name", and the value will be |
73 | 77 | * stored under the key "value". This method does not do checking or |
74 | 78 | * 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 => httponly). If an attribute is specified more than |
| 81 | + * once, only the value found closer to the end of the cookie-string is kept. |
76 | 82 | * @param string The cookie specification string. |
77 | 83 | * @return A JSONObject containing "name", "value", and possibly other |
78 | 84 | * 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. |
80 | 88 | */ |
81 | | - public static JSONObject toJSONObject(String string) throws JSONException { |
| 89 | + public static JSONObject toJSONObject(String string) { |
| 90 | + final JSONObject jo = new JSONObject(); |
82 | 91 | String name; |
83 | | - JSONObject jo = new JSONObject(); |
84 | 92 | Object value; |
| 93 | + |
| 94 | + |
85 | 95 | 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. |
87 | 105 | x.next('='); |
88 | | - jo.put("value", x.nextTo(';')); |
| 106 | + jo.put("value", unescape(x.nextTo(';')).trim()); |
| 107 | + // discard the ';' |
89 | 108 | x.next(); |
| 109 | + // parse the remaining cookie attributes |
90 | 110 | 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 |
92 | 120 | 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; |
98 | 122 | } else { |
99 | | - value = unescape(x.nextTo(';')); |
| 123 | + value = unescape(x.nextTo(';')).trim(); |
100 | 124 | x.next(); |
101 | 125 | } |
102 | | - jo.put(name, value); |
| 126 | + // only store non-blank attributes |
| 127 | + if(!"".equals(name) && !"".equals(value)) { |
| 128 | + jo.put(name, value); |
| 129 | + } |
103 | 130 | } |
104 | 131 | return jo; |
105 | 132 | } |
106 | 133 |
|
107 | 134 |
|
108 | 135 | /** |
109 | 136 | * 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> |
114 | 142 | * @param jo A JSONObject |
115 | 143 | * @return A cookie specification string |
116 | | - * @throws JSONException if a called function fails |
| 144 | + * @throws JSONException thrown if the cookie has no name. |
117 | 145 | */ |
118 | 146 | public static String toString(JSONObject jo) throws JSONException { |
119 | 147 | 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 | + } |
127 | 161 | } |
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"); |
131 | 165 | } |
132 | | - if (jo.has("path")) { |
133 | | - sb.append(";path="); |
134 | | - sb.append(escape(jo.getString("path"))); |
| 166 | + if(value == null) { |
| 167 | + value = ""; |
135 | 168 | } |
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 | + } |
138 | 192 | } |
| 193 | + |
139 | 194 | return sb.toString(); |
140 | 195 | } |
141 | 196 |
|
|
0 commit comments