tenfourfox/toolkit/components/extensions/ext-cookies.js
Cameron Kaiser c9b2922b70 hello FPR
2017-04-19 00:56:45 -07:00

268 lines
7.7 KiB
JavaScript

"use strict";
const { interfaces: Ci, utils: Cu } = Components;
Cu.import("resource://gre/modules/ExtensionUtils.jsm");
var {
EventManager,
runSafe,
} = ExtensionUtils;
// Cookies from private tabs currently can't be enumerated.
var DEFAULT_STORE = "firefox-default";
function convert(cookie) {
let result = {
name: cookie.name,
value: cookie.value,
domain: cookie.host,
hostOnly: !cookie.isDomain,
path: cookie.path,
secure: cookie.isSecure,
httpOnly: cookie.isHttpOnly,
session: cookie.isSession,
storeId: DEFAULT_STORE,
};
if (!cookie.isSession) {
result.expirationDate = cookie.expiry;
}
return result;
}
function* query(detailsIn, props) {
// Different callers want to filter on different properties. |props|
// tells us which ones they're interested in.
let details = {};
props.map(property => {
if (detailsIn[property] !== null) {
details[property] = detailsIn[property];
}
});
// We can use getCookiesFromHost for faster searching.
let enumerator;
if ("url" in details) {
try {
let uri = Services.io.newURI(details.url, null, null);
enumerator = Services.cookies.getCookiesFromHost(uri.host);
} catch (ex) {
// This often happens for about: URLs
return;
}
} else if ("domain" in details) {
enumerator = Services.cookies.getCookiesFromHost(details.domain);
} else {
enumerator = Services.cookies.enumerator;
}
// Based on nsCookieService::GetCookieStringInternal
function matches(cookie) {
function domainMatches(host) {
return cookie.rawHost == host || (cookie.isDomain && host.endsWith(cookie.host));
}
function pathMatches(path) {
// Calculate cookie path length, excluding trailing '/'.
let length = cookie.path.length;
if (cookie.path.endsWith("/")) {
length -= 1;
}
// If the path is shorter than the cookie path, don't send it back.
if (!path.startsWith(cookie.path.substring(0, length))) {
return false;
}
let pathDelimiter = ["/", "?", "#", ";"];
if (path.length > length && !pathDelimiter.includes(path.charAt(length))) {
return false;
}
return true;
}
// "Restricts the retrieved cookies to those that would match the given URL."
if ("url" in details) {
let uri = Services.io.newURI(details.url, null, null);
if (!domainMatches(uri.host)) {
return false;
}
if (cookie.isSecure && uri.scheme != "https") {
return false;
}
if (!pathMatches(uri.path)) {
return false;
}
}
if ("name" in details && details.name != cookie.name) {
return false;
}
// "Restricts the retrieved cookies to those whose domains match or are subdomains of this one."
if ("domain" in details) {
if (cookie.rawHost != details.domain &&
!cookie.rawHost.endsWith("." + details.domain)) {
return false;
}
}
// "Restricts the retrieved cookies to those whose path exactly matches this string.""
if ("path" in details && details.path != cookie.path) {
return false;
}
if ("secure" in details && details.secure != cookie.isSecure) {
return false;
}
if ("session" in details && details.session != cookie.isSession) {
return false;
}
if ("storeId" in details && details.storeId != DEFAULT_STORE) {
return false;
}
return true;
}
while (enumerator.hasMoreElements()) {
let cookie = enumerator.getNext().QueryInterface(Ci.nsICookie2);
if (matches(cookie)) {
yield cookie;
}
}
}
extensions.registerSchemaAPI("cookies", "cookies", (extension, context) => {
let self = {
cookies: {
get: function(details, callback) {
// FIXME: We don't sort by length of path and creation time.
for (let cookie of query(details, ["url", "name", "storeId"])) {
runSafe(context, callback, convert(cookie));
return;
}
// Found no match.
runSafe(context, callback, null);
},
getAll: function(details, callback) {
let allowed = ["url", "name", "domain", "path", "secure", "session", "storeId"];
let result = [];
for (let cookie of query(details, allowed)) {
result.push(convert(cookie));
}
runSafe(context, callback, result);
},
set: function(details, callback) {
let uri = Services.io.newURI(details.url, null, null);
let domain;
if (details.domain !== null) {
domain = "." + details.domain;
} else {
domain = uri.host; // "If omitted, the cookie becomes a host-only cookie."
}
let path;
if (details.path !== null) {
path = details.path;
} else {
// Chrome seems to trim the path after the last slash.
// /x/abc/ddd == /x/abc
// /xxxx?abc == /
// We always have at least one slash.
let index = uri.path.slice(1).lastIndexOf("/");
if (index == -1) {
path = "/";
} else {
path = uri.path.slice(0, index + 1); // This removes the last slash.
}
}
let name = details.name !== null ? details.name : "";
let value = details.value !== null ? details.value : "";
let secure = details.secure !== null ? details.secure : false;
let httpOnly = details.httpOnly !== null ? details.httpOnly : false;
let isSession = details.expirationDate === null;
let expiry = isSession ? 0 : details.expirationDate;
// Ingore storeID.
Services.cookies.add(domain, path, name, value, secure, httpOnly, isSession, expiry);
if (callback) {
self.cookies.get(details, callback);
}
},
remove: function(details, callback) {
for (let cookie of query(details, ["url", "name", "storeId"])) {
Services.cookies.remove(cookie.host, cookie.name, cookie.path, false);
if (callback) {
runSafe(context, callback, {
url: details.url,
name: details.name,
storeId: DEFAULT_STORE,
});
}
// Todo: could there be multiple per subdomain?
return;
}
if (callback) {
runSafe(context, callback, null);
}
},
getAllCookieStores: function(callback) {
// Todo: list all the tabIds for non-private tabs
runSafe(context, callback, [{id: DEFAULT_STORE, tabIds: []}]);
},
onChanged: new EventManager(context, "cookies.onChanged", fire => {
let observer = (subject, topic, data) => {
let notify = (removed, cookie, cause) => {
fire({removed, cookie: convert(cookie.QueryInterface(Ci.nsICookie2)), cause});
};
// We do our best effort here to map the incompatible states.
switch (data) {
case "deleted":
notify(true, subject, "explicit");
break;
case "added":
notify(false, subject, "explicit");
break;
case "changed":
notify(false, subject, "overwrite");
break;
case "batch-deleted":
for (let i = 0; i < subject.length; subject++) {
let cookie = subject.queryElementAt(i, Ci.nsICookie2);
if (!cookie.isSession && cookie.expiry < Date.now()) {
notify(true, cookie, "expired");
} else {
notify(true, cookie, "evicted");
}
}
break;
}
};
Services.obs.addObserver(observer, "cookie-changed", false);
return () => Services.obs.removeObserver(observer, "cookie-changed");
}).api(),
},
};
return self;
});