"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; });