Source code
Revision control
Copy as Markdown
Other Tools
/* 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,
const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
PrivacyLevel: "resource://gre/modules/sessionstore/PrivacyLevel.sys.mjs",
});
const MAX_EXPIRY = Number.MAX_SAFE_INTEGER;
/**
* The external API implemented by the SessionCookies module.
*/
export var SessionCookies = Object.freeze({
collect() {
return SessionCookiesInternal.collect();
},
restore(cookies) {
SessionCookiesInternal.restore(cookies);
},
});
/**
* The internal API.
*/
var SessionCookiesInternal = {
/**
* Stores whether we're initialized, yet.
*/
_initialized: false,
/**
* Retrieve an array of all stored session cookies.
*/
collect() {
this._ensureInitialized();
return CookieStore.toArray();
},
/**
* Restores a given list of session cookies.
*/
restore(cookies) {
for (let cookie of cookies) {
let expiry = "expiry" in cookie ? cookie.expiry : MAX_EXPIRY;
let exists = false;
try {
exists = Services.cookies.cookieExists(
cookie.host,
cookie.path || "",
cookie.name || "",
cookie.originAttributes || {}
);
} catch (ex) {
console.error(
`CookieService::CookieExists failed with error '${ex}' for '${JSON.stringify(
cookie
)}'.`
);
}
if (!exists) {
// Enforces isPartitioned if the partitionKey is set. We need to do this
// because the session store didn't store the isPartitioned flag.
// Otherwise, we'd end up setting partitioned cookies without
// isPartitioned flag.
let isPartitioned =
cookie.isPartitioned ||
cookie.originAttributes?.partitionKey?.length > 0;
try {
Services.cookies.add(
cookie.host,
cookie.path || "",
cookie.name || "",
cookie.value,
!!cookie.secure,
!!cookie.httponly,
/* isSession = */ true,
expiry,
cookie.originAttributes || {},
cookie.sameSite || Ci.nsICookie.SAMESITE_NONE,
cookie.schemeMap || Ci.nsICookie.SCHEME_HTTPS,
isPartitioned
);
} catch (ex) {
console.error(
`CookieService::Add failed with error '${ex}' for cookie ${JSON.stringify(
cookie
)}.`
);
}
}
}
},
/**
* Handles observers notifications that are sent whenever cookies are added,
* changed, or removed. Ensures that the storage is updated accordingly.
*/
observe(subject) {
let notification = subject.QueryInterface(Ci.nsICookieNotification);
let {
COOKIE_DELETED,
COOKIE_ADDED,
COOKIE_CHANGED,
ALL_COOKIES_CLEARED,
COOKIES_BATCH_DELETED,
} = Ci.nsICookieNotification;
switch (notification.action) {
case COOKIE_ADDED:
this._addCookie(notification.cookie);
break;
case COOKIE_CHANGED:
this._updateCookie(notification.cookie);
break;
case COOKIE_DELETED:
this._removeCookie(notification.cookie);
break;
case ALL_COOKIES_CLEARED:
CookieStore.clear();
break;
case COOKIES_BATCH_DELETED:
this._removeCookies(notification.batchDeletedCookies);
break;
default:
throw new Error("Unhandled session-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() {
if (this._initialized) {
return;
}
this._reloadCookies();
this._initialized = true;
Services.obs.addObserver(this, "session-cookie-changed");
// Listen for privacy level changes to reload cookies when needed.
Services.prefs.addObserver("browser.sessionstore.privacy_level", () => {
this._reloadCookies();
});
},
/**
* Adds a given cookie to the store.
*/
_addCookie(cookie) {
cookie.QueryInterface(Ci.nsICookie);
// Store only session cookies, obey the privacy level.
if (cookie.isSession && lazy.PrivacyLevel.canSave(cookie.isSecure)) {
CookieStore.add(cookie);
}
},
/**
* Updates a given cookie.
*/
_updateCookie(cookie) {
cookie.QueryInterface(Ci.nsICookie);
// Store only session cookies, obey the privacy level.
if (cookie.isSession && lazy.PrivacyLevel.canSave(cookie.isSecure)) {
CookieStore.add(cookie);
} else {
CookieStore.delete(cookie);
}
},
/**
* Removes a given cookie from the store.
*/
_removeCookie(cookie) {
cookie.QueryInterface(Ci.nsICookie);
if (cookie.isSession) {
CookieStore.delete(cookie);
}
},
/**
* Removes a given list of cookies from the store.
*/
_removeCookies(cookies) {
for (let i = 0; i < cookies.length; i++) {
this._removeCookie(cookies.queryElementAt(i, Ci.nsICookie));
}
},
/**
* Iterates all cookies in the cookies service and puts them into the store
* if they're session cookies. Obeys the user's chosen privacy level.
*/
_reloadCookies() {
CookieStore.clear();
// Bail out if we're not supposed to store cookies at all.
if (!lazy.PrivacyLevel.canSave(false)) {
return;
}
for (let cookie of Services.cookies.sessionCookies) {
this._addCookie(cookie);
}
},
};
/**
* The internal storage that keeps track of session cookies.
*/
var CookieStore = {
/**
* The internal map holding all known session cookies.
*/
_entries: new Map(),
/**
* Stores a given cookie.
*
* @param cookie
* The nsICookie object to add to the storage.
*/
add(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;
}
if (cookie.originAttributes) {
jscookie.originAttributes = cookie.originAttributes;
}
if (cookie.sameSite) {
jscookie.sameSite = cookie.sameSite;
}
if (cookie.schemeMap) {
jscookie.schemeMap = cookie.schemeMap;
}
if (cookie.isPartitioned) {
jscookie.isPartitioned = true;
}
this._entries.set(this._getKeyForCookie(cookie), jscookie);
},
/**
* Removes a given cookie.
*
* @param cookie
* The nsICookie object to be removed from storage.
*/
delete(cookie) {
this._entries.delete(this._getKeyForCookie(cookie));
},
/**
* Removes all cookies.
*/
clear() {
this._entries.clear();
},
/**
* Return all cookies as an array.
*/
toArray() {
return [...this._entries.values()];
},
/**
* Returns the key needed to properly store and identify a given cookie.
* A cookie is uniquely identified by the combination of its host, name,
* path, and originAttributes properties.
*
* @param cookie
* The nsICookie object to compute a key for.
* @return string
*/
_getKeyForCookie(cookie) {
return JSON.stringify({
host: cookie.host,
name: cookie.name,
path: cookie.path,
attr: ChromeUtils.originAttributesToSuffix(cookie.originAttributes),
});
},
};