|
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