1/*
2 * Copyright (C) 2010 Google Inc. All rights reserved.
3 *
4 * Redistribution and use in source and binary forms, with or without
5 * modification, are permitted provided that the following conditions are
6 * met:
7 *
8 *     * Redistributions of source code must retain the above copyright
9 * notice, this list of conditions and the following disclaimer.
10 *     * Redistributions in binary form must reproduce the above
11 * copyright notice, this list of conditions and the following disclaimer
12 * in the documentation and/or other materials provided with the
13 * distribution.
14 *     * Neither the name of Google Inc. nor the names of its
15 * contributors may be used to endorse or promote products derived from
16 * this software without specific prior written permission.
17 *
18 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
19 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
20 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
21 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
22 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
23 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
24 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
25 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
26 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
27 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29 */
30
31// Ideally, we would rely on platform support for parsing a cookie, since
32// this would save us from any potential inconsistency. However, exposing
33// platform cookie parsing logic would require quite a bit of additional
34// plumbing, and at least some platforms lack support for parsing Cookie,
35// which is in a format slightly different from Set-Cookie and is normally
36// only required on the server side.
37
38WebInspector.CookieParser = function()
39{
40}
41
42WebInspector.CookieParser.prototype = {
43    get cookies()
44    {
45        return this._cookies;
46    },
47
48    parseCookie: function(cookieHeader)
49    {
50        if (!this._initialize(cookieHeader))
51            return;
52
53        for (var kv = this._extractKeyValue(); kv; kv = this._extractKeyValue()) {
54            if (kv.key.charAt(0) === "$" && this._lastCookie)
55                this._lastCookie.addAttribute(kv.key.slice(1), kv.value);
56            else if (kv.key.toLowerCase() !== "$version" && typeof kv.value === "string")
57                this._addCookie(kv, WebInspector.Cookie.Type.Request);
58            this._advanceAndCheckCookieDelimiter();
59        }
60        this._flushCookie();
61        return this._cookies;
62    },
63
64    parseSetCookie: function(setCookieHeader)
65    {
66        if (!this._initialize(setCookieHeader))
67            return;
68        for (var kv = this._extractKeyValue(); kv; kv = this._extractKeyValue()) {
69            if (this._lastCookie)
70                this._lastCookie.addAttribute(kv.key, kv.value);
71            else
72                this._addCookie(kv, WebInspector.Cookie.Type.Response);
73            if (this._advanceAndCheckCookieDelimiter())
74                this._flushCookie();
75        }
76        this._flushCookie();
77        return this._cookies;
78    },
79
80    _initialize: function(headerValue)
81    {
82        this._input = headerValue;
83        if (typeof headerValue !== "string")
84            return false;
85        this._cookies = [];
86        this._lastCookie = null;
87        this._originalInputLength = this._input.length;
88        return true;
89    },
90
91    _flushCookie: function()
92    {
93        if (this._lastCookie)
94            this._lastCookie.size = this._originalInputLength - this._input.length - this._lastCookiePosition;
95        this._lastCookie = null;
96    },
97
98    _extractKeyValue: function()
99    {
100        if (!this._input || !this._input.length)
101            return null;
102        // Note: RFCs offer an option for quoted values that may contain commas and semicolons.
103        // Many browsers/platforms do not support this, however (see http://webkit.org/b/16699
104        // and http://crbug.com/12361). The logic below matches latest versions of IE, Firefox,
105        // Chrome and Safari on some old platforms. The latest version of Safari supports quoted
106        // cookie values, though.
107        var keyValueMatch = /^[ \t]*([^\s=;]+)[ \t]*(?:=[ \t]*([^;\n]*))?/.exec(this._input);
108        if (!keyValueMatch) {
109            console.log("Failed parsing cookie header before: " + this._input);
110            return null;
111        }
112
113        var result = {
114            key: keyValueMatch[1],
115            value: keyValueMatch[2] && keyValueMatch[2].trim(),
116            position: this._originalInputLength - this._input.length
117        };
118        this._input = this._input.slice(keyValueMatch[0].length);
119        return result;
120    },
121
122    _advanceAndCheckCookieDelimiter: function()
123    {
124        var match = /^\s*[\n;]\s*/.exec(this._input);
125        if (!match)
126            return false;
127        this._input = this._input.slice(match[0].length);
128        return match[0].match("\n") !== null;
129    },
130
131    _addCookie: function(keyValue, type)
132    {
133        if (this._lastCookie)
134            this._lastCookie.size = keyValue.position - this._lastCookiePosition;
135        // Mozilla bug 169091: Mozilla, IE and Chrome treat single token (w/o "=") as
136        // specifying a value for a cookie with empty name.
137        this._lastCookie = keyValue.value ? new WebInspector.Cookie(keyValue.key, keyValue.value, type) :
138            new WebInspector.Cookie("", keyValue.key, type);
139        this._lastCookiePosition = keyValue.position;
140        this._cookies.push(this._lastCookie);
141    }
142};
143
144WebInspector.CookieParser.parseCookie = function(header)
145{
146    return (new WebInspector.CookieParser()).parseCookie(header);
147}
148
149WebInspector.CookieParser.parseSetCookie = function(header)
150{
151    return (new WebInspector.CookieParser()).parseSetCookie(header);
152}
153
154WebInspector.Cookie = function(name, value, type)
155{
156    this.name = name;
157    this.value = value;
158    this.type = type;
159    this._attributes = {};
160}
161
162WebInspector.Cookie.prototype = {
163    get httpOnly()
164    {
165        return "httponly" in this._attributes;
166    },
167
168    get secure()
169    {
170        return "secure" in this._attributes;
171    },
172
173    get session()
174    {
175        // RFC 2965 suggests using Discard attribute to mark session cookies, but this does not seem to be widely used.
176        // Check for absence of explicity max-age or expiry date instead.
177        return  !("expries" in this._attributes || "max-age" in this._attributes);
178    },
179
180    get path()
181    {
182        return this._attributes.path;
183    },
184
185    get domain()
186    {
187        return this._attributes.domain;
188    },
189
190    expires: function(requestDate)
191    {
192        return this._attributes.expires ? new Date(this._attributes.expires) :
193            (this._attributes["max-age"] ? new Date(requestDate.getTime() + 1000 * this._attributes["max-age"]) : null);
194    },
195
196    get attributes()
197    {
198        return this._attributes;
199    },
200
201    addAttribute: function(key, value)
202    {
203        this._attributes[key.toLowerCase()] = value;
204    }
205}
206
207WebInspector.Cookie.Type = {
208    Request: 0,
209    Response: 1
210};
211