mirror of
https://gitlab.winehq.org/wine/wine-gecko.git
synced 2024-09-13 09:24:08 -07:00
438 lines
13 KiB
JavaScript
438 lines
13 KiB
JavaScript
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
|
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
|
|
|
"use strict";
|
|
|
|
this.EXPORTED_SYMBOLS = ["SessionCookies"];
|
|
|
|
const Cu = Components.utils;
|
|
const Ci = Components.interfaces;
|
|
|
|
Cu.import("resource://gre/modules/Services.jsm", this);
|
|
Cu.import("resource://gre/modules/XPCOMUtils.jsm", this);
|
|
|
|
XPCOMUtils.defineLazyModuleGetter(this, "Utils",
|
|
"resource:///modules/sessionstore/Utils.jsm");
|
|
XPCOMUtils.defineLazyModuleGetter(this, "PrivacyLevel",
|
|
"resource:///modules/sessionstore/PrivacyLevel.jsm");
|
|
|
|
// MAX_EXPIRY should be 2^63-1, but JavaScript can't handle that precision.
|
|
const MAX_EXPIRY = Math.pow(2, 62);
|
|
|
|
/**
|
|
* The external API implemented by the SessionCookies module.
|
|
*/
|
|
this.SessionCookies = Object.freeze({
|
|
update: function (windows) {
|
|
SessionCookiesInternal.update(windows);
|
|
},
|
|
|
|
getHostsForWindow: function (window, checkPrivacy = false) {
|
|
return SessionCookiesInternal.getHostsForWindow(window, checkPrivacy);
|
|
}
|
|
});
|
|
|
|
/**
|
|
* The internal API.
|
|
*/
|
|
let SessionCookiesInternal = {
|
|
/**
|
|
* Stores whether we're initialized, yet.
|
|
*/
|
|
_initialized: false,
|
|
|
|
/**
|
|
* Retrieve the list of all hosts contained in the given windows' session
|
|
* history entries (per window) and collect the associated cookies for those
|
|
* hosts, if any. The given state object is being modified.
|
|
*
|
|
* @param windows
|
|
* Array of window state objects.
|
|
* [{ tabs: [...], cookies: [...] }, ...]
|
|
*/
|
|
update: function (windows) {
|
|
this._ensureInitialized();
|
|
|
|
for (let window of windows) {
|
|
let cookies = [];
|
|
|
|
// Collect all hosts for the current window.
|
|
let hosts = this.getHostsForWindow(window, true);
|
|
|
|
for (let host of Object.keys(hosts)) {
|
|
let isPinned = hosts[host];
|
|
|
|
for (let cookie of CookieStore.getCookiesForHost(host)) {
|
|
// _getCookiesForHost() will only return hosts with the right privacy
|
|
// rules, so there is no need to do anything special with this call
|
|
// to PrivacyLevel.canSave().
|
|
if (PrivacyLevel.canSave({isHttps: cookie.secure, isPinned: isPinned})) {
|
|
cookies.push(cookie);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Don't include/keep empty cookie sections.
|
|
if (cookies.length) {
|
|
window.cookies = cookies;
|
|
} else if ("cookies" in window) {
|
|
delete window.cookies;
|
|
}
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Returns a map of all hosts for a given window that we might want to
|
|
* collect cookies for.
|
|
*
|
|
* @param window
|
|
* A window state object containing tabs with history entries.
|
|
* @param checkPrivacy (bool)
|
|
* Whether to check the privacy level for each host.
|
|
* @return {object} A map of hosts for a given window state object. The keys
|
|
* will be hosts, the values are boolean and determine
|
|
* whether we will use the deferred privacy level when
|
|
* checking how much data to save on quitting.
|
|
*/
|
|
getHostsForWindow: function (window, checkPrivacy = false) {
|
|
let hosts = {};
|
|
|
|
for (let tab of window.tabs) {
|
|
for (let entry of tab.entries) {
|
|
this._extractHostsFromEntry(entry, hosts, checkPrivacy, tab.pinned);
|
|
}
|
|
}
|
|
|
|
return hosts;
|
|
},
|
|
|
|
/**
|
|
* Handles observers notifications that are sent whenever cookies are added,
|
|
* changed, or removed. Ensures that the storage is updated accordingly.
|
|
*/
|
|
observe: function (subject, topic, data) {
|
|
switch (data) {
|
|
case "added":
|
|
case "changed":
|
|
this._updateCookie(subject);
|
|
break;
|
|
case "deleted":
|
|
this._removeCookie(subject);
|
|
break;
|
|
case "cleared":
|
|
CookieStore.clear();
|
|
break;
|
|
case "batch-deleted":
|
|
this._removeCookies(subject);
|
|
break;
|
|
case "reload":
|
|
CookieStore.clear();
|
|
this._reloadCookies();
|
|
break;
|
|
default:
|
|
throw new Error("Unhandled cookie-changed notification.");
|
|
}
|
|
},
|
|
|
|
/**
|
|
* If called for the first time in a session, iterates all cookies in the
|
|
* cookies service and puts them into the store if they're session cookies.
|
|
*/
|
|
_ensureInitialized: function () {
|
|
if (!this._initialized) {
|
|
this._reloadCookies();
|
|
this._initialized = true;
|
|
Services.obs.addObserver(this, "cookie-changed", false);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Fill a given map with hosts found in the given entry's session history and
|
|
* any child entries.
|
|
*
|
|
* @param entry
|
|
* the history entry, serialized
|
|
* @param hosts
|
|
* the hash that will be used to store hosts eg, { hostname: true }
|
|
* @param checkPrivacy
|
|
* should we check the privacy level for https
|
|
* @param isPinned
|
|
* is the entry we're evaluating for a pinned tab; used only if
|
|
* checkPrivacy
|
|
*/
|
|
_extractHostsFromEntry: function (entry, hosts, checkPrivacy, isPinned) {
|
|
let host = entry._host;
|
|
let scheme = entry._scheme;
|
|
|
|
// If host & scheme aren't defined, then we are likely here in the startup
|
|
// process via _splitCookiesFromWindow. In that case, we'll turn entry.url
|
|
// into an nsIURI and get host/scheme from that. This will throw for about:
|
|
// urls in which case we don't need to do anything.
|
|
if (!host && !scheme) {
|
|
try {
|
|
let uri = Utils.makeURI(entry.url);
|
|
host = uri.host;
|
|
scheme = uri.scheme;
|
|
this._extractHostsFromHostScheme(host, scheme, hosts, checkPrivacy, isPinned);
|
|
}
|
|
catch (ex) { }
|
|
}
|
|
|
|
if (entry.children) {
|
|
for (let child of entry.children) {
|
|
this._extractHostsFromEntry(child, hosts, checkPrivacy, isPinned);
|
|
}
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Add a given host to a given map of hosts if the privacy level allows
|
|
* saving cookie data for it.
|
|
*
|
|
* @param host
|
|
* the host of a uri (usually via nsIURI.host)
|
|
* @param scheme
|
|
* the scheme of a uri (usually via nsIURI.scheme)
|
|
* @param hosts
|
|
* the hash that will be used to store hosts eg, { hostname: true }
|
|
* @param checkPrivacy
|
|
* should we check the privacy level for https
|
|
* @param isPinned
|
|
* is the entry we're evaluating for a pinned tab; used only if
|
|
* checkPrivacy
|
|
*/
|
|
_extractHostsFromHostScheme:
|
|
function (host, scheme, hosts, checkPrivacy, isPinned) {
|
|
// host and scheme may not be set (for about: urls for example), in which
|
|
// case testing scheme will be sufficient.
|
|
if (/https?/.test(scheme) && !hosts[host] &&
|
|
(!checkPrivacy ||
|
|
PrivacyLevel.canSave({isHttps: scheme == "https", isPinned: isPinned}))) {
|
|
// By setting this to true or false, we can determine when looking at
|
|
// the host in update() if we should check for privacy.
|
|
hosts[host] = isPinned;
|
|
} else if (scheme == "file") {
|
|
hosts[host] = true;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Updates or adds a given cookie to the store.
|
|
*/
|
|
_updateCookie: function (cookie) {
|
|
cookie.QueryInterface(Ci.nsICookie2);
|
|
|
|
if (cookie.isSession) {
|
|
CookieStore.set(cookie);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Removes a given cookie from the store.
|
|
*/
|
|
_removeCookie: function (cookie) {
|
|
cookie.QueryInterface(Ci.nsICookie2);
|
|
|
|
if (cookie.isSession) {
|
|
CookieStore.delete(cookie);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Removes a given list of cookies from the store.
|
|
*/
|
|
_removeCookies: function (cookies) {
|
|
for (let i = 0; i < cookies.length; i++) {
|
|
this._removeCookie(cookies.queryElementAt(i, Ci.nsICookie2));
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Iterates all cookies in the cookies service and puts them into the store
|
|
* if they're session cookies.
|
|
*/
|
|
_reloadCookies: function () {
|
|
let iter = Services.cookies.enumerator;
|
|
while (iter.hasMoreElements()) {
|
|
this._updateCookie(iter.getNext());
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Generates all possible subdomains for a given host and prepends a leading
|
|
* dot to all variants.
|
|
*
|
|
* See http://tools.ietf.org/html/rfc6265#section-5.1.3
|
|
* http://en.wikipedia.org/wiki/HTTP_cookie#Domain_and_Path
|
|
*
|
|
* All cookies belonging to a web page will be internally represented by a
|
|
* nsICookie object. nsICookie.host will be the request host if no domain
|
|
* parameter was given when setting the cookie. If a specific domain was given
|
|
* then nsICookie.host will contain that specific domain and prepend a leading
|
|
* dot to it.
|
|
*
|
|
* We thus generate all possible subdomains for a given domain and prepend a
|
|
* leading dot to them as that is the value that was used as the map key when
|
|
* the cookie was set.
|
|
*/
|
|
function* getPossibleSubdomainVariants(host) {
|
|
// Try given domain with a leading dot (.www.example.com).
|
|
yield "." + host;
|
|
|
|
// Stop if there are only two parts left (e.g. example.com was given).
|
|
let parts = host.split(".");
|
|
if (parts.length < 3) {
|
|
return;
|
|
}
|
|
|
|
// Remove the first subdomain (www.example.com -> example.com).
|
|
let rest = parts.slice(1).join(".");
|
|
|
|
// Try possible parent subdomains.
|
|
yield* getPossibleSubdomainVariants(rest);
|
|
}
|
|
|
|
/**
|
|
* The internal cookie storage that keeps track of every active session cookie.
|
|
* These are stored using maps per host, path, and cookie name.
|
|
*/
|
|
let CookieStore = {
|
|
/**
|
|
* The internal structure holding all known cookies.
|
|
*
|
|
* Host =>
|
|
* Path =>
|
|
* Name => {path: "/", name: "sessionid", secure: true}
|
|
*
|
|
* Maps are used for storage but the data structure is equivalent to this:
|
|
*
|
|
* this._hosts = {
|
|
* "www.mozilla.org": {
|
|
* "/": {
|
|
* "username": {name: "username", value: "my_name_is", etc...},
|
|
* "sessionid": {name: "sessionid", value: "1fdb3a", etc...}
|
|
* }
|
|
* },
|
|
* "tbpl.mozilla.org": {
|
|
* "/path": {
|
|
* "cookiename": {name: "cookiename", value: "value", etc...}
|
|
* }
|
|
* },
|
|
* ".example.com": {
|
|
* "/path": {
|
|
* "cookiename": {name: "cookiename", value: "value", etc...}
|
|
* }
|
|
* }
|
|
* };
|
|
*/
|
|
_hosts: new Map(),
|
|
|
|
/**
|
|
* Returns the list of stored session cookies for a given host.
|
|
*
|
|
* @param host
|
|
* A string containing the host name we want to get cookies for.
|
|
*/
|
|
getCookiesForHost: function (host) {
|
|
let cookies = [];
|
|
|
|
let appendCookiesForHost = host => {
|
|
if (!this._hosts.has(host)) {
|
|
return;
|
|
}
|
|
|
|
for (let pathToNamesMap of this._hosts.get(host).values()) {
|
|
cookies.push(...pathToNamesMap.values());
|
|
}
|
|
}
|
|
|
|
// Try to find cookies for the given host, e.g. <www.example.com>.
|
|
// The full hostname will be in the map if the Set-Cookie header did not
|
|
// have a domain= attribute, i.e. the cookie will only be stored for the
|
|
// request domain. Also, try to find cookies for subdomains, e.g.
|
|
// <.example.com>. We will find those variants with a leading dot in the
|
|
// map if the Set-Cookie header had a domain= attribute, i.e. the cookie
|
|
// will be stored for a parent domain and we send it for any subdomain.
|
|
for (let variant of [host, ...getPossibleSubdomainVariants(host)]) {
|
|
appendCookiesForHost(variant);
|
|
}
|
|
|
|
return cookies;
|
|
},
|
|
|
|
/**
|
|
* Stores a given cookie.
|
|
*
|
|
* @param cookie
|
|
* The nsICookie2 object to add to the storage.
|
|
*/
|
|
set: function (cookie) {
|
|
let jscookie = {host: cookie.host, value: cookie.value};
|
|
|
|
// Only add properties with non-default values to save a few bytes.
|
|
if (cookie.path) {
|
|
jscookie.path = cookie.path;
|
|
}
|
|
|
|
if (cookie.name) {
|
|
jscookie.name = cookie.name;
|
|
}
|
|
|
|
if (cookie.isSecure) {
|
|
jscookie.secure = true;
|
|
}
|
|
|
|
if (cookie.isHttpOnly) {
|
|
jscookie.httponly = true;
|
|
}
|
|
|
|
if (cookie.expiry < MAX_EXPIRY) {
|
|
jscookie.expiry = cookie.expiry;
|
|
}
|
|
|
|
this._ensureMap(cookie).set(cookie.name, jscookie);
|
|
},
|
|
|
|
/**
|
|
* Removes a given cookie.
|
|
*
|
|
* @param cookie
|
|
* The nsICookie2 object to be removed from storage.
|
|
*/
|
|
delete: function (cookie) {
|
|
this._ensureMap(cookie).delete(cookie.name);
|
|
},
|
|
|
|
/**
|
|
* Removes all cookies.
|
|
*/
|
|
clear: function () {
|
|
this._hosts.clear();
|
|
},
|
|
|
|
/**
|
|
* Creates all maps necessary to store a given cookie.
|
|
*
|
|
* @param cookie
|
|
* The nsICookie2 object to create maps for.
|
|
*
|
|
* @return The newly created Map instance mapping cookie names to
|
|
* internal jscookies, in the given path of the given host.
|
|
*/
|
|
_ensureMap: function (cookie) {
|
|
if (!this._hosts.has(cookie.host)) {
|
|
this._hosts.set(cookie.host, new Map());
|
|
}
|
|
|
|
let pathToNamesMap = this._hosts.get(cookie.host);
|
|
|
|
if (!pathToNamesMap.has(cookie.path)) {
|
|
pathToNamesMap.set(cookie.path, new Map());
|
|
}
|
|
|
|
return pathToNamesMap.get(cookie.path);
|
|
}
|
|
};
|