diff --git a/.eslintignore b/.eslintignore index 1ba5b4a56970..680563f86fcb 100644 --- a/.eslintignore +++ b/.eslintignore @@ -190,6 +190,7 @@ dom/grid/** dom/html/** dom/ipc/** dom/jsurl/** +dom/localstorage/** dom/manifest/** dom/media/test/** dom/media/tests/** diff --git a/browser/base/content/test/sanitize/browser_cookiePermission.js b/browser/base/content/test/sanitize/browser_cookiePermission.js index 4ca839ce72d7..f800395193a5 100644 --- a/browser/base/content/test/sanitize/browser_cookiePermission.js +++ b/browser/base/content/test/sanitize/browser_cookiePermission.js @@ -277,7 +277,7 @@ add_task(async function deleteStorageInAboutURL() { let principal = Services.scriptSecurityManager.createCodebasePrincipalFromOrigin("about:newtab"); await new Promise(aResolve => { - let req = Services.qms.clearStoragesForPrincipal(principal, null, false); + let req = Services.qms.clearStoragesForPrincipal(principal); req.callback = () => { aResolve(); }; }); }); @@ -313,7 +313,7 @@ add_task(async function deleteStorageOnlyCustomPermissionInAboutURL() { let principal = Services.scriptSecurityManager.createCodebasePrincipalFromOrigin("about:newtab"); await new Promise(aResolve => { - let req = Services.qms.clearStoragesForPrincipal(principal, null, false); + let req = Services.qms.clearStoragesForPrincipal(principal); req.callback = () => { aResolve(); }; }); diff --git a/browser/base/content/test/webextensions/browser.ini b/browser/base/content/test/webextensions/browser.ini index 66d49a4fc761..c1e2838cc4de 100644 --- a/browser/base/content/test/webextensions/browser.ini +++ b/browser/base/content/test/webextensions/browser.ini @@ -2,7 +2,6 @@ support-files = head.js file_install_extensions.html - browser_legacy.xpi browser_legacy_webext.xpi browser_webext_permissions.xpi browser_webext_nopermissions.xpi diff --git a/browser/base/content/test/webextensions/browser_extension_update_background_noprompt.js b/browser/base/content/test/webextensions/browser_extension_update_background_noprompt.js index dd813f012381..e9b1eb09feed 100644 --- a/browser/base/content/test/webextensions/browser_extension_update_background_noprompt.js +++ b/browser/base/content/test/webextensions/browser_extension_update_background_noprompt.js @@ -1,7 +1,6 @@ const {AddonManagerPrivate} = ChromeUtils.import("resource://gre/modules/AddonManager.jsm", {}); const ID_PERMS = "update_perms@tests.mozilla.org"; -const ID_LEGACY = "legacy_update@tests.mozilla.org"; const ID_ORIGINS = "update_origins@tests.mozilla.org"; function getBadgeStatus() { @@ -80,11 +79,6 @@ async function testNoPrompt(origUrl, id) { add_task(() => testNoPrompt(`${BASE}/browser_webext_update_perms1.xpi`, ID_PERMS)); -// Test that an update from a legacy extension to a webextension -// doesn't show a prompt even when the webextension uses -// promptable required permissions. -add_task(() => testNoPrompt(`${BASE}/browser_legacy.xpi`, ID_LEGACY)); - // Test that an update that narrows origin permissions is just applied without // showing a notification promt add_task(() => testNoPrompt(`${BASE}/browser_webext_update_origins1.xpi`, diff --git a/browser/base/content/test/webextensions/browser_legacy.xpi b/browser/base/content/test/webextensions/browser_legacy.xpi deleted file mode 100644 index da4b3062796c..000000000000 Binary files a/browser/base/content/test/webextensions/browser_legacy.xpi and /dev/null differ diff --git a/browser/base/content/test/webextensions/browser_update_interactive_noprompt.js b/browser/base/content/test/webextensions/browser_update_interactive_noprompt.js index 46a0894d7db5..fe95a7fadbd5 100644 --- a/browser/base/content/test/webextensions/browser_update_interactive_noprompt.js +++ b/browser/base/content/test/webextensions/browser_update_interactive_noprompt.js @@ -51,11 +51,6 @@ async function testUpdateNoPrompt(filename, id, await addon.uninstall(); } -// Test that we don't see a prompt when updating from a legacy -// extension to a webextension. -add_task(() => testUpdateNoPrompt("browser_legacy.xpi", - "legacy_update@tests.mozilla.org", "1.1")); - // Test that we don't see a prompt when no new promptable permissions // are added. add_task(() => testUpdateNoPrompt("browser_webext_update_perms1.xpi", diff --git a/browser/components/aboutconfig/content/aboutconfig.css b/browser/components/aboutconfig/content/aboutconfig.css index a39b683e0851..2fca8f31a893 100644 --- a/browser/components/aboutconfig/content/aboutconfig.css +++ b/browser/components/aboutconfig/content/aboutconfig.css @@ -14,6 +14,7 @@ background-position: 9px center; background-size: 12px 12px; padding-left: 30px; + z-index: 1; } #prefs { @@ -39,11 +40,23 @@ font-weight: bold; } +#prefs > tr.locked { + opacity: 0.4; + background-image: url("chrome://browser/skin/preferences/in-content/privacy-security.svg"); + background-repeat: no-repeat; + background-position: 9px center; + background-size: 16px 16px; +} + #prefs > tr > td { padding: 4px; width: 50%; } +#prefs > tr > td.cell-name { + padding-inline-start: 30px; +} + .cell-value { word-break: break-all; } diff --git a/browser/components/aboutconfig/content/aboutconfig.js b/browser/components/aboutconfig/content/aboutconfig.js index 3580bf5926be..2b26609b89e7 100644 --- a/browser/components/aboutconfig/content/aboutconfig.js +++ b/browser/components/aboutconfig/content/aboutconfig.js @@ -19,14 +19,16 @@ function onLoad() { let hasUserValue = Services.prefs.prefHasUserValue(name); let pref = { name, - value: Preferences.get(name), hasUserValue, hasDefaultValue: hasUserValue ? prefHasDefaultValue(name) : true, + isLocked: Services.prefs.prefIsLocked(name), }; - // Try in case it's a localized string. - // Throws an exception if there is no equivalent value in the localized files for the pref. + // Try in case it's a localized string or locked user added pref // If an execption is thrown the pref value is set to the empty string. try { + // Throws an exception in case locked user added pref without default value + pref.value = Preferences.get(name); + // Throws an exception if there is no equivalent value in the localized files for the pref. if (!pref.hasUserValue && /^chrome:\/\/.+\/locale\/.+\.properties/.test(pref.value)) { pref.value = Services.prefs.getComplexValue(name, Ci.nsIPrefLocalizedString).data; } @@ -121,6 +123,9 @@ function createPrefsFragment(prefArray) { if (pref.hasUserValue) { row.classList.add("has-user-value"); } + if (pref.isLocked) { + row.classList.add("locked"); + } row.setAttribute("aria-label", pref.name); row.appendChild(getPrefRow(pref)); @@ -135,6 +140,7 @@ function createNewPrefFragment(name) { row.classList.add("has-user-value"); row.setAttribute("aria-label", name); let nameCell = document.createElement("td"); + nameCell.className = "cell-name"; nameCell.append(name); row.appendChild(nameCell); @@ -164,6 +170,7 @@ function createNewPrefFragment(name) { function getPrefRow(pref) { let rowFragment = document.createDocumentFragment(); let nameCell = document.createElement("td"); + nameCell.className = "cell-name"; // Add behind dots to prevent line breaking in random mid-word places. let parts = pref.name.split("."); for (let i = 0; i < parts.length - 1; i++) { @@ -187,11 +194,14 @@ function getPrefRow(pref) { document.l10n.setAttributes(button, "about-config-pref-edit"); button.className = "button-edit"; } + if (pref.isLocked) { + button.disabled = true; + } editCell.appendChild(button); rowFragment.appendChild(editCell); let buttonCell = document.createElement("td"); - if (pref.hasUserValue) { + if (!pref.isLocked && pref.hasUserValue) { let resetButton = document.createElement("button"); if (!pref.hasDefaultValue) { document.l10n.setAttributes(resetButton, "about-config-pref-delete"); @@ -201,6 +211,7 @@ function getPrefRow(pref) { } buttonCell.appendChild(resetButton); } + rowFragment.appendChild(buttonCell); return rowFragment; } diff --git a/browser/components/aboutconfig/test/browser/browser.ini b/browser/components/aboutconfig/test/browser/browser.ini index bbd3d89aac8a..832af5a57ce4 100644 --- a/browser/components/aboutconfig/test/browser/browser.ini +++ b/browser/components/aboutconfig/test/browser/browser.ini @@ -5,3 +5,5 @@ skip-if = debug # Bug 1507747 [browser_search.js] skip-if = debug # Bug 1507747 +[browser_locked.js] + diff --git a/browser/components/aboutconfig/test/browser/browser_locked.js b/browser/components/aboutconfig/test/browser/browser_locked.js new file mode 100644 index 000000000000..2e8f8cac7770 --- /dev/null +++ b/browser/components/aboutconfig/test/browser/browser_locked.js @@ -0,0 +1,70 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +const PAGE_URL = "chrome://browser/content/aboutconfig/aboutconfig.html"; + +add_task(async function setup() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["test.aboutconfig.a", "some value"], + ], + }); +}); + +add_task(async function test_locked() { + registerCleanupFunction(() => { + Services.prefs.unlockPref("browser.search.searchEnginesURL"); + Services.prefs.unlockPref("test.aboutconfig.a"); + Services.prefs.unlockPref("accessibility.AOM.enabled"); + }); + + Services.prefs.lockPref("browser.search.searchEnginesURL"); + Services.prefs.lockPref("test.aboutconfig.a"); + Services.prefs.lockPref("accessibility.AOM.enabled"); + await BrowserTestUtils.withNewTab({ + gBrowser, + url: PAGE_URL, + }, async browser => { + await ContentTask.spawn(browser, null, () => { + let list = [...content.document.getElementById("prefs") + .getElementsByTagName("tr")]; + function getRow(name) { + return list.find(row => row.querySelector("td").textContent == name); + } + function getValue(name) { + return getRow(name).querySelector("td.cell-value").textContent; + } + function getButton(name) { + return getRow(name).querySelector("button"); + } + + // Test locked default string pref. + let lockedPref = getRow("browser.search.searchEnginesURL"); + Assert.ok(lockedPref.classList.contains("locked")); + Assert.equal(getValue("browser.search.searchEnginesURL"), "https://addons.mozilla.org/%LOCALE%/firefox/search-engines/"); + Assert.ok(getButton("browser.search.searchEnginesURL").classList.contains("button-edit")); + Assert.equal(getButton("browser.search.searchEnginesURL").disabled, true); + + // Test locked default boolean pref. + lockedPref = getRow("accessibility.AOM.enabled"); + Assert.ok(lockedPref.classList.contains("locked")); + Assert.equal(getValue("accessibility.AOM.enabled"), "false"); + Assert.ok(getButton("accessibility.AOM.enabled").classList.contains("button-toggle")); + Assert.equal(getButton("accessibility.AOM.enabled").disabled, true); + + // Test locked user added pref. + lockedPref = getRow("test.aboutconfig.a"); + Assert.ok(lockedPref.classList.contains("locked")); + Assert.equal(getValue("test.aboutconfig.a"), ""); + Assert.ok(getButton("test.aboutconfig.a").classList.contains("button-edit")); + Assert.equal(getButton("test.aboutconfig.a").disabled, true); + + // Test pref not locked + let unlockedPref = getRow("accessibility.indicator.enabled"); + Assert.equal(unlockedPref.classList.contains("locked"), false); + Assert.equal(getValue("accessibility.indicator.enabled"), "false"); + Assert.ok(getButton("accessibility.indicator.enabled").classList.contains("button-toggle")); + Assert.equal(getButton("accessibility.indicator.enabled").disabled, false); + }); + }); +}); diff --git a/browser/components/contextualidentity/test/browser/browser_forgetAPI_quota_clearStoragesForPrincipal.js b/browser/components/contextualidentity/test/browser/browser_forgetAPI_quota_clearStoragesForPrincipal.js index 80790ee4a1ea..46ae1cd4c091 100644 --- a/browser/components/contextualidentity/test/browser/browser_forgetAPI_quota_clearStoragesForPrincipal.js +++ b/browser/components/contextualidentity/test/browser/browser_forgetAPI_quota_clearStoragesForPrincipal.js @@ -92,6 +92,10 @@ async function checkIndexedDB(browser) { } catch (e) { is(e.name, "NotFoundError", "The indexedDB does not exist as expected"); } + + db.close(); + + content.indexedDB.deleteDatabase("idb"); }); } @@ -127,7 +131,12 @@ add_task(async function test_quota_clearStoragesForPrincipal() { let httpURI = caUtils.makeURI("http://" + TEST_HOST); let httpPrincipal = Services.scriptSecurityManager .createCodebasePrincipal(httpURI, {}); - Services.qms.clearStoragesForPrincipal(httpPrincipal, null, true); + let clearRequest = Services.qms.clearStoragesForPrincipal(httpPrincipal, null, null, true); + await new Promise(resolve => { + clearRequest.callback = () => { + resolve(); + }; + }); for (let userContextId of Object.keys(USER_CONTEXTS)) { // Open our tab in the given user context. diff --git a/browser/components/extensions/parent/ext-browsingData.js b/browser/components/extensions/parent/ext-browsingData.js index 8b6f811d2b11..28ecc5ffcb4d 100644 --- a/browser/components/extensions/parent/ext-browsingData.js +++ b/browser/components/extensions/parent/ext-browsingData.js @@ -77,22 +77,26 @@ const clearHistory = options => { const clearIndexedDB = async function(options) { let promises = []; - await new Promise(resolve => { + await new Promise((resolve, reject) => { quotaManagerService.getUsage(request => { if (request.resultCode != Cr.NS_OK) { - // We are probably shutting down. We don't want to propagate the error, - // rejecting the promise. - resolve(); + reject({message: "Clear indexedDB failed"}); return; } for (let item of request.result) { let principal = Services.scriptSecurityManager.createCodebasePrincipalFromOrigin(item.origin); - let uri = principal.URI; - if (uri.scheme == "http" || uri.scheme == "https" || uri.scheme == "file") { - promises.push(new Promise(r => { - let req = quotaManagerService.clearStoragesForPrincipal(principal, null, false); - req.callback = () => { r(); }; + let scheme = principal.URI.scheme; + if (scheme == "http" || scheme == "https" || scheme == "file") { + promises.push(new Promise((resolve, reject) => { + let clearRequest = quotaManagerService.clearStoragesForPrincipal(principal, null, "idb"); + clearRequest.callback = () => { + if (clearRequest.resultCode == Cr.NS_OK) { + resolve(); + } else { + reject({message: "Clear indexedDB failed"}); + } + }; })); } } @@ -109,6 +113,46 @@ const clearLocalStorage = async function(options) { return Promise.reject( {message: "Firefox does not support clearing localStorage with 'since'."}); } + + if (Services.lsm.nextGenLocalStorageEnabled) { + // Ideally we could reuse the logic in Sanitizer.jsm or nsIClearDataService, + // but this API exposes an ability to wipe data at a much finger granularity + // than those APIs. So custom logic is used here to wipe only the QM + // localStorage client (when in use). + + let promises = []; + + await new Promise((resolve, reject) => { + quotaManagerService.getUsage(request => { + if (request.resultCode != Cr.NS_OK) { + reject({message: "Clear localStorage failed"}); + return; + } + + for (let item of request.result) { + let principal = Services.scriptSecurityManager.createCodebasePrincipalFromOrigin(item.origin); + let host = principal.URI.hostPort; + if (!options.hostnames || options.hostnames.includes(host)) { + promises.push(new Promise((resolve, reject) => { + let clearRequest = quotaManagerService.clearStoragesForPrincipal(principal, "default", "ls"); + clearRequest.callback = () => { + if (clearRequest.resultCode == Cr.NS_OK) { + resolve(); + } else { + reject({message: "Clear localStorage failed"}); + } + }; + })); + } + } + + resolve(); + }); + }); + + return Promise.all(promises); + } + if (options.hostnames) { for (let hostname of options.hostnames) { Services.obs.notifyObservers(null, "extension:purge-localStorage", hostname); diff --git a/browser/components/extensions/test/browser/browser_ext_browsingData_indexedDB.js b/browser/components/extensions/test/browser/browser_ext_browsingData_indexedDB.js index b4825c7d6910..a65dc4ab71af 100644 --- a/browser/components/extensions/test/browser/browser_ext_browsingData_indexedDB.js +++ b/browser/components/extensions/test/browser/browser_ext_browsingData_indexedDB.js @@ -53,6 +53,9 @@ add_task(async function testIndexedDB() { let origins = []; Services.qms.getUsage(request => { for (let i = 0; i < request.result.length; ++i) { + if (request.result[i].usage === 0) { + continue; + } if (request.result[i].origin.startsWith("http://mochi.test") || request.result[i].origin.startsWith("http://example.com")) { origins.push(request.result[i].origin); diff --git a/browser/components/extensions/test/browser/browser_ext_browsingData_localStorage.js b/browser/components/extensions/test/browser/browser_ext_browsingData_localStorage.js index 2a2425849030..d072153f807c 100644 --- a/browser/components/extensions/test/browser/browser_ext_browsingData_localStorage.js +++ b/browser/components/extensions/test/browser/browser_ext_browsingData_localStorage.js @@ -60,6 +60,9 @@ add_task(async function testLocalStorage() { await browser.browsingData.remove({}, {localStorage: true}); await sendMessageToTabs(tabs, "checkLocalStorageCleared"); + // Cleanup (checkLocalStorageCleared creates empty LS databases). + await browser.browsingData.removeLocalStorage({}); + browser.tabs.remove(tabs.map(tab => tab.id)); browser.test.notifyPass("done"); diff --git a/browser/components/preferences/in-content/tests/siteData/browser.ini b/browser/components/preferences/in-content/tests/siteData/browser.ini index 6c8b7fcba763..c5fe2caee189 100644 --- a/browser/components/preferences/in-content/tests/siteData/browser.ini +++ b/browser/components/preferences/in-content/tests/siteData/browser.ini @@ -9,7 +9,7 @@ support-files = [browser_clearSiteData.js] [browser_siteData.js] -skip-if = (os == 'linux' && debug) # Bug 1439332 +skip-if = (os == 'linux' && debug) || verify # Bug 1439332 and bug 1436395 [browser_siteData2.js] [browser_siteData3.js] [browser_siteData_multi_select.js] diff --git a/browser/components/preferences/in-content/tests/siteData/browser_siteData.js b/browser/components/preferences/in-content/tests/siteData/browser_siteData.js index 8a063c0ab3af..76f02cb90ff6 100644 --- a/browser/components/preferences/in-content/tests/siteData/browser_siteData.js +++ b/browser/components/preferences/in-content/tests/siteData/browser_siteData.js @@ -44,7 +44,7 @@ add_task(async function() { await new Promise(resolve => { let principal = Services.scriptSecurityManager .createCodebasePrincipalFromOrigin(TEST_QUOTA_USAGE_ORIGIN); - let request = Services.qms.clearStoragesForPrincipal(principal, null, true); + let request = Services.qms.clearStoragesForPrincipal(principal, null, null, true); request.callback = resolve; }); diff --git a/browser/modules/SiteDataManager.jsm b/browser/modules/SiteDataManager.jsm index e86aa4b9029e..6f0baac26304 100644 --- a/browser/modules/SiteDataManager.jsm +++ b/browser/modules/SiteDataManager.jsm @@ -304,7 +304,7 @@ var SiteDataManager = { // We are clearing *All* across OAs so need to ensure a principal without suffix here, // or the call of `clearStoragesForPrincipal` would fail. principal = Services.scriptSecurityManager.createCodebasePrincipalFromOrigin(originNoSuffix); - let request = this._qms.clearStoragesForPrincipal(principal, null, true); + let request = this._qms.clearStoragesForPrincipal(principal, null, null, true); request.callback = resolve; })); } diff --git a/caps/ContentPrincipal.cpp b/caps/ContentPrincipal.cpp index b6ced998078c..04481ebfa6f4 100644 --- a/caps/ContentPrincipal.cpp +++ b/caps/ContentPrincipal.cpp @@ -396,8 +396,17 @@ GetSpecialBaseDomain(const nsCOMPtr& aCodebase, { *aHandled = false; - // For a file URI, we return the file path. + // Special handling for a file URI. if (NS_URIIsLocalFile(aCodebase)) { + // If strict file origin policy is not in effect, all local files are + // considered to be same-origin, so return a known dummy domain here. + if (!nsScriptSecurityManager::GetStrictFileOriginPolicy()) { + *aHandled = true; + aBaseDomain.AssignLiteral("UNIVERSAL_FILE_URI_ORIGIN"); + return NS_OK; + } + + // Otherwise, we return the file path. nsCOMPtr url = do_QueryInterface(aCodebase); if (url) { diff --git a/config/check_macroassembler_style.py b/config/check_macroassembler_style.py index d9b1383dcbd6..1b2dcb2fe599 100644 --- a/config/check_macroassembler_style.py +++ b/config/check_macroassembler_style.py @@ -48,6 +48,8 @@ def get_normalized_signatures(signature, fileAnnot=None): signature = signature.replace(';', ' ') # Normalize spaces. signature = re.sub(r'\s+', ' ', signature).strip() + # Remove new-line induced spaces after opening braces. + signature = re.sub(r'\(\s+', '(', signature).strip() # Match arguments, and keep only the type. signature = reMatchArg.sub('\g', signature) # Remove class name @@ -144,45 +146,62 @@ def get_macroassembler_definitions(filename): return [] style_section = False - code_section = False lines = '' signatures = [] with open(filename) as f: for line in f: if '//{{{ check_macroassembler_style' in line: + if style_section: + raise 'check_macroassembler_style section already opened.' style_section = True + braces_depth = 0 elif '//}}} check_macroassembler_style' in line: style_section = False if not style_section: continue + # Remove comments from the processed line. line = re.sub(r'//.*', '', line) - if line.startswith('{') or line.strip() == "{}": + + # Locate and count curly braces. + open_curly_brace = line.find('{') + was_braces_depth = braces_depth + braces_depth = braces_depth + line.count('{') - line.count('}') + + # Raise an error if the check_macroassembler_style macro is used + # across namespaces / classes scopes. + if braces_depth < 0: + raise 'check_macroassembler_style annotations are not well scoped.' + + # If the current line contains an opening curly brace, check if + # this line combines with the previous one can be identified as a + # MacroAssembler function signature. + if open_curly_brace != -1 and was_braces_depth == 0: + lines = lines + line[:open_curly_brace] if 'MacroAssembler::' in lines: signatures.extend( get_normalized_signatures(lines, fileAnnot)) - if line.strip() != "{}": # Empty declaration, no need to declare - # a new code section - code_section = True - continue - if line.startswith('}'): - code_section = False lines = '' continue - if code_section: - continue - if len(line.strip()) == 0: - lines = '' + # We do not aggregate any lines if we are scanning lines which are + # in-between a set of curly braces. + if braces_depth > 0: continue + if was_braces_depth != 0: + line = line[line.rfind('}') + 1:] + + # This logic is used to remove template instantiation, static + # variable definitions and function declaration from the next + # function definition. + last_semi_colon = line.rfind(';') + if last_semi_colon != -1: + lines = '' + line = line[last_semi_colon + 1:] + + # Aggregate lines of non-braced text, which corresponds to the space + # where we are expecting to find function definitions. lines = lines + line - # Continue until we have a complete declaration - if '{' not in lines: - continue - # Skip variable declarations - if ')' not in lines: - lines = '' - continue return signatures @@ -201,14 +220,17 @@ def get_macroassembler_declaration(filename): continue line = re.sub(r'//.*', '', line) - if len(line.strip()) == 0: + if len(line.strip()) == 0 or 'public:' in line or 'private:' in line: lines = '' continue lines = lines + line + # Continue until we have a complete declaration if ';' not in lines: continue - # Skip variable declarations + + # Skip member declarations: which are lines ending with a + # semi-colon without any list of arguments. if ')' not in lines: lines = '' continue diff --git a/devtools/client/aboutdebugging/test/browser.ini b/devtools/client/aboutdebugging/test/browser.ini index 8ee48029be5c..b3809f5bb445 100644 --- a/devtools/client/aboutdebugging/test/browser.ini +++ b/devtools/client/aboutdebugging/test/browser.ini @@ -23,6 +23,8 @@ support-files = !/devtools/client/shared/test/telemetry-test-helpers.js [browser_addons_debug_bootstrapped.js] +# To be removed in bug 1497264 +skip-if = true [browser_addons_debug_info.js] [browser_addons_debug_webextension.js] tags = webextensions @@ -34,11 +36,16 @@ tags = webextensions skip-if = (verify && debug) || (debug && os == "linux" && bits == 64) # verify: crashes on shutdown, timeouts linux debug Bug 1299001 tags = webextensions [browser_addons_debugging_initial_state.js] +# To be removed or updated in bug 1497264 +skip-if = true [browser_addons_install.js] -skip-if = verify && debug +# To be updated in bug 1497264 (was "verify && debug") +skip-if = true [browser_addons_reload.js] [browser_addons_remove.js] [browser_addons_toggle_debug.js] +# To be removed or updated in bug 1497264 +skip-if = true [browser_page_not_found.js] [browser_service_workers.js] [browser_service_workers_fetch_flag.js] diff --git a/devtools/client/aboutdebugging/test/browser_addons_debug_info.js b/devtools/client/aboutdebugging/test/browser_addons_debug_info.js index fb1814beda1f..eb43b72c5102 100644 --- a/devtools/client/aboutdebugging/test/browser_addons_debug_info.js +++ b/devtools/client/aboutdebugging/test/browser_addons_debug_info.js @@ -13,6 +13,8 @@ function testFilePath(container, expectedFilePath) { is(filePath.previousElementSibling.textContent, "Location", "file path has label"); } +// Remove in Bug 1497264 +/* add_task(async function testLegacyAddon() { const addonId = "test-devtools@mozilla.org"; const addonName = "test-devtools"; @@ -32,6 +34,7 @@ add_task(async function testLegacyAddon() { await closeAboutDebugging(tab); }); +*/ add_task(async function testWebExtension() { const addonId = "test-devtools-webextension-nobg@mozilla.org"; diff --git a/devtools/client/aboutdebugging/test/browser_addons_install.js b/devtools/client/aboutdebugging/test/browser_addons_install.js index ab779210ff64..420e306ca381 100644 --- a/devtools/client/aboutdebugging/test/browser_addons_install.js +++ b/devtools/client/aboutdebugging/test/browser_addons_install.js @@ -7,9 +7,6 @@ loader.lazyImporter(this, "AddonTestUtils", AddonTestUtils.initMochitest(this); -const ADDON_ID = "test-devtools@mozilla.org"; -const ADDON_NAME = "test-devtools"; - function mockFilePicker(window, file) { // Mock the file picker to select a test addon const MockFilePicker = SpecialPowers.MockFilePicker; @@ -35,6 +32,9 @@ function promiseWriteWebManifestForExtension(manifest, dir) { } add_task(async function testLegacyInstallSuccess() { + const ADDON_ID = "test-devtools@mozilla.org"; + const ADDON_NAME = "test-devtools"; + const { tab, document } = await openAboutDebugging("addons"); await waitForInitialAddonList(document); diff --git a/devtools/client/aboutdebugging/test/browser_addons_reload.js b/devtools/client/aboutdebugging/test/browser_addons_reload.js index 16353be7d72f..a371a735dfde 100644 --- a/devtools/client/aboutdebugging/test/browser_addons_reload.js +++ b/devtools/client/aboutdebugging/test/browser_addons_reload.js @@ -3,7 +3,6 @@ "use strict"; const ADDON_ID = "test-devtools@mozilla.org"; -const ADDON_NAME = "test-devtools"; const PACKAGED_ADDON_ID = "bug1273184@tests"; const PACKAGED_ADDON_NAME = "bug 1273184"; @@ -60,7 +59,10 @@ class TempWebExt { } } +// Remove in Bug 1497264 +/* add_task(async function reloadButtonReloadsAddon() { + const ADDON_NAME = "test-devtools"; const { tab, document, window } = await openAboutDebugging("addons"); const { AboutDebugging } = window; await waitForInitialAddonList(document); @@ -96,6 +98,7 @@ add_task(async function reloadButtonReloadsAddon() { await tearDownAddon(AboutDebugging, reloadedAddon); await closeAboutDebugging(tab); }); +*/ add_task(async function reloadButtonRefreshesMetadata() { const { tab, document, window } = await openAboutDebugging("addons"); diff --git a/devtools/client/aboutdebugging/test/browser_addons_remove.js b/devtools/client/aboutdebugging/test/browser_addons_remove.js index 27ed891b7b5f..f04a4a15719e 100644 --- a/devtools/client/aboutdebugging/test/browser_addons_remove.js +++ b/devtools/client/aboutdebugging/test/browser_addons_remove.js @@ -12,6 +12,8 @@ function getRemoveButton(document, id) { return document.querySelector(`[data-addon-id="${id}"] .uninstall-button`); } +// Remove in Bug 1497264 +/* add_task(async function removeLegacyExtension() { const addonID = "test-devtools@mozilla.org"; const addonName = "test-devtools"; @@ -36,6 +38,7 @@ add_task(async function removeLegacyExtension() { await closeAboutDebugging(tab); }); +*/ add_task(async function removeWebextension() { const addonID = "test-devtools-webextension@mozilla.org"; diff --git a/devtools/client/inspector/flexbox/flexbox.js b/devtools/client/inspector/flexbox/flexbox.js index 3bda46821f60..e0d234768747 100644 --- a/devtools/client/inspector/flexbox/flexbox.js +++ b/devtools/client/inspector/flexbox/flexbox.js @@ -401,7 +401,7 @@ class FlexboxInspector { * highlighter is toggled on/off for. */ onToggleFlexboxHighlighter(node) { - this.highlighters.toggleFlexboxHighlighter(node); + this.highlighters.toggleFlexboxHighlighter(node, "layout"); this.store.dispatch(updateFlexboxHighlighted(node !== this.highlighters.flexboxHighlighterShow)); } diff --git a/devtools/client/inspector/flexbox/test/browser.ini b/devtools/client/inspector/flexbox/test/browser.ini index b41325aa7b2c..f259c80b553e 100644 --- a/devtools/client/inspector/flexbox/test/browser.ini +++ b/devtools/client/inspector/flexbox/test/browser.ini @@ -26,6 +26,7 @@ support-files = [browser_flexbox_empty_state.js] [browser_flexbox_highlighter_color_picker_on_ESC.js] [browser_flexbox_highlighter_color_picker_on_RETURN.js] +[browser_flexbox_highlighter_opened_telemetry.js] [browser_flexbox_item_list_01.js] [browser_flexbox_item_list_02.js] [browser_flexbox_item_list_updates_on_change.js] diff --git a/devtools/client/inspector/flexbox/test/browser_flexbox_highlighter_opened_telemetry.js b/devtools/client/inspector/flexbox/test/browser_flexbox_highlighter_opened_telemetry.js new file mode 100644 index 000000000000..552b8ec185ae --- /dev/null +++ b/devtools/client/inspector/flexbox/test/browser_flexbox_highlighter_opened_telemetry.js @@ -0,0 +1,32 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that the telemetry is correct when the flexbox highlighter is activated from +// the layout view. + +const TEST_URI = URL_ROOT + "doc_flexbox_specific_cases.html"; + +add_task(async function() { + await addTab(TEST_URI); + startTelemetry(); + const { inspector, flexboxInspector } = await openLayoutView(); + const { document: doc } = flexboxInspector; + const { highlighters, store } = inspector; + + const onFlexHighlighterToggleRendered = waitForDOM(doc, "#flexbox-checkbox-toggle"); + await selectNode("#container", inspector); + const [flexHighlighterToggle] = await onFlexHighlighterToggleRendered; + + await toggleHighlighterON(flexHighlighterToggle, highlighters, store); + await toggleHighlighterOFF(flexHighlighterToggle, highlighters, store); + + checkResults(); +}); + +function checkResults() { + checkTelemetry("devtools.layout.flexboxhighlighter.opened", "", 1, "scalar"); + checkTelemetry("DEVTOOLS_FLEXBOX_HIGHLIGHTER_TIME_ACTIVE_SECONDS", "", null, + "hasentries"); +} diff --git a/devtools/client/inspector/flexbox/test/head.js b/devtools/client/inspector/flexbox/test/head.js index abaed3161a93..5cb52d5b6274 100644 --- a/devtools/client/inspector/flexbox/test/head.js +++ b/devtools/client/inspector/flexbox/test/head.js @@ -2,6 +2,7 @@ /* Any copyright is dedicated to the Public Domain. http://creativecommons.org/publicdomain/zero/1.0/ */ /* eslint no-unused-vars: [2, {"vars": "local"}] */ +/* import-globals-from ../../../shared/test/telemetry-test-helpers.js */ /* import-globals-from ../../test/head.js */ "use strict"; diff --git a/devtools/client/inspector/grids/test/browser.ini b/devtools/client/inspector/grids/test/browser.ini index 7f4b8a745cf9..c0015939362d 100644 --- a/devtools/client/inspector/grids/test/browser.ini +++ b/devtools/client/inspector/grids/test/browser.ini @@ -39,6 +39,7 @@ skip-if = (verify && (os == 'win')) [browser_grids_grid-outline-writing-mode.js] skip-if = (verify && (os == 'win')) [browser_grids_highlighter-setting-rules-grid-toggle.js] +[browser_grids_highlighter-toggle-telemetry.js] [browser_grids_number-of-css-grids-telemetry.js] [browser_grids_persist-color-palette.js] [browser_grids_restored-after-reload.js] diff --git a/devtools/client/inspector/grids/test/browser_grids_highlighter-toggle-telemetry.js b/devtools/client/inspector/grids/test/browser_grids_highlighter-toggle-telemetry.js new file mode 100644 index 000000000000..38ab6a4511ff --- /dev/null +++ b/devtools/client/inspector/grids/test/browser_grids_highlighter-toggle-telemetry.js @@ -0,0 +1,46 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that the telemetry count is correct when the grid highlighter is activated from +// the layout view. + +const TEST_URI = ` + +
+
cell1
+
cell2
+
+`; + +add_task(async function() { + await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + startTelemetry(); + const { gridInspector, inspector } = await openLayoutView(); + const { document: doc } = gridInspector; + const { highlighters, store } = inspector; + + await selectNode("#grid", inspector); + const gridList = doc.getElementById("grid-list"); + const checkbox = gridList.children[0].querySelector("input"); + + info("Toggling ON the CSS grid highlighter from the layout panel."); + const onHighlighterShown = highlighters.once("grid-highlighter-shown"); + const onCheckboxChange = waitUntilState(store, state => + state.grids.length == 1 && + state.grids[0].highlighted); + checkbox.click(); + await onHighlighterShown; + await onCheckboxChange; + + checkResults(); +}); + +function checkResults() { + checkTelemetry("devtools.grid.gridinspector.opened", "", 1, "scalar"); +} diff --git a/devtools/client/inspector/markup/test/browser.ini b/devtools/client/inspector/markup/test/browser.ini index e7b40095b4b1..d50d24c52a3f 100644 --- a/devtools/client/inspector/markup/test/browser.ini +++ b/devtools/client/inspector/markup/test/browser.ini @@ -135,8 +135,10 @@ skip-if = true # Bug 1177550 [browser_markup_events_source_map.js] [browser_markup_events-windowed-host.js] [browser_markup_flex_display_badge.js] +[browser_markup_flex_display_badge_telemetry.js] [browser_markup_grid_display_badge_01.js] [browser_markup_grid_display_badge_02.js] +[browser_markup_grid_display_badge_telemetry.js] [browser_markup_links_01.js] [browser_markup_links_02.js] [browser_markup_links_03.js] diff --git a/devtools/client/inspector/markup/test/browser_markup_flex_display_badge_telemetry.js b/devtools/client/inspector/markup/test/browser_markup_flex_display_badge_telemetry.js new file mode 100644 index 000000000000..cdd9e9888a84 --- /dev/null +++ b/devtools/client/inspector/markup/test/browser_markup_flex_display_badge_telemetry.js @@ -0,0 +1,48 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that the telemetry is correct when the flexbox highlighter is activated from +// the markup view. + +const TEST_URI = ` + +
+`; + +add_task(async function() { + await pushPref("devtools.inspector.flexboxHighlighter.enabled", true); + await pushPref("devtools.flexboxinspector.enabled", true); + await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + startTelemetry(); + const { inspector } = await openLayoutView(); + const { highlighters } = inspector; + + await selectNode("#flex", inspector); + const flexContainer = await getContainerForSelector("#flex", inspector); + const flexDisplayBadge = flexContainer.elt.querySelector( + ".inspector-badge.interactive[data-display]"); + + info("Toggling ON the flexbox highlighter from the flex display badge."); + const onHighlighterShown = highlighters.once("flexbox-highlighter-shown"); + flexDisplayBadge.click(); + await onHighlighterShown; + + info("Toggling OFF the flexbox highlighter from the flex display badge."); + const onHighlighterHidden = highlighters.once("flexbox-highlighter-hidden"); + flexDisplayBadge.click(); + await onHighlighterHidden; + + checkResults(); +}); + +function checkResults() { + checkTelemetry("devtools.markup.flexboxhighlighter.opened", "", 1, "scalar"); + checkTelemetry("DEVTOOLS_FLEXBOX_HIGHLIGHTER_TIME_ACTIVE_SECONDS", "", null, + "hasentries"); +} diff --git a/devtools/client/inspector/markup/test/browser_markup_grid_display_badge_telemetry.js b/devtools/client/inspector/markup/test/browser_markup_grid_display_badge_telemetry.js new file mode 100644 index 000000000000..1b26b7980350 --- /dev/null +++ b/devtools/client/inspector/markup/test/browser_markup_grid_display_badge_telemetry.js @@ -0,0 +1,43 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that the telemetry count is correct when the grid highlighter is activated from +// the markup view. + +const TEST_URI = ` + +
+`; + +add_task(async function() { + await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + startTelemetry(); + const { inspector } = await openLayoutView(); + const { highlighters, store } = inspector; + + await selectNode("#grid", inspector); + const gridContainer = await getContainerForSelector("#grid", inspector); + const gridDisplayBadge = gridContainer.elt.querySelector( + ".inspector-badge.interactive[data-display]"); + + info("Toggling ON the CSS grid highlighter from the grid display badge."); + const onHighlighterShown = highlighters.once("grid-highlighter-shown"); + const onCheckboxChange = waitUntilState(store, state => + state.grids.length === 1 && + state.grids[0].highlighted); + gridDisplayBadge.click(); + await onHighlighterShown; + await onCheckboxChange; + + checkResults(); +}); + +function checkResults() { + checkTelemetry("devtools.markup.gridinspector.opened", "", 1, "scalar"); +} diff --git a/devtools/client/inspector/markup/test/head.js b/devtools/client/inspector/markup/test/head.js index 9be070fda20b..6ad666668e98 100644 --- a/devtools/client/inspector/markup/test/head.js +++ b/devtools/client/inspector/markup/test/head.js @@ -2,6 +2,7 @@ * 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/. */ /* eslint no-unused-vars: [2, {"vars": "local"}] */ +/* import-globals-from ../../../shared/test/telemetry-test-helpers.js */ /* import-globals-from ../../test/head.js */ "use strict"; diff --git a/devtools/client/inspector/markup/views/element-editor.js b/devtools/client/inspector/markup/views/element-editor.js index cc5c5bc92c82..5336a3f66e0a 100644 --- a/devtools/client/inspector/markup/views/element-editor.js +++ b/devtools/client/inspector/markup/views/element-editor.js @@ -749,7 +749,7 @@ ElementEditor.prototype = { this.stopTrackingFlexboxHighlighterEvents(); this._displayBadge.classList.toggle("active"); - await this.highlighters.toggleFlexboxHighlighter(this.node); + await this.highlighters.toggleFlexboxHighlighter(this.node, "markup"); this.startTrackingFlexboxHighlighterEvents(); } diff --git a/devtools/client/inspector/rules/test/browser.ini b/devtools/client/inspector/rules/test/browser.ini index a203ffa2b2c1..02650f979f14 100644 --- a/devtools/client/inspector/rules/test/browser.ini +++ b/devtools/client/inspector/rules/test/browser.ini @@ -166,6 +166,7 @@ skip-if = (os == "win" && debug) # bug 963492: win. [browser_rules_flexbox-highlighter-on-navigate.js] [browser_rules_flexbox-highlighter-on-reload.js] [browser_rules_flexbox-highlighter-restored-after-reload.js] +[browser_rules_flexbox-toggle-telemetry.js] [browser_rules_flexbox-toggle_01.js] [browser_rules_flexbox-toggle_01b.js] [browser_rules_flexbox-toggle_02.js] @@ -176,6 +177,7 @@ skip-if = (os == "win" && debug) # bug 963492: win. [browser_rules_grid-highlighter-on-navigate.js] [browser_rules_grid-highlighter-on-reload.js] [browser_rules_grid-highlighter-restored-after-reload.js] +[browser_rules_grid-toggle-telemetry.js] [browser_rules_grid-toggle_01.js] [browser_rules_grid-toggle_01b.js] [browser_rules_grid-toggle_02.js] diff --git a/devtools/client/inspector/rules/test/browser_rules_flexbox-toggle-telemetry.js b/devtools/client/inspector/rules/test/browser_rules_flexbox-toggle-telemetry.js new file mode 100644 index 000000000000..90a130b29d13 --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_flexbox-toggle-telemetry.js @@ -0,0 +1,46 @@ +/* vim: set ft=javascript ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that the telemetry is correct when the flexbox highlighter is activated from +// the rules view. + +const TEST_URI = ` + +
+`; + +add_task(async function() { + await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + startTelemetry(); + const {inspector, view} = await openRuleView(); + const {highlighters} = view; + + await selectNode("#flex", inspector); + const container = getRuleViewProperty(view, "#flex", "display").valueSpan; + const flexboxToggle = container.querySelector(".ruleview-flex"); + + info("Toggling ON the flexbox highlighter from the rule-view."); + const onHighlighterShown = highlighters.once("flexbox-highlighter-shown"); + flexboxToggle.click(); + await onHighlighterShown; + + info("Toggling OFF the flexbox highlighter from the rule-view."); + const onHighlighterHidden = highlighters.once("flexbox-highlighter-hidden"); + flexboxToggle.click(); + await onHighlighterHidden; + + checkResults(); +}); + +function checkResults() { + checkTelemetry("devtools.rules.flexboxhighlighter.opened", "", 1, "scalar"); + checkTelemetry("DEVTOOLS_FLEXBOX_HIGHLIGHTER_TIME_ACTIVE_SECONDS", "", null, + "hasentries"); +} diff --git a/devtools/client/inspector/rules/test/browser_rules_grid-toggle-telemetry.js b/devtools/client/inspector/rules/test/browser_rules_grid-toggle-telemetry.js new file mode 100644 index 000000000000..0ab5e071a224 --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_grid-toggle-telemetry.js @@ -0,0 +1,42 @@ +/* vim: set ft=javascript ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that the telemetry count is correct when the grid highlighter is activated from +// the rules view. + +const TEST_URI = ` + +
+
cell1
+
cell2
+
+`; + +add_task(async function() { + await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + startTelemetry(); + const {inspector, view} = await openRuleView(); + const highlighters = view.highlighters; + + await selectNode("#grid", inspector); + const container = getRuleViewProperty(view, "#grid", "display").valueSpan; + const gridToggle = container.querySelector(".ruleview-grid"); + + info("Toggling ON the CSS grid highlighter from the rule-view."); + const onHighlighterShown = highlighters.once("grid-highlighter-shown"); + gridToggle.click(); + await onHighlighterShown; + + checkResults(); +}); + +function checkResults() { + checkTelemetry("devtools.rules.gridinspector.opened", "", 1, "scalar"); +} diff --git a/devtools/client/inspector/rules/test/head.js b/devtools/client/inspector/rules/test/head.js index d86003cc5d01..6ad81ab7f6a3 100644 --- a/devtools/client/inspector/rules/test/head.js +++ b/devtools/client/inspector/rules/test/head.js @@ -2,6 +2,7 @@ /* Any copyright is dedicated to the Public Domain. http://creativecommons.org/publicdomain/zero/1.0/ */ /* eslint no-unused-vars: [2, {"vars": "local"}] */ +/* import-globals-from ../../../shared/test/telemetry-test-helpers.js */ /* import-globals-from ../../test/head.js */ "use strict"; diff --git a/devtools/client/inspector/shared/highlighters-overlay.js b/devtools/client/inspector/shared/highlighters-overlay.js index d4f2730cb022..7b63c5da3f9d 100644 --- a/devtools/client/inspector/shared/highlighters-overlay.js +++ b/devtools/client/inspector/shared/highlighters-overlay.js @@ -258,16 +258,18 @@ class HighlightersOverlay { * * @param {NodeFront} node * The NodeFront of the flexbox container element to highlight. - * @param {Object} options - * Object used for passing options to the flexbox highlighter. + * @param. {String} trigger + * String name matching "layout", "markup" or "rule" to indicate where the + * flexbox highlighter was toggled on from. "layout" represents the layout view. + * "markup" represents the markup view. "rule" represents the rule view. */ - async toggleFlexboxHighlighter(node, options = {}) { + async toggleFlexboxHighlighter(node, trigger) { if (node == this.flexboxHighlighterShown) { await this.hideFlexboxHighlighter(node); return; } - await this.showFlexboxHighlighter(node, options); + await this.showFlexboxHighlighter(node, {}, trigger); } /** @@ -277,8 +279,12 @@ class HighlightersOverlay { * The NodeFront of the flexbox container element to highlight. * @param {Object} options * Object used for passing options to the flexbox highlighter. + * @param. {String} trigger + * String name matching "layout", "markup" or "rule" to indicate where the + * flexbox highlighter was toggled on from. "layout" represents the layout view. + * "markup" represents the markup view. "rule" represents the rule view. */ - async showFlexboxHighlighter(node, options) { + async showFlexboxHighlighter(node, options, trigger) { const highlighter = await this._getHighlighter("FlexboxHighlighter"); if (!highlighter) { return; @@ -293,6 +299,17 @@ class HighlightersOverlay { this._toggleRuleViewIcon(node, true, ".ruleview-flex"); + this.telemetry.toolOpened("flexbox_highlighter", this.inspector.toolbox.sessionId, + this); + + if (trigger === "layout") { + this.telemetry.scalarAdd("devtools.layout.flexboxhighlighter.opened", 1); + } else if (trigger === "markup") { + this.telemetry.scalarAdd("devtools.markup.flexboxhighlighter.opened", 1); + } else if (trigger === "rule") { + this.telemetry.scalarAdd("devtools.rules.flexboxhighlighter.opened", 1); + } + try { // Save flexbox highlighter state. const { url } = this.inspector.target; @@ -319,6 +336,9 @@ class HighlightersOverlay { return; } + this.telemetry.toolClosed("flexbox_highlighter", this.inspector.toolbox.sessionId, + this); + this._toggleRuleViewIcon(node, false, ".ruleview-flex"); await this.highlighters.FlexboxHighlighter.hide(); @@ -407,10 +427,10 @@ class HighlightersOverlay { * * @param {NodeFront} node * The NodeFront of the grid container element to highlight. - * @param. {String|null} trigger - * String name matching "grid" or "rule" to indicate where the - * grid highlighter was toggled on from. "grid" represents the grid view - * "rule" represents the rule view. + * @param. {String} trigger + * String name matching "grid", "markup" or "rule" to indicate where the + * grid highlighter was toggled on from. "grid" represents the grid view. + * "markup" represents the markup view. "rule" represents the rule view. */ async toggleGridHighlighter(node, trigger) { if (this.gridHighlighters.has(node)) { @@ -428,10 +448,10 @@ class HighlightersOverlay { * The NodeFront of the grid container element to highlight. * @param {Object} options * Object used for passing options to the grid highlighter. - * @param. {String|null} trigger - * String name matching "grid" or "rule" to indicate where the - * grid highlighter was toggled on from. "grid" represents the grid view - * "rule" represents the rule view. + * @param. {String} trigger + * String name matching "grid", "markup" or "rule" to indicate where the + * grid highlighter was toggled on from. "grid" represents the grid view. + * "markup" represents the markup view. "rule" represents the rule view. */ async showGridHighlighter(node, options, trigger) { // When the grid highlighter has the given node, it is probably called with new @@ -464,9 +484,11 @@ class HighlightersOverlay { this._toggleRuleViewIcon(node, true, ".ruleview-grid"); - if (trigger == "grid") { + if (trigger === "grid") { this.telemetry.scalarAdd("devtools.grid.gridinspector.opened", 1); - } else if (trigger == "rule") { + } else if (trigger === "markup") { + this.telemetry.scalarAdd("devtools.markup.gridinspector.opened", 1); + } else if (trigger === "rule") { this.telemetry.scalarAdd("devtools.rules.gridinspector.opened", 1); } @@ -968,7 +990,7 @@ class HighlightersOverlay { if (this._isRuleViewDisplayFlex(event.target)) { event.stopPropagation(); - this.toggleFlexboxHighlighter(this.inspector.selection.nodeFront); + this.toggleFlexboxHighlighter(this.inspector.selection.nodeFront, "rule"); } if (this._isRuleViewShapeSwatch(event.target)) { diff --git a/devtools/client/shared/telemetry.js b/devtools/client/shared/telemetry.js index 0e0fa863f1b9..8f3f330a6366 100644 --- a/devtools/client/shared/telemetry.js +++ b/devtools/client/shared/telemetry.js @@ -732,16 +732,14 @@ function getChartsFromToolId(id) { timerHist = `DEVTOOLS_${id}_TIME_ACTIVE_SECONDS`; countHist = `DEVTOOLS_${id}_OPENED_COUNT`; break; + case "FLEXBOX_HIGHLIGHTER": + timerHist = `DEVTOOLS_${id}_TIME_ACTIVE_SECONDS`; + break; default: timerHist = `DEVTOOLS_CUSTOM_TIME_ACTIVE_SECONDS`; countHist = `DEVTOOLS_CUSTOM_OPENED_COUNT`; } - if (!timerHist || (!countHist && !countScalar)) { - throw new Error(`getChartsFromToolId cannot be called without a timer ` + - `histogram and either a count histogram or count scalar.`); - } - return { useTimedEvent: useTimedEvent, timerHist: timerHist, diff --git a/devtools/client/shared/test/browser.ini b/devtools/client/shared/test/browser.ini index d5cb61629d95..0f7448c06b84 100644 --- a/devtools/client/shared/test/browser.ini +++ b/devtools/client/shared/test/browser.ini @@ -77,7 +77,9 @@ support-files = [browser_cubic-bezier-06.js] [browser_cubic-bezier-07.js] [browser_dbg_addon-console.js] -skip-if = (e10s && debug || os == 'win' || verify) +# To be removed or updated in bug 1497264 +# previously was: (e10s && debug || os == 'win' || verify) +skip-if = true tags = addons [browser_dbg_debugger-statement.js] skip-if = e10s && debug @@ -245,7 +247,9 @@ skip-if = !e10s || os == "win" # RDM only works for remote tabs, Win: bug 140419 skip-if = verify [browser_theme_switching.js] [browser_dbg_listaddons.js] -skip-if = e10s && debug +# To be removed or updated in bug 1497264 +# previously was: e10s && debug +skip-if = true tags = addons [browser_dbg_listtabs-01.js] [browser_dbg_listtabs-02.js] diff --git a/docshell/base/nsAboutRedirector.cpp b/docshell/base/nsAboutRedirector.cpp index 5c95be2d6133..4bdd53de3a62 100644 --- a/docshell/base/nsAboutRedirector.cpp +++ b/docshell/base/nsAboutRedirector.cpp @@ -7,6 +7,7 @@ #include "nsAboutRedirector.h" #include "nsNetUtil.h" #include "nsAboutProtocolUtils.h" +#include "nsBaseChannel.h" #include "mozilla/ArrayUtils.h" #include "nsIProtocolHandler.h" @@ -27,6 +28,36 @@ struct RedirEntry uint32_t flags; }; +class CrashChannel final : public nsBaseChannel +{ +public: + explicit CrashChannel(nsIURI* aURI) + { + SetURI(aURI); + } + + nsresult OpenContentStream(bool async, nsIInputStream **stream, + nsIChannel** channel) override + { + nsAutoCString spec; + mURI->GetSpec(spec); + + if (spec.EqualsASCII("about:crashparent") && XRE_IsParentProcess()) { + MOZ_CRASH("Crash via about:crashparent"); + } + + if (spec.EqualsASCII("about:crashcontent") && XRE_IsContentProcess()) { + MOZ_CRASH("Crash via about:crashcontent"); + } + + NS_WARNING("Unhandled about:crash* URI or wrong process"); + return NS_ERROR_NOT_IMPLEMENTED; + } + +protected: + virtual ~CrashChannel() = default; +}; + /* Entries which do not have URI_SAFE_FOR_UNTRUSTED_CONTENT will run with chrome privileges. This is potentially dangerous. Please use @@ -145,12 +176,10 @@ static const RedirEntry kRedirMap[] = { }, { "crashparent", "about:blank", - nsIAboutModule::URI_SAFE_FOR_UNTRUSTED_CONTENT | nsIAboutModule::HIDE_FROM_ABOUTABOUT }, { "crashcontent", "about:blank", - nsIAboutModule::URI_SAFE_FOR_UNTRUSTED_CONTENT | nsIAboutModule::HIDE_FROM_ABOUTABOUT | nsIAboutModule::URI_CAN_LOAD_IN_CHILD | nsIAboutModule::URI_MUST_LOAD_IN_CHILD @@ -174,12 +203,10 @@ nsAboutRedirector::NewChannel(nsIURI* aURI, nsCOMPtr ioService = do_GetIOService(&rv); NS_ENSURE_SUCCESS(rv, rv); - if (XRE_IsParentProcess() && path.EqualsASCII("crashparent")) { - MOZ_CRASH("Crash via about:crashparent"); - } - - if (XRE_IsContentProcess() && path.EqualsASCII("crashcontent")) { - MOZ_CRASH("Crash via about:crashcontent"); + if (path.EqualsASCII("crashparent") || path.EqualsASCII("crashcontent")) { + nsCOMPtr channel = new CrashChannel(aURI); + channel.forget(aResult); + return NS_OK; } #ifdef ABOUT_CONFIG_BLOCKED_GV diff --git a/docshell/test/mochitest/mochitest.ini b/docshell/test/mochitest/mochitest.ini index 4455fcb7d2f4..8f12715a905e 100644 --- a/docshell/test/mochitest/mochitest.ini +++ b/docshell/test/mochitest/mochitest.ini @@ -115,3 +115,4 @@ support-files = file_framedhistoryframes.html [test_pushState_after_document_open.html] [test_windowedhistoryframes.html] [test_triggeringprincipal_location_seturi.html] +[test_bug1507702.html] diff --git a/docshell/test/mochitest/test_bug1507702.html b/docshell/test/mochitest/test_bug1507702.html new file mode 100644 index 000000000000..94e140df4b19 --- /dev/null +++ b/docshell/test/mochitest/test_bug1507702.html @@ -0,0 +1,57 @@ + + + + + + Test for Bug 1507702 + + + + + +Mozilla Bug 1507702 + + + + + + + diff --git a/dom/asmjscache/AsmJSCache.cpp b/dom/asmjscache/AsmJSCache.cpp index b92714a05bf8..368626706290 100644 --- a/dom/asmjscache/AsmJSCache.cpp +++ b/dom/asmjscache/AsmJSCache.cpp @@ -792,7 +792,9 @@ ParentRunnable::ReadMetadata() nsresult rv = qm->EnsureOriginIsInitialized(quota::PERSISTENCE_TYPE_TEMPORARY, mSuffix, - mGroup, mOrigin, getter_AddRefs(mDirectory)); + mGroup, mOrigin, + /* aCreateIfNotExists */ true, + getter_AddRefs(mDirectory)); if (NS_WARN_IF(NS_FAILED(rv))) { mResult = JS::AsmJSCache_StorageInitFailure; return rv; diff --git a/dom/base/ChromeUtils.cpp b/dom/base/ChromeUtils.cpp index 381339ab4cea..d69abf666169 100644 --- a/dom/base/ChromeUtils.cpp +++ b/dom/base/ChromeUtils.cpp @@ -619,10 +619,7 @@ ChromeUtils::IsOriginAttributesEqual(dom::GlobalObject& aGlobal, ChromeUtils::IsOriginAttributesEqual(const dom::OriginAttributesDictionary& aA, const dom::OriginAttributesDictionary& aB) { - return aA.mAppId == aB.mAppId && - aA.mInIsolatedMozBrowser == aB.mInIsolatedMozBrowser && - aA.mUserContextId == aB.mUserContextId && - aA.mPrivateBrowsingId == aB.mPrivateBrowsingId; + return aA == aB; } #ifdef NIGHTLY_BUILD diff --git a/dom/base/nsGlobalWindowInner.cpp b/dom/base/nsGlobalWindowInner.cpp index 112502b61ffc..f37f8e4415ca 100644 --- a/dom/base/nsGlobalWindowInner.cpp +++ b/dom/base/nsGlobalWindowInner.cpp @@ -23,6 +23,8 @@ #include "mozilla/dom/DOMPrefs.h" #include "mozilla/dom/EventTarget.h" #include "mozilla/dom/LocalStorage.h" +#include "mozilla/dom/LocalStorageCommon.h" +#include "mozilla/dom/LSObject.h" #include "mozilla/dom/PartitionedLocalStorage.h" #include "mozilla/dom/Storage.h" #include "mozilla/dom/IdleRequest.h" @@ -4908,32 +4910,38 @@ nsGlobalWindowInner::GetLocalStorage(ErrorResult& aError) if (access != nsContentUtils::StorageAccess::ePartitionedOrDeny && (!mLocalStorage || mLocalStorage->Type() == Storage::ePartitionedLocalStorage)) { - nsresult rv; - nsCOMPtr storageManager = - do_GetService("@mozilla.org/dom/localStorage-manager;1", &rv); - if (NS_FAILED(rv)) { - aError.Throw(rv); - return nullptr; - } + RefPtr storage; - nsString documentURI; - if (mDoc) { - aError = mDoc->GetDocumentURI(documentURI); - if (NS_WARN_IF(aError.Failed())) { + if (NextGenLocalStorageEnabled()) { + aError = LSObject::CreateForWindow(this, getter_AddRefs(storage)); + } else { + nsresult rv; + nsCOMPtr storageManager = + do_GetService("@mozilla.org/dom/localStorage-manager;1", &rv); + if (NS_FAILED(rv)) { + aError.Throw(rv); return nullptr; } + + nsString documentURI; + if (mDoc) { + aError = mDoc->GetDocumentURI(documentURI); + if (NS_WARN_IF(aError.Failed())) { + return nullptr; + } + } + + nsIPrincipal *principal = GetPrincipal(); + if (!principal) { + aError.Throw(NS_ERROR_DOM_SECURITY_ERR); + return nullptr; + } + + aError = storageManager->CreateStorage(this, principal, documentURI, + IsPrivateBrowsing(), + getter_AddRefs(storage)); } - nsIPrincipal *principal = GetPrincipal(); - if (!principal) { - aError.Throw(NS_ERROR_DOM_SECURITY_ERR); - return nullptr; - } - - RefPtr storage; - aError = storageManager->CreateStorage(this, principal, documentURI, - IsPrivateBrowsing(), - getter_AddRefs(storage)); if (aError.Failed()) { return nullptr; } @@ -5809,7 +5817,8 @@ nsGlobalWindowInner::ObserveStorageNotification(StorageEvent* aEvent, MOZ_DIAGNOSTIC_ASSERT(StorageUtils::PrincipalsEqual(aEvent->GetPrincipal(), principal)); - fireMozStorageChanged = mLocalStorage == aEvent->GetStorageArea(); + fireMozStorageChanged = + mLocalStorage && mLocalStorage == aEvent->GetStorageArea(); if (fireMozStorageChanged) { eventType.AssignLiteral("MozLocalStorageChanged"); @@ -5821,7 +5830,7 @@ nsGlobalWindowInner::ObserveStorageNotification(StorageEvent* aEvent, IgnoredErrorResult error; RefPtr clonedEvent = CloneStorageEvent(eventType, aEvent, error); - if (error.Failed()) { + if (error.Failed() || !clonedEvent) { return; } @@ -5856,16 +5865,18 @@ nsGlobalWindowInner::CloneStorageEvent(const nsAString& aType, // If null, this is a localStorage event received by IPC. if (!storageArea) { storage = GetLocalStorage(aRv); - if (aRv.Failed() || !storage) { - return nullptr; - } + if (!NextGenLocalStorageEnabled()) { + if (aRv.Failed() || !storage) { + return nullptr; + } - if (storage->Type() == Storage::eLocalStorage) { - RefPtr localStorage = - static_cast(storage.get()); + if (storage->Type() == Storage::eLocalStorage) { + RefPtr localStorage = + static_cast(storage.get()); - // We must apply the current change to the 'local' localStorage. - localStorage->ApplyEvent(aEvent); + // We must apply the current change to the 'local' localStorage. + localStorage->ApplyEvent(aEvent); + } } } else if (storageArea->Type() == Storage::eSessionStorage) { storage = GetSessionStorage(aRv); @@ -6790,6 +6801,13 @@ nsGlobalWindowInner::EventListenerAdded(nsAtom* aType) ErrorResult rv; GetLocalStorage(rv); rv.SuppressException(); + + if (NextGenLocalStorageEnabled() && + mLocalStorage && mLocalStorage->Type() == Storage::eLocalStorage) { + auto object = static_cast(mLocalStorage.get()); + + Unused << NS_WARN_IF(NS_FAILED(object->EnsureObserver())); + } } } @@ -6803,6 +6821,20 @@ nsGlobalWindowInner::EventListenerRemoved(nsAtom* aType) MOZ_ASSERT(mBeforeUnloadListenerCount >= 0); mTabChild->BeforeUnloadRemoved(); } + + if (aType == nsGkAtoms::onstorage) { + if (NextGenLocalStorageEnabled() && + mLocalStorage && + mLocalStorage->Type() == Storage::eLocalStorage && + // The remove event is fired even if this isn't the last listener, so + // only remove if there are no other listeners left. + mListenerManager && + !mListenerManager->HasListenersFor(nsGkAtoms::onstorage)) { + auto object = static_cast(mLocalStorage.get()); + + object->DropObserver(); + } + } } void @@ -7964,6 +7996,14 @@ nsGlobalWindowInner::StorageAccessGranted() MOZ_ASSERT(mLocalStorage && mLocalStorage->Type() == Storage::eLocalStorage); + + if (NextGenLocalStorageEnabled() && + mListenerManager && + mListenerManager->HasListenersFor(nsGkAtoms::onstorage)) { + auto object = static_cast(mLocalStorage.get()); + + object->EnsureObserver(); + } } } diff --git a/dom/base/nsGlobalWindowOuter.cpp b/dom/base/nsGlobalWindowOuter.cpp index 3a3a984f1994..51a1b602afc9 100644 --- a/dom/base/nsGlobalWindowOuter.cpp +++ b/dom/base/nsGlobalWindowOuter.cpp @@ -27,6 +27,7 @@ #include "mozilla/dom/ContentFrameMessageManager.h" #include "mozilla/dom/EventTarget.h" #include "mozilla/dom/LocalStorage.h" +#include "mozilla/dom/LSObject.h" #include "mozilla/dom/Storage.h" #include "mozilla/dom/IdleRequest.h" #include "mozilla/dom/Performance.h" diff --git a/dom/base/test/mochitest.ini b/dom/base/test/mochitest.ini index b59608e00ae1..18b5f73d6ac8 100644 --- a/dom/base/test/mochitest.ini +++ b/dom/base/test/mochitest.ini @@ -632,7 +632,7 @@ subsuite = clipboard skip-if = toolkit == 'android' #bug 904183 [test_copypaste.xhtml] subsuite = clipboard -skip-if = toolkit == 'android' #bug 904183 +skip-if = toolkit == 'android' && !e10s #bug 904183 [test_createHTMLDocument.html] [test_data_uri.html] skip-if = verify diff --git a/dom/bindings/BindingDeclarations.h b/dom/bindings/BindingDeclarations.h index cd1226adbfe1..710fb1f9b2bc 100644 --- a/dom/bindings/BindingDeclarations.h +++ b/dom/bindings/BindingDeclarations.h @@ -163,6 +163,11 @@ public: return mImpl == aOther.mImpl; } + bool operator!=(const Optional_base& aOther) const + { + return mImpl != aOther.mImpl; + } + template explicit Optional_base(const T1& aValue1, const T2& aValue2) { diff --git a/dom/bindings/Codegen.py b/dom/bindings/Codegen.py index d5ddd3134bf8..b4b6d45dc485 100644 --- a/dom/bindings/Codegen.py +++ b/dom/bindings/Codegen.py @@ -12996,6 +12996,30 @@ class CGDictionary(CGThing): "aOther")], body=body.define()) + def canHaveEqualsOperator(self): + return all(m.type.isString() or m.type.isPrimitive() for (m,_) in + self.memberInfo) + + def equalsOperator(self): + body = CGList([]) + + for m, _ in self.memberInfo: + memberName = self.makeMemberName(m.identifier.name) + memberTest = CGGeneric(fill( + """ + if (${memberName} != aOther.${memberName}) { + return false; + } + """, + memberName=memberName)) + body.append(memberTest) + body.append(CGGeneric("return true;\n")) + return ClassMethod( + "operator==", "bool", + [Argument("const %s&" % self.makeClassName(self.dictionary), + "aOther") + ], const=True, body=body.define()) + def getStructs(self): d = self.dictionary selfName = self.makeClassName(d) @@ -13076,6 +13100,9 @@ class CGDictionary(CGThing): else: disallowCopyConstruction = True + if self.canHaveEqualsOperator(): + methods.append(self.equalsOperator()) + struct = CGClass(selfName, bases=[ClassBase(self.base())], members=members, diff --git a/dom/browser-element/mochitest/mochitest.ini b/dom/browser-element/mochitest/mochitest.ini index d4d65b5822a6..5ce50d7a7b5b 100644 --- a/dom/browser-element/mochitest/mochitest.ini +++ b/dom/browser-element/mochitest/mochitest.ini @@ -107,7 +107,7 @@ support-files = [test_browserElement_inproc_CookiesNotThirdParty.html] [test_browserElement_inproc_CopyPaste.html] subsuite = clipboard -skip-if = (os == "android") # Disabled on Android, see bug 1230421 +skip-if = (os == "android" && !e10s) # Disabled on Android, see bug 1230421 [test_browserElement_inproc_DataURI.html] [test_browserElement_inproc_ExposableURI.html] [test_browserElement_inproc_FirstPaint.html] diff --git a/dom/cache/Context.cpp b/dom/cache/Context.cpp index ea49046f0c67..9bfdce27a276 100644 --- a/dom/cache/Context.cpp +++ b/dom/cache/Context.cpp @@ -435,6 +435,7 @@ Context::QuotaInitRunnable::Run() mQuotaInfo.mSuffix, mQuotaInfo.mGroup, mQuotaInfo.mOrigin, + /* aCreateIfNotExists */ true, getter_AddRefs(mQuotaInfo.mDir)); if (NS_FAILED(rv)) { resolver->Resolve(rv); diff --git a/dom/cache/FileUtils.cpp b/dom/cache/FileUtils.cpp index f51621d7a448..8b1f0eb675c1 100644 --- a/dom/cache/FileUtils.cpp +++ b/dom/cache/FileUtils.cpp @@ -294,7 +294,7 @@ BodyMaybeUpdatePaddingSize(const QuotaInfo& aQuotaInfo, nsIFile* aBaseDir, int64_t fileSize = 0; RefPtr quotaObject = quotaManager->GetQuotaObject(PERSISTENCE_TYPE_DEFAULT, aQuotaInfo.mGroup, - aQuotaInfo.mOrigin, bodyFile, &fileSize); + aQuotaInfo.mOrigin, bodyFile, -1, &fileSize); MOZ_DIAGNOSTIC_ASSERT(quotaObject); MOZ_DIAGNOSTIC_ASSERT(fileSize >= 0); // XXXtt: bug: https://bugzilla.mozilla.org/show_bug.cgi?id=1422815 diff --git a/dom/file/ipc/IPCBlobInputStreamThread.cpp b/dom/file/ipc/IPCBlobInputStreamThread.cpp index 15671b7cb769..a4cfa52a442b 100644 --- a/dom/file/ipc/IPCBlobInputStreamThread.cpp +++ b/dom/file/ipc/IPCBlobInputStreamThread.cpp @@ -247,5 +247,22 @@ IPCBlobInputStreamThread::DelayedDispatch(already_AddRefed, uint32_ return NS_ERROR_NOT_IMPLEMENTED; } +bool +IsOnDOMFileThread() +{ + mozilla::StaticMutexAutoLock lock(gIPCBlobThreadMutex); + + MOZ_ASSERT(!gShutdownHasStarted); + MOZ_ASSERT(gIPCBlobThread); + + return gIPCBlobThread->IsOnCurrentThreadInfallible(); +} + +void +AssertIsOnDOMFileThread() +{ + MOZ_ASSERT(IsOnDOMFileThread()); +} + } // dom namespace } // mozilla namespace diff --git a/dom/file/ipc/IPCBlobInputStreamThread.h b/dom/file/ipc/IPCBlobInputStreamThread.h index 45900a6182a6..02bda56e1de7 100644 --- a/dom/file/ipc/IPCBlobInputStreamThread.h +++ b/dom/file/ipc/IPCBlobInputStreamThread.h @@ -54,6 +54,12 @@ private: nsTArray> mPendingActors; }; +bool +IsOnDOMFileThread(); + +void +AssertIsOnDOMFileThread(); + } // dom namespace } // mozilla namespace diff --git a/dom/indexedDB/ActorsParent.cpp b/dom/indexedDB/ActorsParent.cpp index 4e611ecb2139..fccd8b4b69c6 100644 --- a/dom/indexedDB/ActorsParent.cpp +++ b/dom/indexedDB/ActorsParent.cpp @@ -18419,11 +18419,13 @@ Maintenance::DirectoryWork() // Idle maintenance may occur before origin is initailized. // Ensure origin is initialized first. It will initialize all origins // for temporary storage including IDB origins. - rv = quotaManager->EnsureOriginIsInitialized(persistenceType, - suffix, - group, - origin, - getter_AddRefs(directory)); + rv = quotaManager->EnsureOriginIsInitialized( + persistenceType, + suffix, + group, + origin, + /* aCreateIfNotExists */ true, + getter_AddRefs(directory)); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; @@ -21319,6 +21321,7 @@ OpenDatabaseOp::DoDatabaseWork() mSuffix, mGroup, mOrigin, + /* aCreateIfNotExists */ true, getter_AddRefs(dbDirectory)); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; diff --git a/dom/interfaces/storage/nsIDOMStorageManager.idl b/dom/interfaces/storage/nsIDOMStorageManager.idl index aad0aa2ba9da..894995ec9964 100644 --- a/dom/interfaces/storage/nsIDOMStorageManager.idl +++ b/dom/interfaces/storage/nsIDOMStorageManager.idl @@ -49,6 +49,11 @@ interface nsIDOMStorageManager : nsISupports in AString aDocumentURI, [optional] in bool aPrivate); /** + * DEPRECATED. The only good reason to use this was if you were writing a + * test and wanted to hackily determine if a preload happened. That's now + * covered by `nsILocalStorageManager.isPreloaded` and you should use that if + * that's what you want. If LSNG is in use, this will throw. + * * Returns instance of DOM storage object for given principal. * If there is no storage managed for the scope, then null is returned and * no object is created. Otherwise, an object (new) for the existing storage diff --git a/dom/ipc/ContentChild.cpp b/dom/ipc/ContentChild.cpp index fd51a6c42f7e..78321b413c2a 100644 --- a/dom/ipc/ContentChild.cpp +++ b/dom/ipc/ContentChild.cpp @@ -41,6 +41,7 @@ #include "mozilla/dom/FileCreatorHelper.h" #include "mozilla/dom/GetFilesHelper.h" #include "mozilla/dom/IPCBlobUtils.h" +#include "mozilla/dom/LSObject.h" #include "mozilla/dom/MemoryReportRequest.h" #include "mozilla/dom/PLoginReputationChild.h" #include "mozilla/dom/PushNotifier.h" @@ -3811,15 +3812,21 @@ ContentChild::GetSpecificMessageEventTarget(const Message& aMsg) } } -#ifdef NIGHTLY_BUILD void ContentChild::OnChannelReceivedMessage(const Message& aMsg) { + if (aMsg.is_sync()) { + LSObject::CancelSyncLoop(); + } + +#ifdef NIGHTLY_BUILD if (nsContentUtils::IsMessageInputEvent(aMsg)) { mPendingInputEvents++; } +#endif } +#ifdef NIGHTLY_BUILD PContentChild::Result ContentChild::OnMessageReceived(const Message& aMsg) { diff --git a/dom/ipc/ContentChild.h b/dom/ipc/ContentChild.h index 9cf18b87a9aa..1b960f3841ae 100644 --- a/dom/ipc/ContentChild.h +++ b/dom/ipc/ContentChild.h @@ -768,10 +768,10 @@ private: virtual already_AddRefed GetSpecificMessageEventTarget(const Message& aMsg) override; -#ifdef NIGHTLY_BUILD virtual void OnChannelReceivedMessage(const Message& aMsg) override; +#ifdef NIGHTLY_BUILD virtual PContentChild::Result OnMessageReceived(const Message& aMsg) override; diff --git a/dom/ipc/ContentParent.cpp b/dom/ipc/ContentParent.cpp index 52ca081ca838..8294fdd69d4f 100644 --- a/dom/ipc/ContentParent.cpp +++ b/dom/ipc/ContentParent.cpp @@ -49,6 +49,7 @@ #include "mozilla/dom/ExternalHelperAppParent.h" #include "mozilla/dom/GetFilesHelper.h" #include "mozilla/dom/GeolocationBinding.h" +#include "mozilla/dom/LocalStorageCommon.h" #include "mozilla/dom/MemoryReportRequest.h" #include "mozilla/dom/Notification.h" #include "mozilla/dom/PContentBridgeParent.h" @@ -141,6 +142,7 @@ #include "nsIGfxInfo.h" #include "nsIIdleService.h" #include "nsIInterfaceRequestorUtils.h" +#include "nsILocalStorageManager.h" #include "nsIMemoryInfoDumper.h" #include "nsIMemoryReporter.h" #include "nsIMozBrowserFrame.h" @@ -5707,6 +5709,24 @@ ContentParent::AboutToLoadHttpFtpWyciwygDocumentForChild(nsIChannel* aChannel) UpdateCookieStatus(aChannel); } + if (!NextGenLocalStorageEnabled()) { + return NS_OK; + } + + if (principal->GetIsCodebasePrincipal()) { + nsCOMPtr lsm = + do_GetService("@mozilla.org/dom/localStorage-manager;1"); + if (NS_WARN_IF(!lsm)) { + return NS_ERROR_FAILURE; + } + + nsCOMPtr dummy; + rv = lsm->Preload(principal, nullptr, getter_AddRefs(dummy)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } + return NS_OK; } diff --git a/dom/localstorage/ActorsChild.cpp b/dom/localstorage/ActorsChild.cpp new file mode 100644 index 000000000000..39eaf0499db3 --- /dev/null +++ b/dom/localstorage/ActorsChild.cpp @@ -0,0 +1,336 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/. */ + +#include "ActorsChild.h" + +#include "LocalStorageCommon.h" +#include "LSDatabase.h" +#include "LSObject.h" +#include "LSObserver.h" +#include "LSSnapshot.h" + +namespace mozilla { +namespace dom { + +/******************************************************************************* + * LSDatabaseChild + ******************************************************************************/ + +LSDatabaseChild::LSDatabaseChild(LSDatabase* aDatabase) + : mDatabase(aDatabase) +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(aDatabase); + + MOZ_COUNT_CTOR(LSDatabaseChild); +} + +LSDatabaseChild::~LSDatabaseChild() +{ + AssertIsOnOwningThread(); + + MOZ_COUNT_DTOR(LSDatabaseChild); +} + +void +LSDatabaseChild::SendDeleteMeInternal() +{ + AssertIsOnOwningThread(); + + if (mDatabase) { + mDatabase->ClearActor(); + mDatabase = nullptr; + + MOZ_ALWAYS_TRUE(PBackgroundLSDatabaseChild::SendDeleteMe()); + } +} + +void +LSDatabaseChild::ActorDestroy(ActorDestroyReason aWhy) +{ + AssertIsOnOwningThread(); + + if (mDatabase) { + mDatabase->ClearActor(); +#ifdef DEBUG + mDatabase = nullptr; +#endif + } +} + +mozilla::ipc::IPCResult +LSDatabaseChild::RecvRequestAllowToClose() +{ + AssertIsOnOwningThread(); + + if (mDatabase) { + mDatabase->RequestAllowToClose(); + + // TODO: A new datastore will be prepared at first LocalStorage API + // synchronous call. It would be better to start preparing a new + // datastore right here, but asynchronously. + // However, we probably shouldn't do that if we are shutting down. + } + + return IPC_OK(); +} + +PBackgroundLSSnapshotChild* +LSDatabaseChild::AllocPBackgroundLSSnapshotChild(const nsString& aDocumentURI, + const bool& aIncreasePeakUsage, + const int64_t& aRequestedSize, + const int64_t& aMinSize, + LSSnapshotInitInfo* aInitInfo) +{ + MOZ_CRASH("PBackgroundLSSnapshotChild actor should be manually constructed!"); +} + +bool +LSDatabaseChild::DeallocPBackgroundLSSnapshotChild( + PBackgroundLSSnapshotChild* aActor) +{ + MOZ_ASSERT(aActor); + + delete aActor; + return true; +} + +/******************************************************************************* + * LSObserverChild + ******************************************************************************/ + +LSObserverChild::LSObserverChild(LSObserver* aObserver) + : mObserver(aObserver) +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(aObserver); + + MOZ_COUNT_CTOR(LSObserverChild); +} + +LSObserverChild::~LSObserverChild() +{ + AssertIsOnOwningThread(); + + MOZ_COUNT_DTOR(LSObserverChild); +} + +void +LSObserverChild::SendDeleteMeInternal() +{ + AssertIsOnOwningThread(); + + if (mObserver) { + mObserver->ClearActor(); + mObserver = nullptr; + + MOZ_ALWAYS_TRUE(PBackgroundLSObserverChild::SendDeleteMe()); + } +} + +void +LSObserverChild::ActorDestroy(ActorDestroyReason aWhy) +{ + AssertIsOnOwningThread(); + + if (mObserver) { + mObserver->ClearActor(); +#ifdef DEBUG + mObserver = nullptr; +#endif + } +} + +mozilla::ipc::IPCResult +LSObserverChild::RecvObserve(const PrincipalInfo& aPrincipalInfo, + const uint32_t& aPrivateBrowsingId, + const nsString& aDocumentURI, + const nsString& aKey, + const nsString& aOldValue, + const nsString& aNewValue) +{ + AssertIsOnOwningThread(); + + if (!mObserver) { + return IPC_OK(); + } + + nsresult rv; + nsCOMPtr principal = + PrincipalInfoToPrincipal(aPrincipalInfo, &rv); + if (NS_WARN_IF(NS_FAILED(rv))) { + return IPC_FAIL_NO_REASON(this); + } + + Storage::NotifyChange(/* aStorage */ nullptr, + principal, + aKey, + aOldValue, + aNewValue, + /* aStorageType */ kLocalStorageType, + aDocumentURI, + /* aIsPrivate */ !!aPrivateBrowsingId, + /* aImmediateDispatch */ true); + + return IPC_OK(); +} + +/******************************************************************************* + * LocalStorageRequestChild + ******************************************************************************/ + +LSRequestChild::LSRequestChild(LSRequestChildCallback* aCallback) + : mCallback(aCallback) + , mFinishing(false) +{ + AssertIsOnOwningThread(); + + MOZ_COUNT_CTOR(LSRequestChild); +} + +LSRequestChild::~LSRequestChild() +{ + AssertIsOnOwningThread(); + + MOZ_COUNT_DTOR(LSRequestChild); +} + +bool +LSRequestChild::Finishing() const +{ + AssertIsOnOwningThread(); + + return mFinishing; +} + +void +LSRequestChild::ActorDestroy(ActorDestroyReason aWhy) +{ + AssertIsOnOwningThread(); +} + +mozilla::ipc::IPCResult +LSRequestChild::Recv__delete__(const LSRequestResponse& aResponse) +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(mCallback); + + mCallback->OnResponse(aResponse); + + return IPC_OK(); +} + +mozilla::ipc::IPCResult +LSRequestChild::RecvReady() +{ + AssertIsOnOwningThread(); + + mFinishing = true; + + SendFinish(); + + return IPC_OK(); +} + +/******************************************************************************* + * LSSimpleRequestChild + ******************************************************************************/ + +LSSimpleRequestChild::LSSimpleRequestChild( + LSSimpleRequestChildCallback* aCallback) + : mCallback(aCallback) +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(aCallback); + + MOZ_COUNT_CTOR(LSSimpleRequestChild); +} + +LSSimpleRequestChild::~LSSimpleRequestChild() +{ + AssertIsOnOwningThread(); + + MOZ_COUNT_DTOR(LSSimpleRequestChild); +} + +void +LSSimpleRequestChild::ActorDestroy(ActorDestroyReason aWhy) +{ + AssertIsOnOwningThread(); +} + +mozilla::ipc::IPCResult +LSSimpleRequestChild::Recv__delete__(const LSSimpleRequestResponse& aResponse) +{ + AssertIsOnOwningThread(); + + mCallback->OnResponse(aResponse); + + return IPC_OK(); +} + +/******************************************************************************* + * LSSnapshotChild + ******************************************************************************/ + +LSSnapshotChild::LSSnapshotChild(LSSnapshot* aSnapshot) + : mSnapshot(aSnapshot) +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(aSnapshot); + + MOZ_COUNT_CTOR(LSSnapshotChild); +} + +LSSnapshotChild::~LSSnapshotChild() +{ + AssertIsOnOwningThread(); + + MOZ_COUNT_DTOR(LSSnapshotChild); +} + +void +LSSnapshotChild::SendDeleteMeInternal() +{ + AssertIsOnOwningThread(); + + if (mSnapshot) { + mSnapshot->ClearActor(); + mSnapshot = nullptr; + + MOZ_ALWAYS_TRUE(PBackgroundLSSnapshotChild::SendDeleteMe()); + } +} + +void +LSSnapshotChild::ActorDestroy(ActorDestroyReason aWhy) +{ + AssertIsOnOwningThread(); + + if (mSnapshot) { + mSnapshot->ClearActor(); +#ifdef DEBUG + mSnapshot = nullptr; +#endif + } +} + +mozilla::ipc::IPCResult +LSSnapshotChild::RecvMarkDirty() +{ + AssertIsOnOwningThread(); + + if (!mSnapshot) { + return IPC_OK(); + } + + mSnapshot->MarkDirty(); + + return IPC_OK(); +} + +} // namespace dom +} // namespace mozilla diff --git a/dom/localstorage/ActorsChild.h b/dom/localstorage/ActorsChild.h new file mode 100644 index 000000000000..a8dbbace27b8 --- /dev/null +++ b/dom/localstorage/ActorsChild.h @@ -0,0 +1,313 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/. */ + +#ifndef mozilla_dom_localstorage_ActorsChild_h +#define mozilla_dom_localstorage_ActorsChild_h + +#include "mozilla/dom/PBackgroundLSDatabaseChild.h" +#include "mozilla/dom/PBackgroundLSObserverChild.h" +#include "mozilla/dom/PBackgroundLSRequestChild.h" +#include "mozilla/dom/PBackgroundLSSimpleRequestChild.h" +#include "mozilla/dom/PBackgroundLSSnapshotChild.h" + +namespace mozilla { + +namespace ipc { + +class BackgroundChildImpl; + +} // namespace ipc + +namespace dom { + +class LocalStorageManager2; +class LSDatabase; +class LSObject; +class LSObserver; +class LSRequestChildCallback; +class LSSimpleRequestChildCallback; +class LSSnapshot; + +/** + * Minimal glue actor with standard IPC-managed new/delete existence that exists + * primarily to track the continued existence of the LSDatabase in the child. + * Most of the interesting bits happen via PBackgroundLSSnapshot. + * + * Mutual raw pointers are maintained between LSDatabase and this class that are + * cleared at either (expected) when the child starts the deletion process + * (SendDeleteMeInternal) or unexpected actor death (ActorDestroy). + * + * See `PBackgroundLSDatabase.ipdl` for more information. + * + * + * ## Low-Level Lifecycle ## + * - Created by LSObject::EnsureDatabase if it had to create a database. + * - Deletion begun by LSDatabase's destructor invoking SendDeleteMeInternal + * which will result in the parent sending __delete__ which destroys the + * actor. + */ +class LSDatabaseChild final + : public PBackgroundLSDatabaseChild +{ + friend class mozilla::ipc::BackgroundChildImpl; + friend class LSDatabase; + friend class LSObject; + + LSDatabase* mDatabase; + + NS_DECL_OWNINGTHREAD + +public: + void + AssertIsOnOwningThread() const + { + NS_ASSERT_OWNINGTHREAD(LSDatabaseChild); + } + +private: + // Only created by LSObject. + explicit LSDatabaseChild(LSDatabase* aDatabase); + + // Only destroyed by mozilla::ipc::BackgroundChildImpl. + ~LSDatabaseChild(); + + void + SendDeleteMeInternal(); + + // IPDL methods are only called by IPDL. + void + ActorDestroy(ActorDestroyReason aWhy) override; + + mozilla::ipc::IPCResult + RecvRequestAllowToClose() override; + + PBackgroundLSSnapshotChild* + AllocPBackgroundLSSnapshotChild(const nsString& aDocumentURI, + const bool& aIncreasePeakUsage, + const int64_t& aRequestedSize, + const int64_t& aMinSize, + LSSnapshotInitInfo* aInitInfo) override; + + bool + DeallocPBackgroundLSSnapshotChild(PBackgroundLSSnapshotChild* aActor) + override; +}; + +/** + * Minimal IPC-managed (new/delete) actor that exists to receive and relay + * "storage" events from changes to LocalStorage that take place in other + * processes as their Snapshots are checkpointed to the canonical Datastore in + * the parent process. + * + * See `PBackgroundLSObserver.ipdl` for more info. + */ +class LSObserverChild final + : public PBackgroundLSObserverChild +{ + friend class mozilla::ipc::BackgroundChildImpl; + friend class LSObserver; + friend class LSObject; + + LSObserver* mObserver; + + NS_DECL_OWNINGTHREAD + +public: + void + AssertIsOnOwningThread() const + { + NS_ASSERT_OWNINGTHREAD(LSObserverChild); + } + +private: + // Only created by LSObject. + explicit LSObserverChild(LSObserver* aObserver); + + // Only destroyed by mozilla::ipc::BackgroundChildImpl. + ~LSObserverChild(); + + void + SendDeleteMeInternal(); + + // IPDL methods are only called by IPDL. + void + ActorDestroy(ActorDestroyReason aWhy) override; + + mozilla::ipc::IPCResult + RecvObserve(const PrincipalInfo& aPrinciplaInfo, + const uint32_t& aPrivateBrowsingId, + const nsString& aDocumentURI, + const nsString& aKey, + const nsString& aOldValue, + const nsString& aNewValue) override; +}; + +/** + * Minimal glue IPC-managed (new/delete) actor that is used by LSObject and its + * RequestHelper to perform synchronous requests on top of an asynchronous + * protocol. + * + * Takes an `LSReuestChildCallback` to be invoked when a response is received + * via __delete__. + * + * See `PBackgroundLSRequest.ipdl`, `LSObject`, and `RequestHelper` for more + * info. + */ +class LSRequestChild final + : public PBackgroundLSRequestChild +{ + friend class LSObject; + friend class LocalStorageManager2; + + RefPtr mCallback; + + bool mFinishing; + + NS_DECL_OWNINGTHREAD + +public: + void + AssertIsOnOwningThread() const + { + NS_ASSERT_OWNINGTHREAD(LSReqeustChild); + } + + bool + Finishing() const; + +private: + // Only created by LSObject. + explicit LSRequestChild(LSRequestChildCallback* aCallback); + + // Only destroyed by mozilla::ipc::BackgroundChildImpl. + ~LSRequestChild(); + + // IPDL methods are only called by IPDL. + void + ActorDestroy(ActorDestroyReason aWhy) override; + + mozilla::ipc::IPCResult + Recv__delete__(const LSRequestResponse& aResponse) override; + + mozilla::ipc::IPCResult + RecvReady() override; +}; + +class NS_NO_VTABLE LSRequestChildCallback +{ +public: + NS_INLINE_DECL_PURE_VIRTUAL_REFCOUNTING + + virtual void + OnResponse(const LSRequestResponse& aResponse) = 0; + +protected: + virtual ~LSRequestChildCallback() + { } +}; + +/** + * Minimal glue IPC-managed (new/delete) actor used by `LocalStorageManager2` to + * issue asynchronous requests in an asynchronous fashion. + * + * Takes an `LSSimpleRequestChildCallback` to be invoked when a response is + * received via __delete__. + * + * See `PBackgroundLSSimpleRequest.ipdl` for more info. + */ +class LSSimpleRequestChild final + : public PBackgroundLSSimpleRequestChild +{ + friend class LocalStorageManager2; + + RefPtr mCallback; + + NS_DECL_OWNINGTHREAD + +public: + void + AssertIsOnOwningThread() const + { + NS_ASSERT_OWNINGTHREAD(LSSimpleReqeustChild); + } + +private: + // Only created by LocalStorageManager2. + explicit LSSimpleRequestChild(LSSimpleRequestChildCallback* aCallback); + + // Only destroyed by mozilla::ipc::BackgroundChildImpl. + ~LSSimpleRequestChild(); + + // IPDL methods are only called by IPDL. + void + ActorDestroy(ActorDestroyReason aWhy) override; + + mozilla::ipc::IPCResult + Recv__delete__(const LSSimpleRequestResponse& aResponse) override; +}; + +class NS_NO_VTABLE LSSimpleRequestChildCallback +{ +public: + NS_INLINE_DECL_PURE_VIRTUAL_REFCOUNTING + + virtual void + OnResponse(const LSSimpleRequestResponse& aResponse) = 0; + +protected: + virtual ~LSSimpleRequestChildCallback() + { } +}; + +/** + * Minimal IPC-managed (new/delete) actor that lasts as long as its owning + * LSSnapshot. + * + * Mutual raw pointers are maintained between LSSnapshot and this class that are + * cleared at either (expected) when the child starts the deletion process + * (SendDeleteMeInternal) or unexpected actor death (ActorDestroy). + * + * See `PBackgroundLSSnapshot.ipdl` and `LSSnapshot` for more info. + */ +class LSSnapshotChild final + : public PBackgroundLSSnapshotChild +{ + friend class LSDatabase; + friend class LSSnapshot; + + LSSnapshot* mSnapshot; + + NS_DECL_OWNINGTHREAD + +public: + void + AssertIsOnOwningThread() const + { + NS_ASSERT_OWNINGTHREAD(LSSnapshotChild); + } + +private: + // Only created by LSDatabase. + explicit LSSnapshotChild(LSSnapshot* aSnapshot); + + // Only destroyed by LSDatabaseChild. + ~LSSnapshotChild(); + + void + SendDeleteMeInternal(); + + // IPDL methods are only called by IPDL. + void + ActorDestroy(ActorDestroyReason aWhy) override; + + mozilla::ipc::IPCResult + RecvMarkDirty() override; +}; + +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_localstorage_ActorsChild_h diff --git a/dom/localstorage/ActorsParent.cpp b/dom/localstorage/ActorsParent.cpp new file mode 100644 index 000000000000..042658ca077b --- /dev/null +++ b/dom/localstorage/ActorsParent.cpp @@ -0,0 +1,8539 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/. */ + +#include "ActorsParent.h" + +#include "LocalStorageCommon.h" +#include "LSObject.h" +#include "mozIStorageConnection.h" +#include "mozIStorageFunction.h" +#include "mozIStorageService.h" +#include "mozStorageCID.h" +#include "mozStorageHelper.h" +#include "mozilla/Preferences.h" +#include "mozilla/Unused.h" +#include "mozilla/dom/ContentParent.h" +#include "mozilla/dom/PBackgroundLSDatabaseParent.h" +#include "mozilla/dom/PBackgroundLSObserverParent.h" +#include "mozilla/dom/PBackgroundLSRequestParent.h" +#include "mozilla/dom/PBackgroundLSSharedTypes.h" +#include "mozilla/dom/PBackgroundLSSimpleRequestParent.h" +#include "mozilla/dom/PBackgroundLSSnapshotParent.h" +#include "mozilla/dom/StorageDBUpdater.h" +#include "mozilla/dom/StorageUtils.h" +#include "mozilla/dom/quota/OriginScope.h" +#include "mozilla/dom/quota/QuotaManager.h" +#include "mozilla/dom/quota/QuotaObject.h" +#include "mozilla/dom/quota/UsageInfo.h" +#include "mozilla/ipc/BackgroundParent.h" +#include "mozilla/ipc/PBackgroundParent.h" +#include "mozilla/ipc/PBackgroundSharedTypes.h" +#include "nsClassHashtable.h" +#include "nsDataHashtable.h" +#include "nsInterfaceHashtable.h" +#include "nsISimpleEnumerator.h" +#include "nsNetUtil.h" +#include "nsRefPtrHashtable.h" +#include "ReportInternalError.h" + +#define DISABLE_ASSERTS_FOR_FUZZING 0 + +#if DISABLE_ASSERTS_FOR_FUZZING +#define ASSERT_UNLESS_FUZZING(...) do { } while (0) +#else +#define ASSERT_UNLESS_FUZZING(...) MOZ_ASSERT(false, __VA_ARGS__) +#endif + +#if defined(MOZ_WIDGET_ANDROID) +#define LS_MOBILE +#endif + +namespace mozilla { +namespace dom { + +using namespace mozilla::dom::quota; +using namespace mozilla::dom::StorageUtils; +using namespace mozilla::ipc; + +namespace { + +struct ArchivedOriginInfo; +class ArchivedOriginScope; +class Connection; +class ConnectionThread; +class Database; +class PrepareDatastoreOp; +class PreparedDatastore; +class QuotaClient; +class Snapshot; + +typedef nsClassHashtable + ArchivedOriginHashtable; + +/******************************************************************************* + * Constants + ******************************************************************************/ + +// Major schema version. Bump for almost everything. +const uint32_t kMajorSchemaVersion = 1; + +// Minor schema version. Should almost always be 0 (maybe bump on release +// branches if we have to). +const uint32_t kMinorSchemaVersion = 0; + +// The schema version we store in the SQLite database is a (signed) 32-bit +// integer. The major version is left-shifted 4 bits so the max value is +// 0xFFFFFFF. The minor version occupies the lower 4 bits and its max is 0xF. +static_assert(kMajorSchemaVersion <= 0xFFFFFFF, + "Major version needs to fit in 28 bits."); +static_assert(kMinorSchemaVersion <= 0xF, + "Minor version needs to fit in 4 bits."); + +const int32_t kSQLiteSchemaVersion = + int32_t((kMajorSchemaVersion << 4) + kMinorSchemaVersion); + +// Changing the value here will override the page size of new databases only. +// A journal mode change and VACUUM are needed to change existing databases, so +// the best way to do that is to use the schema version upgrade mechanism. +const uint32_t kSQLitePageSizeOverride = +#ifdef LS_MOBILE + 512; +#else + 1024; +#endif + +static_assert(kSQLitePageSizeOverride == /* mozStorage default */ 0 || + (kSQLitePageSizeOverride % 2 == 0 && + kSQLitePageSizeOverride >= 512 && + kSQLitePageSizeOverride <= 65536), + "Must be 0 (disabled) or a power of 2 between 512 and 65536!"); + +// Set to some multiple of the page size to grow the database in larger chunks. +const uint32_t kSQLiteGrowthIncrement = kSQLitePageSizeOverride * 2; + +static_assert(kSQLiteGrowthIncrement >= 0 && + kSQLiteGrowthIncrement % kSQLitePageSizeOverride == 0 && + kSQLiteGrowthIncrement < uint32_t(INT32_MAX), + "Must be 0 (disabled) or a positive multiple of the page size!"); + +/** + * The database name for LocalStorage data in a per-origin directory. + */ +#define DATA_FILE_NAME "data.sqlite" +/** + * The journal corresponding to DATA_FILE_NAME. (We don't use WAL mode.) + */ +#define JOURNAL_FILE_NAME "data.sqlite-journal" + +/** + * How long between the first moment we know we have data to be written on a + * `Connection` and when we should actually perform the write. This helps + * limit disk churn under silly usage patterns and is historically consistent + * with the previous, legacy implementation. + * + * Note that flushing happens downstream of Snapshot checkpointing and its + * batch mechanism which helps avoid wasteful IPC in the case of silly content + * code. + */ +const uint32_t kFlushTimeoutMs = 5000; + +const char kPrivateBrowsingObserverTopic[] = "last-pb-context-exited"; + +const uint32_t kDefaultOriginLimitKB = 5 * 1024; +const uint32_t kDefaultShadowWrites = true; +const uint32_t kDefaultSnapshotPrefill = 4096; +/** + * LocalStorage data limit as determined by summing up the lengths of all string + * keys and values. This is consistent with the legacy implementation and other + * browser engines. This value should really only ever change in unit testing + * where being able to lower it makes it easier for us to test certain edge + * cases. + */ +const char kDefaultQuotaPref[] = "dom.storage.default_quota"; +/** + * Should all mutations also be reflected in the "shadow" database, which is + * the legacy webappsstore.sqlite database. When this is enabled, users can + * downgrade their version of Firefox and/or otherwise fall back to the legacy + * implementation without loss of data. (Older versions of Firefox will + * recognize the presence of ls-archive.sqlite and purge it and the other + * LocalStorage directories so privacy is maintained.) + */ +const char kShadowWritesPref[] = "dom.storage.shadow_writes"; +/** + * Byte budget for sending data down to the LSSnapshot instance when it is first + * created. If there is less data than this (measured by tallying the string + * length of the keys and values), all data is sent, otherwise partial data is + * sent. See `Snapshot`. + */ +const char kSnapshotPrefillPref[] = "dom.storage.snapshot_prefill"; + +/** + * The amount of time a PreparedDatastore instance should stick around after a + * preload is triggered in order to give time for the page to use LocalStorage + * without triggering worst-case synchronous jank. + */ +const uint32_t kPreparedDatastoreTimeoutMs = 20000; + +/** + * Cold storage for LocalStorage data extracted from webappsstore.sqlite at + * LSNG first-run that has not yet been migrated to its own per-origin directory + * by use. + * + * In other words, at first run, LSNG copies the contents of webappsstore.sqlite + * into this database. As requests are made for that LocalStorage data, the + * contents are removed from this database and placed into per-origin QM + * storage. So the contents of this database are always old, unused + * LocalStorage data that we can potentially get rid of at some point in the + * future. + */ +#define LS_ARCHIVE_FILE_NAME "ls-archive.sqlite" +/** + * The legacy LocalStorage database. Its contents are maintained as our + * "shadow" database so that LSNG can be disabled without loss of user data. + */ +#define WEB_APPS_STORE_FILE_NAME "webappsstore.sqlite" + +// Shadow database Write Ahead Log's maximum size is 512KB +const uint32_t kShadowMaxWALSize = 512 * 1024; + +const uint32_t kShadowJournalSizeLimit = kShadowMaxWALSize * 3; + +bool +IsOnConnectionThread(); + +void +AssertIsOnConnectionThread(); + +/******************************************************************************* + * SQLite functions + ******************************************************************************/ + +#if 0 +int32_t +MakeSchemaVersion(uint32_t aMajorSchemaVersion, + uint32_t aMinorSchemaVersion) +{ + return int32_t((aMajorSchemaVersion << 4) + aMinorSchemaVersion); +} +#endif + +nsCString +GetArchivedOriginHashKey(const nsACString& aOriginSuffix, + const nsACString& aOriginNoSuffix) +{ + return aOriginSuffix + NS_LITERAL_CSTRING(":") + aOriginNoSuffix; +} + +nsresult +CreateTables(mozIStorageConnection* aConnection) +{ + AssertIsOnIOThread(); + MOZ_ASSERT(aConnection); + + // Table `database` + nsresult rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "CREATE TABLE database" + "( origin TEXT NOT NULL" + ", last_vacuum_time INTEGER NOT NULL DEFAULT 0" + ", last_analyze_time INTEGER NOT NULL DEFAULT 0" + ", last_vacuum_size INTEGER NOT NULL DEFAULT 0" + ");" + )); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + // Table `data` + rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "CREATE TABLE data" + "( key TEXT PRIMARY KEY" + ", value TEXT NOT NULL" + ", compressed INTEGER NOT NULL DEFAULT 0" + ", lastAccessTime INTEGER NOT NULL DEFAULT 0" + ");" + )); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection->SetSchemaVersion(kSQLiteSchemaVersion); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + return NS_OK; +} + +#if 0 +nsresult +UpgradeSchemaFrom1_0To2_0(mozIStorageConnection* aConnection) +{ + AssertIsOnIOThread(); + MOZ_ASSERT(aConnection); + + nsresult rv = aConnection->SetSchemaVersion(MakeSchemaVersion(2, 0)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + return NS_OK; +} +#endif + +nsresult +SetDefaultPragmas(mozIStorageConnection* aConnection) +{ + MOZ_ASSERT(!NS_IsMainThread()); + MOZ_ASSERT(aConnection); + + nsresult rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "PRAGMA synchronous = FULL;" + )); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + +#ifndef LS_MOBILE + if (kSQLiteGrowthIncrement) { + // This is just an optimization so ignore the failure if the disk is + // currently too full. + rv = aConnection->SetGrowthIncrement(kSQLiteGrowthIncrement, + EmptyCString()); + if (rv != NS_ERROR_FILE_TOO_BIG && NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } +#endif // LS_MOBILE + + return NS_OK; +} + +nsresult +CreateStorageConnection(nsIFile* aDBFile, + const nsACString& aOrigin, + mozIStorageConnection** aConnection) +{ + AssertIsOnIOThread(); + MOZ_ASSERT(aDBFile); + MOZ_ASSERT(aConnection); + + nsresult rv; + + nsCOMPtr ss = + do_GetService(MOZ_STORAGE_SERVICE_CONTRACTID, &rv); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + nsCOMPtr connection; + rv = ss->OpenDatabase(aDBFile, getter_AddRefs(connection)); + if (rv == NS_ERROR_FILE_CORRUPTED) { + // Nuke the database file. + rv = aDBFile->Remove(false); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = ss->OpenDatabase(aDBFile, getter_AddRefs(connection)); + } + + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = SetDefaultPragmas(connection); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + // Check to make sure that the database schema is correct. + int32_t schemaVersion; + rv = connection->GetSchemaVersion(&schemaVersion); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (schemaVersion > kSQLiteSchemaVersion) { + LS_WARNING("Unable to open LocalStorage database, schema is too high!"); + return NS_ERROR_FAILURE; + } + + if (schemaVersion != kSQLiteSchemaVersion) { + const bool newDatabase = !schemaVersion; + + if (newDatabase) { + // Set the page size first. + if (kSQLitePageSizeOverride) { + rv = connection->ExecuteSimpleSQL( + nsPrintfCString("PRAGMA page_size = %" PRIu32 ";", kSQLitePageSizeOverride) + ); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } + + // We have to set the auto_vacuum mode before opening a transaction. + rv = connection->ExecuteSimpleSQL( +#ifdef LS_MOBILE + // Turn on full auto_vacuum mode to reclaim disk space on mobile + // devices (at the cost of some COMMIT speed). + NS_LITERAL_CSTRING("PRAGMA auto_vacuum = FULL;") +#else + // Turn on incremental auto_vacuum mode on desktop builds. + NS_LITERAL_CSTRING("PRAGMA auto_vacuum = INCREMENTAL;") +#endif + ); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } + + mozStorageTransaction transaction(connection, false, + mozIStorageConnection::TRANSACTION_IMMEDIATE); + + if (newDatabase) { + rv = CreateTables(connection); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + MOZ_ASSERT(NS_SUCCEEDED(connection->GetSchemaVersion(&schemaVersion))); + MOZ_ASSERT(schemaVersion == kSQLiteSchemaVersion); + + nsCOMPtr stmt; + nsresult rv = connection->CreateStatement(NS_LITERAL_CSTRING( + "INSERT INTO database (origin) " + "VALUES (:origin)" + ), getter_AddRefs(stmt)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = stmt->BindUTF8StringByName(NS_LITERAL_CSTRING("origin"), aOrigin); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = stmt->Execute(); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } else { + // This logic needs to change next time we change the schema! + static_assert(kSQLiteSchemaVersion == int32_t((1 << 4) + 0), + "Upgrade function needed due to schema version increase."); + + while (schemaVersion != kSQLiteSchemaVersion) { +#if 0 + if (schemaVersion == MakeSchemaVersion(1, 0)) { + rv = UpgradeSchemaFrom1_0To2_0(connection); + } else { +#endif + LS_WARNING("Unable to open LocalStorage database, no upgrade path is " + "available!"); + return NS_ERROR_FAILURE; +#if 0 + } +#endif + + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = connection->GetSchemaVersion(&schemaVersion); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } + + MOZ_ASSERT(schemaVersion == kSQLiteSchemaVersion); + } + + rv = transaction.Commit(); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (newDatabase) { + // Windows caches the file size, let's force it to stat the file again. + bool dummy; + rv = aDBFile->Exists(&dummy); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + int64_t fileSize; + rv = aDBFile->GetFileSize(&fileSize); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + MOZ_ASSERT(fileSize > 0); + + PRTime vacuumTime = PR_Now(); + MOZ_ASSERT(vacuumTime); + + nsCOMPtr vacuumTimeStmt; + rv = connection->CreateStatement(NS_LITERAL_CSTRING( + "UPDATE database " + "SET last_vacuum_time = :time" + ", last_vacuum_size = :size;" + ), getter_AddRefs(vacuumTimeStmt)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = vacuumTimeStmt->BindInt64ByName(NS_LITERAL_CSTRING("time"), + vacuumTime); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = vacuumTimeStmt->BindInt64ByName(NS_LITERAL_CSTRING("size"), + fileSize); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = vacuumTimeStmt->Execute(); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } + } + + connection.forget(aConnection); + return NS_OK; +} + +nsresult +GetStorageConnection(const nsAString& aDatabaseFilePath, + mozIStorageConnection** aConnection) +{ + AssertIsOnConnectionThread(); + MOZ_ASSERT(!aDatabaseFilePath.IsEmpty()); + MOZ_ASSERT(StringEndsWith(aDatabaseFilePath, NS_LITERAL_STRING(".sqlite"))); + MOZ_ASSERT(aConnection); + + nsCOMPtr databaseFile; + nsresult rv = NS_NewLocalFile(aDatabaseFilePath, false, + getter_AddRefs(databaseFile)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + bool exists; + rv = databaseFile->Exists(&exists); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (NS_WARN_IF(!exists)) { + return NS_ERROR_FAILURE; + } + + nsCOMPtr ss = + do_GetService(MOZ_STORAGE_SERVICE_CONTRACTID, &rv); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + nsCOMPtr connection; + rv = ss->OpenDatabase(databaseFile, getter_AddRefs(connection)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = SetDefaultPragmas(connection); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + connection.forget(aConnection); + return NS_OK; +} + +nsresult +GetArchiveFile(const nsAString& aStoragePath, + nsIFile** aArchiveFile) +{ + AssertIsOnIOThread(); + MOZ_ASSERT(!aStoragePath.IsEmpty()); + MOZ_ASSERT(aArchiveFile); + + nsCOMPtr archiveFile; + nsresult rv = NS_NewLocalFile(aStoragePath, + false, + getter_AddRefs(archiveFile)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = archiveFile->Append(NS_LITERAL_STRING(LS_ARCHIVE_FILE_NAME)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + archiveFile.forget(aArchiveFile); + return NS_OK; +} + +nsresult +CreateArchiveStorageConnection(const nsAString& aStoragePath, + mozIStorageConnection** aConnection) +{ + AssertIsOnIOThread(); + MOZ_ASSERT(!aStoragePath.IsEmpty()); + MOZ_ASSERT(aConnection); + + nsCOMPtr archiveFile; + nsresult rv = GetArchiveFile(aStoragePath, getter_AddRefs(archiveFile)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + // QuotaManager ensures this file always exists. + DebugOnly exists; + MOZ_ASSERT(NS_SUCCEEDED(archiveFile->Exists(&exists))); + MOZ_ASSERT(exists); + + bool isDirectory; + rv = archiveFile->IsDirectory(&isDirectory); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (isDirectory) { + LS_WARNING("ls-archive is not a file!"); + *aConnection = nullptr; + return NS_OK; + } + + nsCOMPtr ss = + do_GetService(MOZ_STORAGE_SERVICE_CONTRACTID, &rv); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + nsCOMPtr connection; + rv = ss->OpenUnsharedDatabase(archiveFile, getter_AddRefs(connection)); + if (rv == NS_ERROR_FILE_CORRUPTED) { + // Don't throw an error, leave a corrupted ls-archive database as it is. + *aConnection = nullptr; + return NS_OK; + } + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = StorageDBUpdater::Update(connection); + if (NS_FAILED(rv)) { + // Don't throw an error, leave a non-updateable ls-archive database as + // it is. + *aConnection = nullptr; + return NS_OK; + } + + connection.forget(aConnection); + return NS_OK; +} + +nsresult +AttachArchiveDatabase(const nsAString& aStoragePath, + mozIStorageConnection* aConnection) +{ + AssertIsOnIOThread(); + MOZ_ASSERT(!aStoragePath.IsEmpty()); + MOZ_ASSERT(aConnection); + nsCOMPtr archiveFile; + + nsresult rv = GetArchiveFile(aStoragePath, getter_AddRefs(archiveFile)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + +#ifdef DEBUG + bool exists; + rv = archiveFile->Exists(&exists); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + MOZ_ASSERT(exists); +#endif + + nsString path; + rv = archiveFile->GetPath(path); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + nsCOMPtr stmt; + rv = aConnection->CreateStatement( + NS_LITERAL_CSTRING("ATTACH DATABASE :path AS archive;"), + getter_AddRefs(stmt)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = stmt->BindStringByName(NS_LITERAL_CSTRING("path"), path); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = stmt->Execute(); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + return NS_OK; +} + +nsresult +DetachArchiveDatabase(mozIStorageConnection* aConnection) +{ + AssertIsOnIOThread(); + MOZ_ASSERT(aConnection); + + nsresult rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "DETACH DATABASE archive" + )); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + return NS_OK; +} + +nsresult +GetShadowFile(const nsAString& aBasePath, + nsIFile** aArchiveFile) +{ + MOZ_ASSERT(IsOnIOThread() || IsOnConnectionThread()); + MOZ_ASSERT(!aBasePath.IsEmpty()); + MOZ_ASSERT(aArchiveFile); + + nsCOMPtr archiveFile; + nsresult rv = NS_NewLocalFile(aBasePath, + false, + getter_AddRefs(archiveFile)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = archiveFile->Append(NS_LITERAL_STRING(WEB_APPS_STORE_FILE_NAME)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + archiveFile.forget(aArchiveFile); + return NS_OK; +} + +nsresult +SetShadowJournalMode(mozIStorageConnection* aConnection) +{ + AssertIsOnIOThread(); + MOZ_ASSERT(aConnection); + + // Try enabling WAL mode. This can fail in various circumstances so we have to + // check the results here. + NS_NAMED_LITERAL_CSTRING(journalModeQueryStart, "PRAGMA journal_mode = "); + NS_NAMED_LITERAL_CSTRING(journalModeWAL, "wal"); + + nsCOMPtr stmt; + nsresult rv = + aConnection->CreateStatement(journalModeQueryStart + journalModeWAL, + getter_AddRefs(stmt)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + bool hasResult; + rv = stmt->ExecuteStep(&hasResult); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + MOZ_ASSERT(hasResult); + + nsCString journalMode; + rv = stmt->GetUTF8String(0, journalMode); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (journalMode.Equals(journalModeWAL)) { + // WAL mode successfully enabled. Set limits on its size here. + + // Set the threshold for auto-checkpointing the WAL. We don't want giant + // logs slowing down us. + rv = aConnection->CreateStatement(NS_LITERAL_CSTRING( + "PRAGMA page_size;" + ), getter_AddRefs(stmt)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + bool hasResult; + rv = stmt->ExecuteStep(&hasResult); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + MOZ_ASSERT(hasResult); + + int32_t pageSize; + rv = stmt->GetInt32(0, &pageSize); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + MOZ_ASSERT(pageSize >= 512 && pageSize <= 65536); + + nsAutoCString pageCount; + pageCount.AppendInt(static_cast(kShadowMaxWALSize / pageSize)); + + rv = aConnection->ExecuteSimpleSQL( + NS_LITERAL_CSTRING("PRAGMA wal_autocheckpoint = ") + pageCount); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + // Set the maximum WAL log size to reduce footprint on mobile (large empty + // WAL files will be truncated) + nsAutoCString sizeLimit; + sizeLimit.AppendInt(kShadowJournalSizeLimit); + + rv = aConnection->ExecuteSimpleSQL( + NS_LITERAL_CSTRING("PRAGMA journal_size_limit = ") + sizeLimit); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } else { + rv = aConnection->ExecuteSimpleSQL(journalModeQueryStart + + NS_LITERAL_CSTRING("truncate")); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } + + return NS_OK; +} + +nsresult +CreateShadowStorageConnection(const nsAString& aBasePath, + mozIStorageConnection** aConnection) +{ + AssertIsOnIOThread(); + MOZ_ASSERT(!aBasePath.IsEmpty()); + MOZ_ASSERT(aConnection); + + nsCOMPtr shadowFile; + nsresult rv = GetShadowFile(aBasePath, getter_AddRefs(shadowFile)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + nsCOMPtr ss = + do_GetService(MOZ_STORAGE_SERVICE_CONTRACTID, &rv); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + nsCOMPtr connection; + rv = ss->OpenUnsharedDatabase(shadowFile, getter_AddRefs(connection)); + if (rv == NS_ERROR_FILE_CORRUPTED) { + rv = shadowFile->Remove(false); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = ss->OpenUnsharedDatabase(shadowFile, getter_AddRefs(connection)); + } + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = SetShadowJournalMode(connection); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = StorageDBUpdater::Update(connection); + if (NS_FAILED(rv)) { + rv = connection->Close(); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = shadowFile->Remove(false); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = ss->OpenUnsharedDatabase(shadowFile, getter_AddRefs(connection)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = SetShadowJournalMode(connection); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = StorageDBUpdater::Update(connection); + } + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + connection.forget(aConnection); + return NS_OK; +} + +nsresult +GetShadowStorageConnection(const nsAString& aBasePath, + mozIStorageConnection** aConnection) +{ + AssertIsOnIOThread(); + MOZ_ASSERT(!aBasePath.IsEmpty()); + MOZ_ASSERT(aConnection); + + nsCOMPtr shadowFile; + nsresult rv = GetShadowFile(aBasePath, getter_AddRefs(shadowFile)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + bool exists; + rv = shadowFile->Exists(&exists); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (NS_WARN_IF(!exists)) { + return NS_ERROR_FAILURE; + } + + nsCOMPtr ss = + do_GetService(MOZ_STORAGE_SERVICE_CONTRACTID, &rv); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + nsCOMPtr connection; + rv = ss->OpenUnsharedDatabase(shadowFile, getter_AddRefs(connection)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + connection.forget(aConnection); + return NS_OK; +} + +nsresult +AttachShadowDatabase(const nsAString& aBasePath, + mozIStorageConnection* aConnection) +{ + AssertIsOnConnectionThread(); + MOZ_ASSERT(!aBasePath.IsEmpty()); + MOZ_ASSERT(aConnection); + + nsCOMPtr shadowFile; + nsresult rv = GetShadowFile(aBasePath, getter_AddRefs(shadowFile)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + +#ifdef DEBUG + bool exists; + rv = shadowFile->Exists(&exists); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + MOZ_ASSERT(exists); +#endif + + nsString path; + rv = shadowFile->GetPath(path); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + nsCOMPtr stmt; + rv = aConnection->CreateStatement( + NS_LITERAL_CSTRING("ATTACH DATABASE :path AS shadow;"), + getter_AddRefs(stmt)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = stmt->BindStringByName(NS_LITERAL_CSTRING("path"), path); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = stmt->Execute(); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + return NS_OK; +} + +nsresult +DetachShadowDatabase(mozIStorageConnection* aConnection) +{ + AssertIsOnConnectionThread(); + MOZ_ASSERT(aConnection); + + nsresult rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "DETACH DATABASE shadow" + )); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + return NS_OK; +} + +/******************************************************************************* + * Non-actor class declarations + ******************************************************************************/ + +/** + * Coalescing manipulation queue used by `Connection` and `DataStore`. Used by + * `Connection` to buffer and coalesce manipulations applied to the Datastore + * in batches by Snapshot Checkpointing until flushed to disk. Used by + * `Datastore` to update `DataStore::mOrderedItems` efficiently/for code + * simplification. (DataStore does not actually depend on the coalescing, as + * mutations are applied atomically when a Snapshot Checkpoints, and with + * `Datastore::mValues` being updated at the same time the mutations are applied + * to Datastore's mWriteOptimizer.) + */ +class WriteOptimizer final +{ + class WriteInfo; + class AddItemInfo; + class UpdateItemInfo; + class RemoveItemInfo; + class ClearInfo; + + nsAutoPtr mClearInfo; + nsClassHashtable mWriteInfos; + +public: + WriteOptimizer() + { } + + WriteOptimizer(WriteOptimizer&& aWriteOptimizer) + : mClearInfo(std::move(aWriteOptimizer.mClearInfo)) + { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(&aWriteOptimizer != this); + + mWriteInfos.SwapElements(aWriteOptimizer.mWriteInfos); + } + + void + AddItem(const nsString& aKey, + const nsString& aValue); + + void + UpdateItem(const nsString& aKey, + const nsString& aValue); + + void + RemoveItem(const nsString& aKey); + + void + Clear(); + + bool + HasWrites() const + { + AssertIsOnBackgroundThread(); + + return mClearInfo || !mWriteInfos.IsEmpty(); + } + + void + ApplyWrites(nsTArray& aOrderedItems); + + nsresult + PerformWrites(Connection* aConnection, bool aShadowWrites); +}; + +/** + * Base class for specific mutations. Each subclass knows how to `Perform` the + * manipulation against a `Connection` and the "shadow" database (legacy + * webappsstore.sqlite database that exists so LSNG can be disabled/safely + * downgraded from.) + */ +class WriteOptimizer::WriteInfo +{ +public: + enum Type { + AddItem = 0, + UpdateItem, + RemoveItem, + Clear + }; + + virtual Type + GetType() = 0; + + virtual nsresult + Perform(Connection* aConnection, bool aShadowWrites) = 0; + + virtual ~WriteInfo() = default; +}; + +/** + * SetItem mutation where the key did not previously exist. + */ +class WriteOptimizer::AddItemInfo + : public WriteInfo +{ + nsString mKey; + nsString mValue; + +public: + AddItemInfo(const nsAString& aKey, + const nsAString& aValue) + : mKey(aKey) + , mValue(aValue) + { } + + const nsAString& + GetKey() const + { + return mKey; + } + + const nsAString& + GetValue() const + { + return mValue; + } + +private: + Type + GetType() override + { + return AddItem; + } + + nsresult + Perform(Connection* aConnection, bool aShadowWrites) override; +}; + +/** + * SetItem mutation where the key already existed. + */ +class WriteOptimizer::UpdateItemInfo final + : public AddItemInfo +{ +public: + UpdateItemInfo(const nsAString& aKey, + const nsAString& aValue) + : AddItemInfo(aKey, aValue) + { } + +private: + Type + GetType() override + { + return UpdateItem; + } +}; + +class WriteOptimizer::RemoveItemInfo final + : public WriteInfo +{ + nsString mKey; + +public: + explicit RemoveItemInfo(const nsAString& aKey) + : mKey(aKey) + { } + + const nsAString& + GetKey() const + { + return mKey; + } + +private: + Type + GetType() override + { + return RemoveItem; + } + + nsresult + Perform(Connection* aConnection, bool aShadowWrites) override; +}; + +/** + * Clear mutation. + */ +class WriteOptimizer::ClearInfo final + : public WriteInfo +{ +public: + ClearInfo() + { } + +private: + Type + GetType() override + { + return Clear; + } + + nsresult + Perform(Connection* aConnection, bool aShadowWrites) override; +}; + +class DatastoreOperationBase + : public Runnable +{ + nsCOMPtr mOwningEventTarget; + nsresult mResultCode; + Atomic mMayProceedOnNonOwningThread; + bool mMayProceed; + +public: + nsIEventTarget* + OwningEventTarget() const + { + MOZ_ASSERT(mOwningEventTarget); + + return mOwningEventTarget; + } + + bool + IsOnOwningThread() const + { + MOZ_ASSERT(mOwningEventTarget); + + bool current; + return + NS_SUCCEEDED(mOwningEventTarget->IsOnCurrentThread(¤t)) && current; + } + + void + AssertIsOnOwningThread() const + { + MOZ_ASSERT(IsOnBackgroundThread()); + MOZ_ASSERT(IsOnOwningThread()); + } + + nsresult + ResultCode() const + { + return mResultCode; + } + + void + SetFailureCode(nsresult aErrorCode) + { + MOZ_ASSERT(NS_SUCCEEDED(mResultCode)); + MOZ_ASSERT(NS_FAILED(aErrorCode)); + + mResultCode = aErrorCode; + } + + void + MaybeSetFailureCode(nsresult aErrorCode) + { + MOZ_ASSERT(NS_FAILED(aErrorCode)); + + if (NS_SUCCEEDED(mResultCode)) { + mResultCode = aErrorCode; + } + } + + void + NoteComplete() + { + AssertIsOnOwningThread(); + + mMayProceed = false; + mMayProceedOnNonOwningThread = false; + } + + bool + MayProceed() const + { + AssertIsOnOwningThread(); + + return mMayProceed; + } + + // May be called on any thread, but you should call MayProceed() if you know + // you're on the background thread because it is slightly faster. + bool + MayProceedOnNonOwningThread() const + { + return mMayProceedOnNonOwningThread; + } + +protected: + DatastoreOperationBase() + : Runnable("dom::DatastoreOperationBase") + , mOwningEventTarget(GetCurrentThreadEventTarget()) + , mResultCode(NS_OK) + , mMayProceedOnNonOwningThread(true) + , mMayProceed(true) + { } + + ~DatastoreOperationBase() override + { + MOZ_ASSERT(!mMayProceed); + } +}; + +class ConnectionDatastoreOperationBase + : public DatastoreOperationBase +{ +protected: + RefPtr mConnection; + +public: + // This callback will be called on the background thread before releasing the + // final reference to this request object. Subclasses may perform any + // additional cleanup here but must always call the base class implementation. + virtual void + Cleanup(); + +protected: + ConnectionDatastoreOperationBase(Connection* aConnection); + + ~ConnectionDatastoreOperationBase(); + + // Must be overridden in subclasses. Called on the target thread to allow the + // subclass to perform necessary datastore operations. A successful return + // value will trigger an OnSuccess callback on the background thread while + // while a failure value will trigger an OnFailure callback. + virtual nsresult + DoDatastoreWork() = 0; + + // Methods that subclasses may implement. + virtual void + OnSuccess(); + + virtual void + OnFailure(nsresult aResultCode); + +private: + void + RunOnConnectionThread(); + + void + RunOnOwningThread(); + + // Not to be overridden by subclasses. + NS_DECL_NSIRUNNABLE +}; + +class Connection final +{ + friend class ConnectionThread; + +public: + class CachedStatement; + +private: + class FlushOp; + class CloseOp; + + RefPtr mConnectionThread; + nsCOMPtr mFlushTimer; + nsCOMPtr mStorageConnection; + nsAutoPtr mArchivedOriginScope; + nsInterfaceHashtable + mCachedStatements; + WriteOptimizer mWriteOptimizer; + const nsCString mOrigin; + const nsString mFilePath; + bool mFlushScheduled; +#ifdef DEBUG + bool mInUpdateBatch; +#endif + +public: + NS_INLINE_DECL_REFCOUNTING(mozilla::dom::Connection) + + void + AssertIsOnOwningThread() const + { + NS_ASSERT_OWNINGTHREAD(Connection); + } + + ArchivedOriginScope* + GetArchivedOriginScope() const + { + return mArchivedOriginScope; + } + + ////////////////////////////////////////////////////////////////////////////// + // Methods which can only be called on the owning thread. + + // This method is used to asynchronously execute a connection datastore + // operation on the connection thread. + void + Dispatch(ConnectionDatastoreOperationBase* aOp); + + // This method is used to asynchronously close the storage connection on the + // connection thread. + void + Close(nsIRunnable* aCallback); + + void + AddItem(const nsString& aKey, + const nsString& aValue); + + void + UpdateItem(const nsString& aKey, + const nsString& aValue); + + void + RemoveItem(const nsString& aKey); + + void + Clear(); + + void + BeginUpdateBatch(); + + void + EndUpdateBatch(); + + ////////////////////////////////////////////////////////////////////////////// + // Methods which can only be called on the connection thread. + + nsresult + EnsureStorageConnection(); + + mozIStorageConnection* + StorageConnection() const + { + AssertIsOnConnectionThread(); + MOZ_ASSERT(mStorageConnection); + + return mStorageConnection; + } + + void + CloseStorageConnection(); + + nsresult + GetCachedStatement(const nsACString& aQuery, + CachedStatement* aCachedStatement); + +private: + // Only created by ConnectionThread. + Connection(ConnectionThread* aConnectionThread, + const nsACString& aOrigin, + const nsAString& aFilePath, + nsAutoPtr&& aArchivedOriginScope); + + ~Connection(); + + void + ScheduleFlush(); + + void + Flush(); + + static void + FlushTimerCallback(nsITimer* aTimer, void* aClosure); +}; + +class Connection::CachedStatement final +{ + friend class Connection; + + nsCOMPtr mStatement; + Maybe mScoper; + +public: + CachedStatement(); + ~CachedStatement(); + + operator mozIStorageStatement*() const; + + mozIStorageStatement* + operator->() const MOZ_NO_ADDREF_RELEASE_ON_RETURN; + +private: + // Only called by Connection. + void + Assign(Connection* aConnection, + already_AddRefed aStatement); + + // No funny business allowed. + CachedStatement(const CachedStatement&) = delete; + CachedStatement& operator=(const CachedStatement&) = delete; +}; + +class Connection::FlushOp final + : public ConnectionDatastoreOperationBase +{ + RefPtr mQuotaClient; + WriteOptimizer mWriteOptimizer; + bool mShadowWrites; + +public: + FlushOp(Connection* aConnection, + WriteOptimizer&& aWriteOptimizer); + +private: + nsresult + DoDatastoreWork() override; +}; + +class Connection::CloseOp final + : public ConnectionDatastoreOperationBase +{ + nsCOMPtr mCallback; + +public: + CloseOp(Connection* aConnection, + nsIRunnable* aCallback) + : ConnectionDatastoreOperationBase(aConnection) + , mCallback(aCallback) + { } + +private: + nsresult + DoDatastoreWork() override; + + void + Cleanup() override; +}; + +class ConnectionThread final +{ + friend class Connection; + + nsCOMPtr mThread; + nsRefPtrHashtable mConnections; + +public: + ConnectionThread(); + + void + AssertIsOnOwningThread() const + { + NS_ASSERT_OWNINGTHREAD(ConnectionThread); + } + + bool + IsOnConnectionThread(); + + void + AssertIsOnConnectionThread(); + + already_AddRefed + CreateConnection(const nsACString& aOrigin, + const nsAString& aFilePath, + nsAutoPtr&& aArchivedOriginScope); + + void + Shutdown(); + + NS_INLINE_DECL_REFCOUNTING(ConnectionThread) + +private: + ~ConnectionThread(); +}; + +/** + * Canonical state of Storage for an origin, containing all keys and their + * values in the parent process. Specifically, this is the state that will + * be handed out to freshly created Snapshots and that will be persisted to disk + * when the Connection's flush completes. State is mutated in batches as + * Snapshot instances Checkpoint their mutations locally accumulated in the + * child LSSnapshots. + */ +class Datastore final +{ + RefPtr mDirectoryLock; + RefPtr mConnection; + RefPtr mQuotaObject; + nsCOMPtr mCompleteCallback; + /** + * PrepareDatastoreOps register themselves with the Datastore at + * and unregister in PrepareDatastoreOp::Cleanup. + */ + nsTHashtable> mPrepareDatastoreOps; + /** + * PreparedDatastore instances register themselves with their associated + * Datastore at construction time and unregister at destruction time. They + * hang around for kPreparedDatastoreTimeoutMs in order to keep the Datastore + * from closing itself via MaybeClose(), thereby giving the document enough + * time to load and access LocalStorage. + */ + nsTHashtable> mPreparedDatastores; + /** + * A database is live (and in this hashtable) if it has a live LSDatabase + * actor. There is at most one Database per origin per content process. Each + * Database corresponds to an LSDatabase in its associated content process. + */ + nsTHashtable> mDatabases; + /** + * A database is active if it has a non-null `mSnapshot`. As long as there + * are any active databases final deltas can't be calculated and + * `UpdateUsage()` can't be invoked. + */ + nsTHashtable> mActiveDatabases; + /** + * Non-authoritative hashtable representation of mOrderedItems for efficient + * lookup. + */ + nsDataHashtable mValues; + /** + * The authoritative ordered state of the Datastore; mValue also exists as an + * unordered hashtable for efficient lookup. + */ + nsTArray mOrderedItems; + nsTArray mPendingUsageDeltas; + WriteOptimizer mWriteOptimizer; + const nsCString mOrigin; + const uint32_t mPrivateBrowsingId; + int64_t mUsage; + int64_t mUpdateBatchUsage; + int64_t mSizeOfKeys; + int64_t mSizeOfItems; + bool mClosed; +#ifdef DEBUG + bool mInUpdateBatch; +#endif + +public: + // Created by PrepareDatastoreOp. + Datastore(const nsACString& aOrigin, + uint32_t aPrivateBrowsingId, + int64_t aUsage, + int64_t aSizeOfKeys, + int64_t aSizeOfItems, + already_AddRefed&& aDirectoryLock, + already_AddRefed&& aConnection, + already_AddRefed&& aQuotaObject, + nsDataHashtable& aValues, + nsTArray& aOrderedItems); + + const nsCString& + Origin() const + { + return mOrigin; + } + + uint32_t + PrivateBrowsingId() const + { + return mPrivateBrowsingId; + } + + bool + IsPersistent() const + { + // Private-browsing is forbidden from touching disk, but + // StorageAccess::eSessionScoped is allowed to touch disk because + // QuotaManager's storage for such origins is wiped at shutdown. + return mPrivateBrowsingId == 0; + } + + void + Close(); + + bool + IsClosed() const + { + AssertIsOnBackgroundThread(); + + return mClosed; + } + + void + WaitForConnectionToComplete(nsIRunnable* aCallback); + + void + NoteLivePrepareDatastoreOp(PrepareDatastoreOp* aPrepareDatastoreOp); + + void + NoteFinishedPrepareDatastoreOp(PrepareDatastoreOp* aPrepareDatastoreOp); + + void + NoteLivePreparedDatastore(PreparedDatastore* aPreparedDatastore); + + void + NoteFinishedPreparedDatastore(PreparedDatastore* aPreparedDatastore); + +#ifdef DEBUG + bool + HasLivePreparedDatastores() const; +#endif + + void + NoteLiveDatabase(Database* aDatabase); + + void + NoteFinishedDatabase(Database* aDatabase); + +#ifdef DEBUG + bool + HasLiveDatabases() const; +#endif + + void + NoteActiveDatabase(Database* aDatabase); + + void + NoteInactiveDatabase(Database* aDatabase); + + void + GetSnapshotInitInfo(nsTHashtable& aLoadedItems, + nsTArray& aItemInfos, + uint32_t& aTotalLength, + int64_t& aInitialUsage, + int64_t& aPeakUsage, + LSSnapshot::LoadState& aLoadState); + + void + GetItem(const nsString& aKey, nsString& aValue) const; + + void + GetKeys(nsTArray& aKeys) const; + + ////////////////////////////////////////////////////////////////////////////// + // Mutation Methods + // + // These are only called during Snapshot::RecvCheckpoint + + /** + * Used by Snapshot::RecvCheckpoint to set a key/value pair as part of a an + * explicit batch. + */ + void + SetItem(Database* aDatabase, + const nsString& aDocumentURI, + const nsString& aKey, + const nsString& aOldValue, + const nsString& aValue); + + void + RemoveItem(Database* aDatabase, + const nsString& aDocumentURI, + const nsString& aKey, + const nsString& aOldValue); + + void + Clear(Database* aDatabase, + const nsString& aDocumentURI); + + void + PrivateBrowsingClear(); + + void + BeginUpdateBatch(int64_t aSnapshotInitialUsage); + + int64_t + EndUpdateBatch(int64_t aSnapshotPeakUsage); + + int64_t + RequestUpdateUsage(int64_t aRequestedSize, + int64_t aMinSize); + + NS_INLINE_DECL_REFCOUNTING(Datastore) + +private: + // Reference counted. + ~Datastore(); + + bool + UpdateUsage(int64_t aDelta); + + void + MaybeClose(); + + void + ConnectionClosedCallback(); + + void + CleanupMetadata(); + + void + NotifySnapshots(Database* aDatabase, + const nsAString& aKey, + const nsAString& aOldValue, + bool aAffectsOrder); + + void + MarkSnapshotsDirty(); + + void + NotifyObservers(Database* aDatabase, + const nsString& aDocumentURI, + const nsString& aKey, + const nsString& aOldValue, + const nsString& aNewValue); +}; + +class PreparedDatastore +{ + RefPtr mDatastore; + nsCOMPtr mTimer; + const Maybe mContentParentId; + // Strings share buffers if possible, so it's not a problem to duplicate the + // origin here. + const nsCString mOrigin; + uint64_t mDatastoreId; + bool mForPreload; + bool mInvalidated; + +public: + PreparedDatastore(Datastore* aDatastore, + const Maybe& aContentParentId, + const nsACString& aOrigin, + uint64_t aDatastoreId, + bool aForPreload) + : mDatastore(aDatastore) + , mTimer(NS_NewTimer()) + , mContentParentId(aContentParentId) + , mOrigin(aOrigin) + , mDatastoreId(aDatastoreId) + , mForPreload(aForPreload) + , mInvalidated(false) + { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aDatastore); + MOZ_ASSERT(mTimer); + + aDatastore->NoteLivePreparedDatastore(this); + + MOZ_ALWAYS_SUCCEEDS( + mTimer->InitWithNamedFuncCallback(TimerCallback, + this, + kPreparedDatastoreTimeoutMs, + nsITimer::TYPE_ONE_SHOT, + "PreparedDatastore::TimerCallback")); + } + + ~PreparedDatastore() + { + MOZ_ASSERT(mDatastore); + MOZ_ASSERT(mTimer); + + mTimer->Cancel(); + + mDatastore->NoteFinishedPreparedDatastore(this); + } + + Datastore* + GetDatastore() const + { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(mDatastore); + + return mDatastore; + } + + const Maybe& + GetContentParentId() const + { + return mContentParentId; + } + + const nsCString& + Origin() const + { + return mOrigin; + } + + void + Invalidate() + { + AssertIsOnBackgroundThread(); + + mInvalidated = true; + + if (mForPreload) { + mTimer->Cancel(); + + MOZ_ALWAYS_SUCCEEDS( + mTimer->InitWithNamedFuncCallback(TimerCallback, + this, + 0, + nsITimer::TYPE_ONE_SHOT, + "PreparedDatastore::TimerCallback")); + } + } + + bool + IsInvalidated() const + { + AssertIsOnBackgroundThread(); + + return mInvalidated; + } + +private: + void + Destroy(); + + static void + TimerCallback(nsITimer* aTimer, void* aClosure); +}; + +/******************************************************************************* + * Actor class declarations + ******************************************************************************/ + +class Database final + : public PBackgroundLSDatabaseParent +{ + RefPtr mDatastore; + Snapshot* mSnapshot; + const PrincipalInfo mPrincipalInfo; + const Maybe mContentParentId; + // Strings share buffers if possible, so it's not a problem to duplicate the + // origin here. + nsCString mOrigin; + uint32_t mPrivateBrowsingId; + bool mAllowedToClose; + bool mActorDestroyed; + bool mRequestedAllowToClose; +#ifdef DEBUG + bool mActorWasAlive; +#endif + +public: + // Created in AllocPBackgroundLSDatabaseParent. + Database(const PrincipalInfo& aPrincipalInfo, + const Maybe& aContentParentId, + const nsACString& aOrigin, + uint32_t aPrivateBrowsingId); + + Datastore* + GetDatastore() const + { + AssertIsOnBackgroundThread(); + return mDatastore; + } + + const PrincipalInfo& + GetPrincipalInfo() const + { + return mPrincipalInfo; + } + + bool + IsOwnedByProcess(ContentParentId aContentParentId) const + { + return mContentParentId && mContentParentId.value() == aContentParentId; + } + + uint32_t + PrivateBrowsingId() const + { + return mPrivateBrowsingId; + } + + const nsCString& + Origin() const + { + return mOrigin; + } + + void + SetActorAlive(Datastore* aDatastore); + + void + RegisterSnapshot(Snapshot* aSnapshot); + + void + UnregisterSnapshot(Snapshot* aSnapshot); + + Snapshot* + GetSnapshot() const + { + AssertIsOnBackgroundThread(); + return mSnapshot; + } + + void + RequestAllowToClose(); + + NS_INLINE_DECL_REFCOUNTING(mozilla::dom::Database) + +private: + // Reference counted. + ~Database(); + + void + AllowToClose(); + + // IPDL methods are only called by IPDL. + void + ActorDestroy(ActorDestroyReason aWhy) override; + + mozilla::ipc::IPCResult + RecvDeleteMe() override; + + mozilla::ipc::IPCResult + RecvAllowToClose() override; + + PBackgroundLSSnapshotParent* + AllocPBackgroundLSSnapshotParent(const nsString& aDocumentURI, + const bool& aIncreasePeakUsage, + const int64_t& aRequestedSize, + const int64_t& aMinSize, + LSSnapshotInitInfo* aInitInfo) override; + + mozilla::ipc::IPCResult + RecvPBackgroundLSSnapshotConstructor(PBackgroundLSSnapshotParent* aActor, + const nsString& aDocumentURI, + const bool& aIncreasePeakUsage, + const int64_t& aRequestedSize, + const int64_t& aMinSize, + LSSnapshotInitInfo* aInitInfo) override; + + bool + DeallocPBackgroundLSSnapshotParent(PBackgroundLSSnapshotParent* aActor) + override; +}; + +/** + * Attempts to capture the state of the underlying Datastore at the time of its + * creation so run-to-completion semantics can be honored. + * + * Rather than simply duplicate the contents of `DataStore::mValues` and + * `Datastore::mOrderedItems` at the time of their creation, the Snapshot tracks + * mutations to the Datastore as they happen, saving off the state of values as + * they existed when the Snapshot was created. In other words, given an initial + * Datastore state of { foo: 'bar', bar: 'baz' }, the Snapshot won't store those + * values until it hears via `SaveItem` that "foo" is being over-written. At + * that time, it will save off foo='bar' in mValues. + * + * ## Quota Allocation ## + * + * ## States ## + * + */ +class Snapshot final + : public PBackgroundLSSnapshotParent +{ + /** + * The Database that owns this snapshot. There is a 1:1 relationship between + * snapshots and databases. + */ + RefPtr mDatabase; + RefPtr mDatastore; + /** + * The set of keys for which values have been sent to the child LSSnapshot. + * Cleared once all values have been sent as indicated by + * mLoadedItems.Count()==mTotalLength and therefore mLoadedAllItems should be + * true. No requests should be received for keys already in this set, and + * this is enforced by fatal IPC error (unless fuzzing). + */ + nsTHashtable mLoadedItems; + /** + * The set of keys for which a RecvLoadItem request was received but there + * was no such key, and so null was returned. The child LSSnapshot will also + * cache these values, so redundant requests are also handled with fatal + * process termination just like for mLoadedItems. Also cleared when + * mLoadedAllItems becomes true because then the child can infer that all + * other values must be null. (Note: this could also be done when + * mLoadKeysReceived is true as a further optimization, but is not.) + */ + nsTHashtable mUnknownItems; + /** + * Values that have changed in mDatastore as reported by SaveItem + * notifications that are not yet known to the child LSSnapshot. + * + * The naive way to snapshot the state of mDatastore would be to duplicate its + * internal mValues at the time of our creation, but that is wasteful if few + * changes are made to the Datastore's state. So we only track values that + * are changed/evicted from the Datastore as they happen, as reported to us by + * SaveItem notifications. + */ + nsDataHashtable mValues; + /** + * Latched state of mDatastore's keys during a SaveItem notification with + * aAffectsOrder=true. The ordered keys needed to be saved off so that a + * consistent ordering could be presented to the child LSSnapshot when it asks + * for them via RecvLoadKeys. + */ + nsTArray mKeys; + nsString mDocumentURI; + /** + * The number of key/value pairs that were present in the Datastore at the + * time the snapshot was created. Once we have sent this many values to the + * child LSSnapshot, we can infer that it has received all of the keys/values + * and set mLoadedAllItems to true and clear mLoadedItems and mUnknownItems. + * Note that knowing the keys/values is not the same as knowing their ordering + * and so mKeys may be retained. + */ + uint32_t mTotalLength; + int64_t mUsage; + int64_t mPeakUsage; + /** + * True if SaveItem has saved mDatastore's keys into mKeys because a SaveItem + * notification with aAffectsOrder=true was received. + */ + bool mSavedKeys; + bool mActorDestroyed; + bool mFinishReceived; + bool mLoadedReceived; + /** + * True if LSSnapshot's mLoadState should be LoadState::AllOrderedItems or + * LoadState::AllUnorderedItems. It will be AllOrderedItems if the initial + * snapshot contained all the data or if the state was AllOrderedKeys and + * successive RecvLoadItem requests have resulted in the LSSnapshot being told + * all of the key/value pairs. It will be AllUnorderedItems if the state was + * LoadState::Partial and successive RecvLoadItem requests got all the + * keys/values but the key ordering was not retrieved. + */ + bool mLoadedAllItems; + /** + * True if LSSnapshot's mLoadState should be LoadState::AllOrderedItems or + * AllOrderedKeys. This can occur because of the initial snapshot, or because + * a RecvLoadKeys request was received. + */ + bool mLoadKeysReceived; + bool mSentMarkDirty; + +public: + // Created in AllocPBackgroundLSSnapshotParent. + Snapshot(Database* aDatabase, + const nsAString& aDocumentURI); + + void + Init(nsTHashtable& aLoadedItems, + uint32_t aTotalLength, + int64_t aInitialUsage, + int64_t aPeakUsage, + LSSnapshot::LoadState aLoadState) + { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aInitialUsage >= 0); + MOZ_ASSERT(aPeakUsage >= aInitialUsage); + MOZ_ASSERT_IF(aLoadState == LSSnapshot::LoadState::AllOrderedItems, + aLoadedItems.Count() == 0); + MOZ_ASSERT(mTotalLength == 0); + MOZ_ASSERT(mUsage == -1); + MOZ_ASSERT(mPeakUsage == -1); + + mLoadedItems.SwapElements(aLoadedItems); + mTotalLength = aTotalLength; + mUsage = aInitialUsage; + mPeakUsage = aPeakUsage; + if (aLoadState == LSSnapshot::LoadState::AllOrderedKeys) { + mLoadKeysReceived = true; + } else if (aLoadState == LSSnapshot::LoadState::AllOrderedItems) { + mLoadedReceived = true; + mLoadedAllItems = true; + mLoadKeysReceived = true; + } + } + + /** + * Called via NotifySnapshots by Datastore whenever it is updating its + * internal state so that snapshots can save off the state of a value at the + * time of their creation. + */ + void + SaveItem(const nsAString& aKey, + const nsAString& aOldValue, + bool aAffectsOrder); + + void + MarkDirty(); + + NS_INLINE_DECL_REFCOUNTING(mozilla::dom::Snapshot) + +private: + // Reference counted. + ~Snapshot(); + + void + Finish(); + + // IPDL methods are only called by IPDL. + void + ActorDestroy(ActorDestroyReason aWhy) override; + + mozilla::ipc::IPCResult + RecvDeleteMe() override; + + mozilla::ipc::IPCResult + RecvCheckpoint(nsTArray&& aWriteInfos) override; + + mozilla::ipc::IPCResult + RecvFinish() override; + + mozilla::ipc::IPCResult + RecvLoaded() override; + + mozilla::ipc::IPCResult + RecvLoadItem(const nsString& aKey, + nsString* aValue) override; + + mozilla::ipc::IPCResult + RecvLoadKeys(nsTArray* aKeys) override; + + mozilla::ipc::IPCResult + RecvIncreasePeakUsage(const int64_t& aRequestedSize, + const int64_t& aMinSize, + int64_t* aSize) override; + + mozilla::ipc::IPCResult + RecvPing() override; +}; + +class Observer final + : public PBackgroundLSObserverParent +{ + nsCString mOrigin; + bool mActorDestroyed; + +public: + // Created in AllocPBackgroundLSObserverParent. + explicit Observer(const nsACString& aOrigin); + + const nsCString& + Origin() const + { + return mOrigin; + } + + void + Observe(Database* aDatabase, + const nsString& aDocumentURI, + const nsString& aKey, + const nsString& aOldValue, + const nsString& aNewValue); + + NS_INLINE_DECL_REFCOUNTING(mozilla::dom::Observer) + +private: + // Reference counted. + ~Observer(); + + // IPDL methods are only called by IPDL. + void + ActorDestroy(ActorDestroyReason aWhy) override; + + mozilla::ipc::IPCResult + RecvDeleteMe() override; +}; + +class LSRequestBase + : public DatastoreOperationBase + , public PBackgroundLSRequestParent +{ +protected: + enum class State + { + // Just created on the PBackground thread. Next step is Opening. + Initial, + + // Waiting to open/opening on the main thread. Next step is either + // Nesting if a subclass needs to process more nested states or + // SendingReadyMessage if a subclass doesn't need any nested processing. + Opening, + + // Doing nested processing. + Nesting, + + // Waiting to send/sending the ready message on the PBackground thread. Next + // step is WaitingForFinish. + SendingReadyMessage, + + // Waiting for the finish message on the PBackground thread. Next step is + // SendingResults. + WaitingForFinish, + + // Waiting to send/sending results on the PBackground thread. Next step is + // Completed. + SendingResults, + + // All done. + Completed + }; + + nsCOMPtr mMainEventTarget; + State mState; + +public: + explicit LSRequestBase(nsIEventTarget* aMainEventTarget); + + void + Dispatch(); + +protected: + ~LSRequestBase() override; + + virtual nsresult + Open() = 0; + + virtual nsresult + NestedRun(); + + virtual void + GetResponse(LSRequestResponse& aResponse) = 0; + + virtual void + Cleanup() + { } + +private: + void + SendReadyMessage(); + + void + SendResults(); + +protected: + // Common nsIRunnable implementation that subclasses may not override. + NS_IMETHOD + Run() final; + + // IPDL methods. + void + ActorDestroy(ActorDestroyReason aWhy) override; + +private: + mozilla::ipc::IPCResult + RecvCancel() override; + + mozilla::ipc::IPCResult + RecvFinish() override; +}; + +class PrepareDatastoreOp + : public LSRequestBase + , public OpenDirectoryListener +{ + class LoadDataOp; + + enum class NestedState + { + // The nesting has not yet taken place. Next step is + // CheckExistingOperations. + BeforeNesting, + + // Checking if a prepare datastore operation is already running for given + // origin on the PBackground thread. Next step is CheckClosingDatastore. + CheckExistingOperations, + + // Checking if a datastore is closing the connection for given origin on + // the PBackground thread. Next step is PreparationPending. + CheckClosingDatastore, + + // Opening directory or initializing quota manager on the PBackground + // thread. Next step is either DirectoryOpenPending if quota manager is + // already initialized or QuotaManagerPending if quota manager needs to be + // initialized. + // If a datastore already exists for given origin then the next state is + // SendingReadyMessage. + PreparationPending, + + // Waiting for quota manager initialization to complete on the PBackground + // thread. Next step is either SendingReadyMessage if initialization failed + // or DirectoryOpenPending if initialization succeeded. + QuotaManagerPending, + + // Waiting for directory open allowed on the PBackground thread. The next + // step is either SendingReadyMessage if directory lock failed to acquire, + // or DatabaseWorkOpen if directory lock is acquired. + DirectoryOpenPending, + + // Waiting to do/doing work on the QuotaManager IO thread. Its next step is + // BeginLoadData. + DatabaseWorkOpen, + + // Starting a load data operation on the PBackground thread. Next step is + // DatabaseWorkLoadData. + BeginLoadData, + + // Waiting to do/doing work on the connection thread. This involves waiting + // for the LoadDataOp to do its work. Eventually the state will transition + // to SendingReadyMessage. + DatabaseWorkLoadData, + + // The nesting has completed. + AfterNesting + }; + + nsCOMPtr mMainEventTarget; + RefPtr mContentParent; + RefPtr mDelayedOp; + RefPtr mDirectoryLock; + RefPtr mConnection; + RefPtr mDatastore; + nsAutoPtr mArchivedOriginScope; + LoadDataOp* mLoadDataOp; + nsDataHashtable mValues; + nsTArray mOrderedItems; + const LSRequestPrepareDatastoreParams mParams; + Maybe mContentParentId; + nsCString mSuffix; + nsCString mGroup; + nsCString mMainThreadOrigin; + nsCString mOrigin; + nsString mDatabaseFilePath; + uint32_t mPrivateBrowsingId; + int64_t mUsage; + int64_t mSizeOfKeys; + int64_t mSizeOfItems; + NestedState mNestedState; + bool mDatabaseNotAvailable; + bool mRequestedDirectoryLock; + bool mInvalidated; + +#ifdef DEBUG + int64_t mDEBUGUsage; +#endif + +public: + PrepareDatastoreOp(nsIEventTarget* aMainEventTarget, + already_AddRefed aContentParent, + const LSRequestParams& aParams); + + bool + OriginIsKnown() const + { + AssertIsOnOwningThread(); + + return !mOrigin.IsEmpty(); + } + + const nsCString& + Origin() const + { + AssertIsOnOwningThread(); + MOZ_ASSERT(OriginIsKnown()); + + return mOrigin; + } + + bool + RequestedDirectoryLock() const + { + AssertIsOnOwningThread(); + + return mRequestedDirectoryLock; + } + + void + Invalidate() + { + AssertIsOnOwningThread(); + + mInvalidated = true; + } + +private: + ~PrepareDatastoreOp() override; + + nsresult + Open() override; + + nsresult + CheckExistingOperations(); + + nsresult + CheckClosingDatastoreInternal(); + + nsresult + CheckClosingDatastore(); + + nsresult + BeginDatastorePreparationInternal(); + + nsresult + BeginDatastorePreparation(); + + nsresult + QuotaManagerOpen(); + + nsresult + OpenDirectory(); + + void + SendToIOThread(); + + nsresult + DatabaseWork(); + + nsresult + DatabaseNotAvailable(); + + nsresult + EnsureDirectoryEntry(nsIFile* aEntry, + bool aCreateIfNotExists, + bool aDirectory, + bool* aAlreadyExisted = nullptr); + + nsresult + VerifyDatabaseInformation(mozIStorageConnection* aConnection); + + already_AddRefed + GetQuotaObject(); + + nsresult + BeginLoadData(); + + void + FinishNesting(); + + nsresult + FinishNestingOnNonOwningThread(); + + nsresult + NestedRun() override; + + void + GetResponse(LSRequestResponse& aResponse) override; + + void + Cleanup() override; + + void + ConnectionClosedCallback(); + + void + CleanupMetadata(); + + NS_DECL_ISUPPORTS_INHERITED + + // IPDL overrides. + void + ActorDestroy(ActorDestroyReason aWhy) override; + + // OpenDirectoryListener overrides. + void + DirectoryLockAcquired(DirectoryLock* aLock) override; + + void + DirectoryLockFailed() override; +}; + +class PrepareDatastoreOp::LoadDataOp final + : public ConnectionDatastoreOperationBase +{ + RefPtr mPrepareDatastoreOp; + +public: + explicit LoadDataOp(PrepareDatastoreOp* aPrepareDatastoreOp) + : ConnectionDatastoreOperationBase(aPrepareDatastoreOp->mConnection) + , mPrepareDatastoreOp(aPrepareDatastoreOp) + { } + +private: + ~LoadDataOp() = default; + + nsresult + DoDatastoreWork() override; + + void + OnSuccess() override; + + void + OnFailure(nsresult aResultCode) override; + + void + Cleanup() override; +}; + +class PrepareObserverOp + : public LSRequestBase +{ + const LSRequestPrepareObserverParams mParams; + nsCString mOrigin; + +public: + PrepareObserverOp(nsIEventTarget* aMainEventTarget, + const LSRequestParams& aParams); + +private: + nsresult + Open() override; + + void + GetResponse(LSRequestResponse& aResponse) override; +}; + +class LSSimpleRequestBase + : public DatastoreOperationBase + , public PBackgroundLSSimpleRequestParent +{ +protected: + enum class State + { + // Just created on the PBackground thread. Next step is Opening. + Initial, + + // Waiting to open/opening on the main thread. Next step is SendingResults. + Opening, + + // Waiting to send/sending results on the PBackground thread. Next step is + // Completed. + SendingResults, + + // All done. + Completed + }; + + State mState; + +public: + LSSimpleRequestBase(); + + void + Dispatch(); + +protected: + ~LSSimpleRequestBase() override; + + virtual nsresult + Open() = 0; + + virtual void + GetResponse(LSSimpleRequestResponse& aResponse) = 0; + +private: + void + SendResults(); + + // Common nsIRunnable implementation that subclasses may not override. + NS_IMETHOD + Run() final; + + // IPDL methods. + void + ActorDestroy(ActorDestroyReason aWhy) override; +}; + +class PreloadedOp + : public LSSimpleRequestBase +{ + const LSSimpleRequestPreloadedParams mParams; + nsCString mOrigin; + +public: + explicit PreloadedOp(const LSSimpleRequestParams& aParams); + +private: + nsresult + Open() override; + + void + GetResponse(LSSimpleRequestResponse& aResponse) override; +}; + +/******************************************************************************* + * Other class declarations + ******************************************************************************/ + +struct ArchivedOriginInfo +{ + OriginAttributes mOriginAttributes; + nsCString mOriginNoSuffix; + + ArchivedOriginInfo(const OriginAttributes& aOriginAttributes, + const nsACString& aOriginNoSuffix) + : mOriginAttributes(aOriginAttributes) + , mOriginNoSuffix(aOriginNoSuffix) + { } +}; + +class ArchivedOriginScope +{ + struct Origin + { + nsCString mOriginSuffix; + nsCString mOriginNoSuffix; + + Origin(const nsACString& aOriginSuffix, + const nsACString& aOriginNoSuffix) + : mOriginSuffix(aOriginSuffix) + , mOriginNoSuffix(aOriginNoSuffix) + { } + + const nsACString& + OriginSuffix() const + { + return mOriginSuffix; + } + + const nsACString& + OriginNoSuffix() const + { + return mOriginNoSuffix; + } + }; + + struct Prefix + { + nsCString mOriginNoSuffix; + + explicit Prefix(const nsACString& aOriginNoSuffix) + : mOriginNoSuffix(aOriginNoSuffix) + { } + + const nsACString& + OriginNoSuffix() const + { + return mOriginNoSuffix; + } + }; + + struct Pattern + { + UniquePtr mPattern; + + explicit Pattern(const OriginAttributesPattern& aPattern) + : mPattern(MakeUnique(aPattern)) + { } + + Pattern(const Pattern& aOther) + : mPattern(MakeUnique(*aOther.mPattern)) + { } + + Pattern(Pattern&& aOther) = default; + + const OriginAttributesPattern& + GetPattern() const + { + MOZ_ASSERT(mPattern); + return *mPattern; + } + }; + + struct Null + { }; + + using DataType = Variant; + + DataType mData; + +public: + static ArchivedOriginScope* + CreateFromOrigin(nsIPrincipal* aPrincipal); + + static ArchivedOriginScope* + CreateFromPrefix(nsIPrincipal* aPrincipal); + + static ArchivedOriginScope* + CreateFromPattern(const OriginAttributesPattern& aPattern); + + static ArchivedOriginScope* + CreateFromNull(); + + bool + IsOrigin() const + { + return mData.is(); + } + + bool + IsPrefix() const + { + return mData.is(); + } + + bool + IsPattern() const + { + return mData.is(); + } + + bool + IsNull() const + { + return mData.is(); + } + + const nsACString& + OriginSuffix() const + { + MOZ_ASSERT(IsOrigin()); + + return mData.as().OriginSuffix(); + } + + const nsACString& + OriginNoSuffix() const + { + MOZ_ASSERT(IsOrigin() || IsPrefix()); + + if (IsOrigin()) { + return mData.as().OriginNoSuffix(); + } + return mData.as().OriginNoSuffix(); + } + + const OriginAttributesPattern& + GetPattern() const + { + MOZ_ASSERT(IsPattern()); + + return mData.as().GetPattern(); + } + + void + GetBindingClause(nsACString& aBindingClause) const; + + nsresult + BindToStatement(mozIStorageStatement* aStatement) const; + + bool + HasMatches(ArchivedOriginHashtable* aHashtable) const; + + void + RemoveMatches(ArchivedOriginHashtable* aHashtable) const; + +private: + // Move constructors + explicit ArchivedOriginScope(const Origin&& aOrigin) + : mData(aOrigin) + { } + + explicit ArchivedOriginScope(const Pattern&& aPattern) + : mData(aPattern) + { } + + explicit ArchivedOriginScope(const Prefix&& aPrefix) + : mData(aPrefix) + { } + + explicit ArchivedOriginScope(const Null&& aNull) + : mData(aNull) + { } +}; + +class ArchivedOriginScopeHelper + : public Runnable +{ + Monitor mMonitor; + const OriginAttributes mAttrs; + const nsCString mSpec; + nsAutoPtr mArchivedOriginScope; + nsresult mMainThreadResultCode; + bool mWaiting; + bool mPrefix; + +public: + ArchivedOriginScopeHelper(const nsACString& aSpec, + const OriginAttributes& aAttrs, + bool aPrefix) + : Runnable("dom::localstorage::ArchivedOriginScopeHelper") + , mMonitor("ArchivedOriginScopeHelper::mMonitor") + , mAttrs(aAttrs) + , mSpec(aSpec) + , mMainThreadResultCode(NS_OK) + , mWaiting(true) + , mPrefix(aPrefix) + { + AssertIsOnIOThread(); + } + + nsresult + BlockAndReturnArchivedOriginScope( + nsAutoPtr& aArchivedOriginScope); + +private: + nsresult + RunOnMainThread(); + + NS_DECL_NSIRUNNABLE +}; + +class QuotaClient final + : public mozilla::dom::quota::Client +{ + class ClearPrivateBrowsingRunnable; + class Observer; + class MatchFunction; + + static QuotaClient* sInstance; + static bool sObserversRegistered; + + Mutex mShadowDatabaseMutex; + bool mShutdownRequested; + +public: + QuotaClient(); + + static QuotaClient* + GetInstance() + { + AssertIsOnBackgroundThread(); + + return sInstance; + } + + static bool + IsShuttingDownOnBackgroundThread() + { + AssertIsOnBackgroundThread(); + + if (sInstance) { + return sInstance->IsShuttingDown(); + } + + return QuotaManager::IsShuttingDown(); + } + + static bool + IsShuttingDownOnNonBackgroundThread() + { + MOZ_ASSERT(!IsOnBackgroundThread()); + + return QuotaManager::IsShuttingDown(); + } + + static nsresult + RegisterObservers(nsIEventTarget* aBackgroundEventTarget); + + mozilla::Mutex& + ShadowDatabaseMutex() + { + MOZ_ASSERT(IsOnIOThread() || IsOnConnectionThread()); + + return mShadowDatabaseMutex; + } + + bool + IsShuttingDown() const + { + AssertIsOnBackgroundThread(); + + return mShutdownRequested; + } + + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(mozilla::dom::QuotaClient, override) + + Type + GetType() override; + + nsresult + InitOrigin(PersistenceType aPersistenceType, + const nsACString& aGroup, + const nsACString& aOrigin, + const AtomicBool& aCanceled, + UsageInfo* aUsageInfo) override; + + nsresult + GetUsageForOrigin(PersistenceType aPersistenceType, + const nsACString& aGroup, + const nsACString& aOrigin, + const AtomicBool& aCanceled, + UsageInfo* aUsageInfo) override; + + nsresult + AboutToClearOrigins(const Nullable& aPersistenceType, + const OriginScope& aOriginScope) override; + + void + OnOriginClearCompleted(PersistenceType aPersistenceType, + const nsACString& aOrigin) + override; + + void + ReleaseIOThreadObjects() override; + + void + AbortOperations(const nsACString& aOrigin) override; + + void + AbortOperationsForProcess(ContentParentId aContentParentId) override; + + void + StartIdleMaintenance() override; + + void + StopIdleMaintenance() override; + + void + ShutdownWorkThreads() override; + +private: + ~QuotaClient() override; + + nsresult + CreateArchivedOriginScope( + const OriginScope& aOriginScope, + nsAutoPtr& aArchivedOriginScope); + + nsresult + PerformDelete(mozIStorageConnection* aConnection, + const nsACString& aSchemaName, + ArchivedOriginScope* aArchivedOriginScope) const; +}; + +class QuotaClient::ClearPrivateBrowsingRunnable final + : public Runnable +{ +public: + ClearPrivateBrowsingRunnable() + : Runnable("mozilla::dom::ClearPrivateBrowsingRunnable") + { + MOZ_ASSERT(NS_IsMainThread()); + } + +private: + ~ClearPrivateBrowsingRunnable() = default; + + NS_DECL_NSIRUNNABLE +}; + +class QuotaClient::Observer final + : public nsIObserver +{ + nsCOMPtr mBackgroundEventTarget; + +public: + explicit Observer(nsIEventTarget* aBackgroundEventTarget) + : mBackgroundEventTarget(aBackgroundEventTarget) + { + MOZ_ASSERT(NS_IsMainThread()); + } + + NS_DECL_ISUPPORTS + +private: + ~Observer() + { + MOZ_ASSERT(NS_IsMainThread()); + } + + NS_DECL_NSIOBSERVER +}; + +class QuotaClient::MatchFunction final + : public mozIStorageFunction +{ + OriginAttributesPattern mPattern; + +public: + explicit MatchFunction(const OriginAttributesPattern& aPattern) + : mPattern(aPattern) + { } + +private: + ~MatchFunction() = default; + + NS_DECL_ISUPPORTS + NS_DECL_MOZISTORAGEFUNCTION +}; + +/******************************************************************************* + * Globals + ******************************************************************************/ + +typedef nsTArray PrepareDatastoreOpArray; + +StaticAutoPtr gPrepareDatastoreOps; + +typedef nsDataHashtable DatastoreHashtable; + +StaticAutoPtr gDatastores; + +uint64_t gLastDatastoreId = 0; + +typedef nsClassHashtable + PreparedDatastoreHashtable; + +StaticAutoPtr gPreparedDatastores; + +typedef nsTArray LiveDatabaseArray; + +StaticAutoPtr gLiveDatabases; + +StaticRefPtr gConnectionThread; + +uint64_t gLastObserverId = 0; + +typedef nsRefPtrHashtable PreparedObserverHashtable; + +StaticAutoPtr gPreparedObsevers; + +typedef nsClassHashtable> + ObserverHashtable; + +StaticAutoPtr gObservers; + +Atomic gOriginLimitKB(kDefaultOriginLimitKB); +Atomic gShadowWrites(kDefaultShadowWrites); +Atomic gSnapshotPrefill(kDefaultSnapshotPrefill); + +typedef nsDataHashtable UsageHashtable; + +// Can only be touched on the Quota Manager I/O thread. +StaticAutoPtr gUsages; + +StaticAutoPtr gArchivedOrigins; + +// Can only be touched on the Quota Manager I/O thread. +bool gInitializedShadowStorage = false; + +bool +IsOnConnectionThread() +{ + MOZ_ASSERT(gConnectionThread); + return gConnectionThread->IsOnConnectionThread(); +} + +void +AssertIsOnConnectionThread() +{ + MOZ_ASSERT(gConnectionThread); + gConnectionThread->AssertIsOnConnectionThread(); +} + +void +InitUsageForOrigin(const nsACString& aOrigin, int64_t aUsage) +{ + AssertIsOnIOThread(); + + if (!gUsages) { + gUsages = new UsageHashtable(); + } + + MOZ_ASSERT(!gUsages->Contains(aOrigin)); + gUsages->Put(aOrigin, aUsage); +} + +nsresult +LoadArchivedOrigins() +{ + AssertIsOnIOThread(); + MOZ_ASSERT(!gArchivedOrigins); + + QuotaManager* quotaManager = QuotaManager::Get(); + MOZ_ASSERT(quotaManager); + + // Ensure that the webappsstore.sqlite is moved to new place. + nsresult rv = quotaManager->EnsureStorageIsInitialized(); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + nsCOMPtr connection; + rv = CreateArchiveStorageConnection(quotaManager->GetStoragePath(), + getter_AddRefs(connection)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (!connection) { + gArchivedOrigins = new ArchivedOriginHashtable(); + return NS_OK; + } + + nsCOMPtr stmt; + rv = connection->CreateStatement(NS_LITERAL_CSTRING( + "SELECT DISTINCT originAttributes, originKey " + "FROM webappsstore2;" + ), getter_AddRefs(stmt)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + nsAutoPtr archivedOrigins( + new ArchivedOriginHashtable()); + + bool hasResult; + while (NS_SUCCEEDED(rv = stmt->ExecuteStep(&hasResult)) && hasResult) { + nsCString originSuffix; + rv = stmt->GetUTF8String(0, originSuffix); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + nsCString originNoSuffix; + rv = stmt->GetUTF8String(1, originNoSuffix); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + nsCString hashKey = GetArchivedOriginHashKey(originSuffix, originNoSuffix); + + OriginAttributes originAttributes; + if (NS_WARN_IF(!originAttributes.PopulateFromSuffix(originSuffix))) { + return NS_ERROR_FAILURE; + } + + nsAutoPtr archivedOriginInfo( + new ArchivedOriginInfo(originAttributes, originNoSuffix)); + + archivedOrigins->Put(hashKey, archivedOriginInfo.forget()); + } + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + gArchivedOrigins = archivedOrigins.forget(); + return NS_OK; +} + +nsresult +GetUsage(mozIStorageConnection* aConnection, + ArchivedOriginScope* aArchivedOriginScope, + int64_t* aUsage) +{ + AssertIsOnIOThread(); + MOZ_ASSERT(aConnection); + MOZ_ASSERT(aUsage); + + nsresult rv; + + nsCOMPtr stmt; + if (aArchivedOriginScope) { + rv = aConnection->CreateStatement(NS_LITERAL_CSTRING( + "SELECT sum(length(key) + length(value)) " + "FROM webappsstore2 " + "WHERE originKey = :originKey " + "AND originAttributes = :originAttributes;" + ), getter_AddRefs(stmt)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aArchivedOriginScope->BindToStatement(stmt); + } else { + rv = aConnection->CreateStatement(NS_LITERAL_CSTRING( + "SELECT sum(length(key) + length(value)) " + "FROM data" + ), getter_AddRefs(stmt)); + } + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + bool hasResult; + rv = stmt->ExecuteStep(&hasResult); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (NS_WARN_IF(!hasResult)) { + return NS_ERROR_FAILURE; + } + + int64_t usage; + rv = stmt->GetInt64(0, &usage); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + *aUsage = usage; + return NS_OK; +} + +void +ShadowWritesPrefChangedCallback(const char* aPrefName, void* aClosure) +{ + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(!strcmp(aPrefName, kShadowWritesPref)); + MOZ_ASSERT(!aClosure); + + gShadowWrites = Preferences::GetBool(aPrefName, kDefaultShadowWrites); +} + +void +SnapshotPrefillPrefChangedCallback(const char* aPrefName, void* aClosure) +{ + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(!strcmp(aPrefName, kSnapshotPrefillPref)); + MOZ_ASSERT(!aClosure); + + int32_t snapshotPrefill = + Preferences::GetInt(aPrefName, kDefaultSnapshotPrefill); + + // The magic -1 is for use only by tests. + if (snapshotPrefill == -1) { + snapshotPrefill = INT32_MAX; + } + + gSnapshotPrefill = snapshotPrefill; +} + +} // namespace + +/******************************************************************************* + * Exported functions + ******************************************************************************/ + +PBackgroundLSDatabaseParent* +AllocPBackgroundLSDatabaseParent(const PrincipalInfo& aPrincipalInfo, + const uint32_t& aPrivateBrowsingId, + const uint64_t& aDatastoreId) +{ + AssertIsOnBackgroundThread(); + + if (NS_WARN_IF(QuotaClient::IsShuttingDownOnBackgroundThread())) { + return nullptr; + } + + if (NS_WARN_IF(!gPreparedDatastores)) { + ASSERT_UNLESS_FUZZING(); + return nullptr; + } + + PreparedDatastore* preparedDatastore = gPreparedDatastores->Get(aDatastoreId); + if (NS_WARN_IF(!preparedDatastore)) { + ASSERT_UNLESS_FUZZING(); + return nullptr; + } + + // If we ever decide to return null from this point on, we need to make sure + // that the datastore is closed and the prepared datastore is removed from the + // gPreparedDatastores hashtable. + // We also assume that IPDL must call RecvPBackgroundLSDatabaseConstructor + // once we return a valid actor in this method. + + RefPtr database = + new Database(aPrincipalInfo, + preparedDatastore->GetContentParentId(), + preparedDatastore->Origin(), + aPrivateBrowsingId); + + // Transfer ownership to IPDL. + return database.forget().take(); +} + +bool +RecvPBackgroundLSDatabaseConstructor(PBackgroundLSDatabaseParent* aActor, + const PrincipalInfo& aPrincipalInfo, + const uint32_t& aPrivateBrowsingId, + const uint64_t& aDatastoreId) +{ + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aActor); + MOZ_ASSERT(gPreparedDatastores); + MOZ_ASSERT(gPreparedDatastores->Get(aDatastoreId)); + MOZ_ASSERT(!QuotaClient::IsShuttingDownOnBackgroundThread()); + + // The actor is now completely built (it has a manager, channel and it's + // registered as a subprotocol). + // ActorDestroy will be called if we fail here. + + nsAutoPtr preparedDatastore; + gPreparedDatastores->Remove(aDatastoreId, &preparedDatastore); + MOZ_ASSERT(preparedDatastore); + + auto* database = static_cast(aActor); + + database->SetActorAlive(preparedDatastore->GetDatastore()); + + // It's possible that AbortOperations was called before the database actor + // was created and became live. Let the child know that the database in no + // longer valid. + if (preparedDatastore->IsInvalidated()) { + database->RequestAllowToClose(); + } + + return true; +} + +bool +DeallocPBackgroundLSDatabaseParent(PBackgroundLSDatabaseParent* aActor) +{ + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aActor); + + // Transfer ownership back from IPDL. + RefPtr actor = dont_AddRef(static_cast(aActor)); + + return true; +} + +PBackgroundLSObserverParent* +AllocPBackgroundLSObserverParent(const uint64_t& aObserverId) +{ + AssertIsOnBackgroundThread(); + + if (NS_WARN_IF(QuotaClient::IsShuttingDownOnBackgroundThread())) { + return nullptr; + } + + if (NS_WARN_IF(!gPreparedObsevers)) { + ASSERT_UNLESS_FUZZING(); + return nullptr; + } + + RefPtr observer = gPreparedObsevers->Get(aObserverId); + if (NS_WARN_IF(!observer)) { + ASSERT_UNLESS_FUZZING(); + return nullptr; + } + + //observer->SetObject(this); + + // Transfer ownership to IPDL. + return observer.forget().take(); +} + +bool +RecvPBackgroundLSObserverConstructor(PBackgroundLSObserverParent* aActor, + const uint64_t& aObserverId) +{ + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aActor); + MOZ_ASSERT(gPreparedObsevers); + MOZ_ASSERT(gPreparedObsevers->GetWeak(aObserverId)); + + RefPtr observer; + gPreparedObsevers->Remove(aObserverId, observer.StartAssignment()); + MOZ_ASSERT(observer); + + if (!gPreparedObsevers->Count()) { + gPreparedObsevers = nullptr; + } + + if (!gObservers) { + gObservers = new ObserverHashtable(); + } + + nsTArray* array; + if (!gObservers->Get(observer->Origin(), &array)) { + array = new nsTArray(); + gObservers->Put(observer->Origin(), array); + } + array->AppendElement(observer); + + return true; +} + +bool +DeallocPBackgroundLSObserverParent(PBackgroundLSObserverParent* aActor) +{ + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aActor); + + // Transfer ownership back from IPDL. + RefPtr actor = dont_AddRef(static_cast(aActor)); + + return true; +} + +PBackgroundLSRequestParent* +AllocPBackgroundLSRequestParent(PBackgroundParent* aBackgroundActor, + const LSRequestParams& aParams) +{ + AssertIsOnBackgroundThread(); + + if (NS_WARN_IF(QuotaClient::IsShuttingDownOnBackgroundThread())) { + return nullptr; + } + + // If we're in the same process as the actor, we need to get the target event + // queue from the current RequestHelper. + nsCOMPtr mainEventTarget; + if (!BackgroundParent::IsOtherProcessActor(aBackgroundActor)) { + mainEventTarget = LSObject::GetSyncLoopEventTarget(); + } + + RefPtr actor; + + switch (aParams.type()) { + case LSRequestParams::TLSRequestPrepareDatastoreParams: { + RefPtr contentParent = + BackgroundParent::GetContentParent(aBackgroundActor); + + RefPtr prepareDatastoreOp = + new PrepareDatastoreOp(mainEventTarget, + contentParent.forget(), + aParams); + + if (!gPrepareDatastoreOps) { + gPrepareDatastoreOps = new PrepareDatastoreOpArray(); + } + gPrepareDatastoreOps->AppendElement(prepareDatastoreOp); + + actor = std::move(prepareDatastoreOp); + + break; + } + + case LSRequestParams::TLSRequestPrepareObserverParams: { + RefPtr prepareObserverOp = + new PrepareObserverOp(mainEventTarget, aParams); + + actor = std::move(prepareObserverOp); + + break; + } + + default: + MOZ_CRASH("Should never get here!"); + } + + // Transfer ownership to IPDL. + return actor.forget().take(); +} + +bool +RecvPBackgroundLSRequestConstructor(PBackgroundLSRequestParent* aActor, + const LSRequestParams& aParams) +{ + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aActor); + MOZ_ASSERT(aParams.type() != LSRequestParams::T__None); + MOZ_ASSERT(!QuotaClient::IsShuttingDownOnBackgroundThread()); + + // The actor is now completely built. + + auto* op = static_cast(aActor); + + op->Dispatch(); + + return true; +} + +bool +DeallocPBackgroundLSRequestParent(PBackgroundLSRequestParent* aActor) +{ + AssertIsOnBackgroundThread(); + + // Transfer ownership back from IPDL. + RefPtr actor = + dont_AddRef(static_cast(aActor)); + + return true; +} + +PBackgroundLSSimpleRequestParent* +AllocPBackgroundLSSimpleRequestParent(const LSSimpleRequestParams& aParams) +{ + AssertIsOnBackgroundThread(); + + if (NS_WARN_IF(QuotaClient::IsShuttingDownOnBackgroundThread())) { + return nullptr; + } + + RefPtr actor; + + switch (aParams.type()) { + case LSSimpleRequestParams::TLSSimpleRequestPreloadedParams: { + RefPtr preloadedOp = + new PreloadedOp(aParams); + + actor = std::move(preloadedOp); + + break; + } + + default: + MOZ_CRASH("Should never get here!"); + } + + // Transfer ownership to IPDL. + return actor.forget().take(); +} + +bool +RecvPBackgroundLSSimpleRequestConstructor( + PBackgroundLSSimpleRequestParent* aActor, + const LSSimpleRequestParams& aParams) +{ + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aActor); + MOZ_ASSERT(aParams.type() != LSSimpleRequestParams::T__None); + MOZ_ASSERT(!QuotaClient::IsShuttingDownOnBackgroundThread()); + + // The actor is now completely built. + + auto* op = static_cast(aActor); + + op->Dispatch(); + + return true; +} + +bool +DeallocPBackgroundLSSimpleRequestParent( + PBackgroundLSSimpleRequestParent* aActor) +{ + AssertIsOnBackgroundThread(); + + // Transfer ownership back from IPDL. + RefPtr actor = + dont_AddRef(static_cast(aActor)); + + return true; +} + +namespace localstorage { + +already_AddRefed +CreateQuotaClient() +{ + AssertIsOnBackgroundThread(); + MOZ_ASSERT(CachedNextGenLocalStorageEnabled()); + + RefPtr client = new QuotaClient(); + return client.forget(); +} + +} // namespace localstorage + +/******************************************************************************* + * WriteOptimizer + ******************************************************************************/ + +void +WriteOptimizer::AddItem(const nsString& aKey, + const nsString& aValue) +{ + AssertIsOnBackgroundThread(); + + WriteInfo* existingWriteInfo; + nsAutoPtr newWriteInfo; + if (mWriteInfos.Get(aKey, &existingWriteInfo) && + existingWriteInfo->GetType() == WriteInfo::RemoveItem) { + newWriteInfo = new UpdateItemInfo(aKey, aValue); + } else { + newWriteInfo = new AddItemInfo(aKey, aValue); + } + mWriteInfos.Put(aKey, newWriteInfo.forget()); +} + +void +WriteOptimizer::UpdateItem(const nsString& aKey, + const nsString& aValue) +{ + AssertIsOnBackgroundThread(); + + WriteInfo* existingWriteInfo; + nsAutoPtr newWriteInfo; + if (mWriteInfos.Get(aKey, &existingWriteInfo) && + existingWriteInfo->GetType() == WriteInfo::AddItem) { + newWriteInfo = new AddItemInfo(aKey, aValue); + } else { + newWriteInfo = new UpdateItemInfo(aKey, aValue); + } + mWriteInfos.Put(aKey, newWriteInfo.forget()); +} + +void +WriteOptimizer::RemoveItem(const nsString& aKey) +{ + AssertIsOnBackgroundThread(); + + WriteInfo* existingWriteInfo; + if (mWriteInfos.Get(aKey, &existingWriteInfo) && + existingWriteInfo->GetType() == WriteInfo::AddItem) { + mWriteInfos.Remove(aKey); + return; + } + + nsAutoPtr newWriteInfo(new RemoveItemInfo(aKey)); + mWriteInfos.Put(aKey, newWriteInfo.forget()); +} + +void +WriteOptimizer::Clear() +{ + AssertIsOnBackgroundThread(); + + mWriteInfos.Clear(); + + if (!mClearInfo) { + mClearInfo = new ClearInfo(); + } +} + +void +WriteOptimizer::ApplyWrites(nsTArray& aOrderedItems) +{ + AssertIsOnBackgroundThread(); + + if (mClearInfo) { + aOrderedItems.Clear(); + mClearInfo = nullptr; + } + + for (int32_t index = aOrderedItems.Length() - 1; + index >= 0; + index--) { + LSItemInfo& item = aOrderedItems[index]; + + if (auto entry = mWriteInfos.Lookup(item.key())) { + WriteInfo* writeInfo = entry.Data(); + + switch (writeInfo->GetType()) { + case WriteInfo::RemoveItem: + aOrderedItems.RemoveElementAt(index); + entry.Remove(); + break; + + case WriteInfo::UpdateItem: { + auto updateItemInfo = static_cast(writeInfo); + item.value() = updateItemInfo->GetValue(); + entry.Remove(); + break; + } + + case WriteInfo::AddItem: + break; + + default: + MOZ_CRASH("Bad type!"); + } + } + } + + for (auto iter = mWriteInfos.ConstIter(); !iter.Done(); iter.Next()) { + WriteInfo* writeInfo = iter.Data(); + + MOZ_ASSERT(writeInfo->GetType() == WriteInfo::AddItem); + + auto addItemInfo = static_cast(writeInfo); + + LSItemInfo* itemInfo = aOrderedItems.AppendElement(); + itemInfo->key() = addItemInfo->GetKey(); + itemInfo->value() = addItemInfo->GetValue(); + } + + mWriteInfos.Clear(); +} + +nsresult +WriteOptimizer::PerformWrites(Connection* aConnection, bool aShadowWrites) +{ + AssertIsOnConnectionThread(); + MOZ_ASSERT(aConnection); + + nsresult rv; + + if (mClearInfo) { + rv = mClearInfo->Perform(aConnection, aShadowWrites); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } + + for (auto iter = mWriteInfos.ConstIter(); !iter.Done(); iter.Next()) { + rv = iter.Data()->Perform(aConnection, aShadowWrites); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } + + return NS_OK; +} + +nsresult +WriteOptimizer:: +AddItemInfo::Perform(Connection* aConnection, bool aShadowWrites) +{ + AssertIsOnConnectionThread(); + MOZ_ASSERT(aConnection); + + Connection::CachedStatement stmt; + nsresult rv = aConnection->GetCachedStatement(NS_LITERAL_CSTRING( + "INSERT OR REPLACE INTO data (key, value) " + "VALUES(:key, :value)"), + &stmt); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = stmt->BindStringByName(NS_LITERAL_CSTRING("key"), mKey); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = stmt->BindStringByName(NS_LITERAL_CSTRING("value"), mValue); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = stmt->Execute(); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (!aShadowWrites) { + return NS_OK; + } + + rv = aConnection->GetCachedStatement(NS_LITERAL_CSTRING( + "INSERT OR REPLACE INTO shadow.webappsstore2 " + "(originAttributes, originKey, scope, key, value) " + "VALUES (:originAttributes, :originKey, :scope, :key, :value) "), + &stmt); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + ArchivedOriginScope* archivedOriginScope = + aConnection->GetArchivedOriginScope(); + + rv = archivedOriginScope->BindToStatement(stmt); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + nsCString scope = Scheme0Scope(archivedOriginScope->OriginSuffix(), + archivedOriginScope->OriginNoSuffix()); + + rv = stmt->BindUTF8StringByName(NS_LITERAL_CSTRING("scope"), + scope); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = stmt->BindStringByName(NS_LITERAL_CSTRING("key"), mKey); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = stmt->BindStringByName(NS_LITERAL_CSTRING("value"), mValue); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = stmt->Execute(); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + return NS_OK; +} + +nsresult +WriteOptimizer:: +RemoveItemInfo::Perform(Connection* aConnection, bool aShadowWrites) +{ + AssertIsOnConnectionThread(); + MOZ_ASSERT(aConnection); + + Connection::CachedStatement stmt; + nsresult rv = aConnection->GetCachedStatement(NS_LITERAL_CSTRING( + "DELETE FROM data " + "WHERE key = :key;"), + &stmt); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = stmt->BindStringByName(NS_LITERAL_CSTRING("key"), mKey); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = stmt->Execute(); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (!aShadowWrites) { + return NS_OK; + } + + rv = aConnection->GetCachedStatement(NS_LITERAL_CSTRING( + "DELETE FROM shadow.webappsstore2 " + "WHERE originAttributes = :originAttributes " + "AND originKey = :originKey " + "AND key = :key;"), + &stmt); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection->GetArchivedOriginScope()->BindToStatement(stmt); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = stmt->BindStringByName(NS_LITERAL_CSTRING("key"), mKey); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = stmt->Execute(); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + return NS_OK; +} + +nsresult +WriteOptimizer:: +ClearInfo::Perform(Connection* aConnection, bool aShadowWrites) +{ + AssertIsOnConnectionThread(); + MOZ_ASSERT(aConnection); + + Connection::CachedStatement stmt; + nsresult rv = aConnection->GetCachedStatement(NS_LITERAL_CSTRING( + "DELETE FROM data;"), + &stmt); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = stmt->Execute(); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (!aShadowWrites) { + return NS_OK; + } + + rv = aConnection->GetCachedStatement(NS_LITERAL_CSTRING( + "DELETE FROM shadow.webappsstore2 " + "WHERE originAttributes = :originAttributes " + "AND originKey = :originKey;"), + &stmt); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection->GetArchivedOriginScope()->BindToStatement(stmt); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = stmt->Execute(); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + return NS_OK; +} + +/******************************************************************************* + * DatastoreOperationBase + ******************************************************************************/ + +/******************************************************************************* + * ConnectionDatastoreOperationBase + ******************************************************************************/ + +ConnectionDatastoreOperationBase::ConnectionDatastoreOperationBase( + Connection* aConnection) + : mConnection(aConnection) +{ + MOZ_ASSERT(aConnection); +} + +ConnectionDatastoreOperationBase::~ConnectionDatastoreOperationBase() +{ + MOZ_ASSERT(!mConnection, + "ConnectionDatabaseOperationBase::Cleanup() was not called by a " + "subclass!"); +} + +void +ConnectionDatastoreOperationBase::Cleanup() +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(mConnection); + + mConnection = nullptr; + + NoteComplete(); +} + +void +ConnectionDatastoreOperationBase::OnSuccess() +{ + AssertIsOnOwningThread(); +} + +void +ConnectionDatastoreOperationBase::OnFailure(nsresult aResultCode) +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(NS_FAILED(aResultCode)); +} + +void +ConnectionDatastoreOperationBase::RunOnConnectionThread() +{ + AssertIsOnConnectionThread(); + MOZ_ASSERT(mConnection); + MOZ_ASSERT(NS_SUCCEEDED(ResultCode())); + + if (!MayProceedOnNonOwningThread()) { + SetFailureCode(NS_ERROR_FAILURE); + } else { + nsresult rv = mConnection->EnsureStorageConnection(); + if (NS_WARN_IF(NS_FAILED(rv))) { + SetFailureCode(rv); + } else { + MOZ_ASSERT(mConnection->StorageConnection()); + + rv = DoDatastoreWork(); + if (NS_FAILED(rv)) { + SetFailureCode(rv); + } + } + } + + MOZ_ALWAYS_SUCCEEDS(OwningEventTarget()->Dispatch(this, NS_DISPATCH_NORMAL)); +} + +void +ConnectionDatastoreOperationBase::RunOnOwningThread() +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(mConnection); + + if (!MayProceed()) { + MaybeSetFailureCode(NS_ERROR_FAILURE); + } else { + if (NS_SUCCEEDED(ResultCode())) { + OnSuccess(); + } else { + OnFailure(ResultCode()); + } + } + + Cleanup(); +} + +NS_IMETHODIMP +ConnectionDatastoreOperationBase::Run() +{ + if (IsOnConnectionThread()) { + RunOnConnectionThread(); + } else { + RunOnOwningThread(); + } + + return NS_OK; +} + +/******************************************************************************* + * Connection implementation + ******************************************************************************/ + +Connection::Connection(ConnectionThread* aConnectionThread, + const nsACString& aOrigin, + const nsAString& aFilePath, + nsAutoPtr&& aArchivedOriginScope) + : mConnectionThread(aConnectionThread) + , mArchivedOriginScope(std::move(aArchivedOriginScope)) + , mOrigin(aOrigin) + , mFilePath(aFilePath) + , mFlushScheduled(false) +#ifdef DEBUG + , mInUpdateBatch(false) +#endif +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(!aOrigin.IsEmpty()); + MOZ_ASSERT(!aFilePath.IsEmpty()); +} + +Connection::~Connection() +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(!mStorageConnection); + MOZ_ASSERT(!mCachedStatements.Count()); + MOZ_ASSERT(!mInUpdateBatch); + MOZ_ASSERT(!mFlushScheduled); +} + +void +Connection::Dispatch(ConnectionDatastoreOperationBase* aOp) +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(mConnectionThread); + + MOZ_ALWAYS_SUCCEEDS(mConnectionThread->mThread->Dispatch(aOp, + NS_DISPATCH_NORMAL)); +} + +void +Connection::Close(nsIRunnable* aCallback) +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(aCallback); + + if (mFlushScheduled) { + MOZ_ASSERT(mFlushTimer); + MOZ_ALWAYS_SUCCEEDS(mFlushTimer->Cancel()); + + Flush(); + + mFlushTimer = nullptr; + } + + RefPtr op = new CloseOp(this, aCallback); + + Dispatch(op); +} + +void +Connection::AddItem(const nsString& aKey, + const nsString& aValue) +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(mInUpdateBatch); + + mWriteOptimizer.AddItem(aKey, aValue); +} + +void +Connection::UpdateItem(const nsString& aKey, + const nsString& aValue) +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(mInUpdateBatch); + + mWriteOptimizer.UpdateItem(aKey, aValue); +} + +void +Connection::RemoveItem(const nsString& aKey) +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(mInUpdateBatch); + + mWriteOptimizer.RemoveItem(aKey); +} + +void +Connection::Clear() +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(mInUpdateBatch); + + mWriteOptimizer.Clear(); +} + +void +Connection::BeginUpdateBatch() +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(!mInUpdateBatch); + +#ifdef DEBUG + mInUpdateBatch = true; +#endif +} + +void +Connection::EndUpdateBatch() +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(mInUpdateBatch); + + if (mWriteOptimizer.HasWrites() && !mFlushScheduled) { + ScheduleFlush(); + } + +#ifdef DEBUG + mInUpdateBatch = false; +#endif +} + +nsresult +Connection::EnsureStorageConnection() +{ + AssertIsOnConnectionThread(); + + if (!mStorageConnection) { + nsCOMPtr storageConnection; + nsresult rv = + GetStorageConnection(mFilePath, + getter_AddRefs(storageConnection)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + mStorageConnection = storageConnection; + } + + return NS_OK; +} + +void +Connection::CloseStorageConnection() +{ + AssertIsOnConnectionThread(); + MOZ_ASSERT(mStorageConnection); + + mCachedStatements.Clear(); + + MOZ_ALWAYS_SUCCEEDS(mStorageConnection->Close()); + mStorageConnection = nullptr; +} + +nsresult +Connection::GetCachedStatement(const nsACString& aQuery, + CachedStatement* aCachedStatement) +{ + AssertIsOnConnectionThread(); + MOZ_ASSERT(!aQuery.IsEmpty()); + MOZ_ASSERT(aCachedStatement); + MOZ_ASSERT(mStorageConnection); + + nsCOMPtr stmt; + + if (!mCachedStatements.Get(aQuery, getter_AddRefs(stmt))) { + nsresult rv = + mStorageConnection->CreateStatement(aQuery, getter_AddRefs(stmt)); + if (NS_FAILED(rv)) { +#ifdef DEBUG + nsCString msg; + MOZ_ALWAYS_SUCCEEDS(mStorageConnection->GetLastErrorString(msg)); + + nsAutoCString error = + NS_LITERAL_CSTRING("The statement '") + aQuery + + NS_LITERAL_CSTRING("' failed to compile with the error message '") + + msg + NS_LITERAL_CSTRING("'."); + + NS_WARNING(error.get()); +#endif + return rv; + } + + mCachedStatements.Put(aQuery, stmt); + } + + aCachedStatement->Assign(this, stmt.forget()); + return NS_OK; +} + +void +Connection::ScheduleFlush() +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(mWriteOptimizer.HasWrites()); + MOZ_ASSERT(!mFlushScheduled); + + if (!mFlushTimer) { + mFlushTimer = NS_NewTimer(); + MOZ_ASSERT(mFlushTimer); + } + + MOZ_ALWAYS_SUCCEEDS( + mFlushTimer->InitWithNamedFuncCallback(FlushTimerCallback, + this, + kFlushTimeoutMs, + nsITimer::TYPE_ONE_SHOT, + "Connection::FlushTimerCallback")); + + mFlushScheduled = true; +} + +void +Connection::Flush() +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(mFlushScheduled); + + if (mWriteOptimizer.HasWrites()) { + RefPtr op = new FlushOp(this, std::move(mWriteOptimizer)); + + Dispatch(op); + } + + mFlushScheduled = false; +} + +// static +void +Connection::FlushTimerCallback(nsITimer* aTimer, void* aClosure) +{ + MOZ_ASSERT(aClosure); + + auto* self = static_cast(aClosure); + MOZ_ASSERT(self); + MOZ_ASSERT(self->mFlushScheduled); + + self->Flush(); +} + +Connection:: +CachedStatement::CachedStatement() +{ + AssertIsOnConnectionThread(); + + MOZ_COUNT_CTOR(Connection::CachedStatement); +} + +Connection:: +CachedStatement::~CachedStatement() +{ + AssertIsOnConnectionThread(); + + MOZ_COUNT_DTOR(Connection::CachedStatement); +} + +Connection:: +CachedStatement::operator mozIStorageStatement*() const +{ + AssertIsOnConnectionThread(); + + return mStatement; +} + +mozIStorageStatement* +Connection:: +CachedStatement::operator->() const +{ + AssertIsOnConnectionThread(); + MOZ_ASSERT(mStatement); + + return mStatement; +} + +void +Connection:: +CachedStatement::Assign(Connection* aConnection, + already_AddRefed aStatement) +{ + AssertIsOnConnectionThread(); + + mScoper.reset(); + + mStatement = aStatement; + + if (mStatement) { + mScoper.emplace(mStatement); + } +} + +Connection:: +FlushOp::FlushOp(Connection* aConnection, + WriteOptimizer&& aWriteOptimizer) + : ConnectionDatastoreOperationBase(aConnection) + , mQuotaClient(QuotaClient::GetInstance()) + , mWriteOptimizer(std::move(aWriteOptimizer)) + , mShadowWrites(gShadowWrites) +{ + MOZ_ASSERT(mQuotaClient); +} + +nsresult +Connection:: +FlushOp::DoDatastoreWork() +{ + AssertIsOnConnectionThread(); + MOZ_ASSERT(mConnection); + + QuotaManager* quotaManager = QuotaManager::Get(); + MOZ_ASSERT(quotaManager); + + nsCOMPtr storageConnection = + mConnection->StorageConnection(); + MOZ_ASSERT(storageConnection); + + nsresult rv; + + Maybe shadowDatabaseLock; + + if (mShadowWrites) { + MOZ_ASSERT(mQuotaClient); + + shadowDatabaseLock.emplace(mQuotaClient->ShadowDatabaseMutex()); + + rv = AttachShadowDatabase(quotaManager->GetBasePath(), storageConnection); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } + + CachedStatement stmt; + rv = mConnection->GetCachedStatement(NS_LITERAL_CSTRING("BEGIN IMMEDIATE;"), + &stmt); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = stmt->Execute(); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = mWriteOptimizer.PerformWrites(mConnection, mShadowWrites); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = mConnection->GetCachedStatement(NS_LITERAL_CSTRING("COMMIT;"), &stmt); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = stmt->Execute(); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (mShadowWrites) { + rv = DetachShadowDatabase(storageConnection); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } + + return NS_OK; +} + +nsresult +Connection:: +CloseOp::DoDatastoreWork() +{ + AssertIsOnConnectionThread(); + MOZ_ASSERT(mConnection); + + mConnection->CloseStorageConnection(); + + return NS_OK; +} + +void +Connection:: +CloseOp::Cleanup() +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(mConnection); + + mConnection->mConnectionThread->mConnections.Remove(mConnection->mOrigin); + + nsCOMPtr callback; + mCallback.swap(callback); + + callback->Run(); + + ConnectionDatastoreOperationBase::Cleanup(); +} + +/******************************************************************************* + * ConnectionThread implementation + ******************************************************************************/ + +ConnectionThread::ConnectionThread() +{ + AssertIsOnOwningThread(); + AssertIsOnBackgroundThread(); + + MOZ_ALWAYS_SUCCEEDS(NS_NewNamedThread("LS Thread", getter_AddRefs(mThread))); +} + +ConnectionThread::~ConnectionThread() +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(!mConnections.Count()); +} + +bool +ConnectionThread::IsOnConnectionThread() +{ + MOZ_ASSERT(mThread); + + bool current; + return NS_SUCCEEDED(mThread->IsOnCurrentThread(¤t)) && current; +} + +void +ConnectionThread::AssertIsOnConnectionThread() +{ + MOZ_ASSERT(IsOnConnectionThread()); +} + +already_AddRefed +ConnectionThread::CreateConnection( + const nsACString& aOrigin, + const nsAString& aFilePath, + nsAutoPtr&& aArchivedOriginScope) +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(!aOrigin.IsEmpty()); + MOZ_ASSERT(!mConnections.GetWeak(aOrigin)); + + RefPtr connection = + new Connection(this, aOrigin, aFilePath, std::move(aArchivedOriginScope)); + mConnections.Put(aOrigin, connection); + + return connection.forget(); +} + +void +ConnectionThread::Shutdown() +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(mThread); + + mThread->Shutdown(); +} + +/******************************************************************************* + * Datastore + ******************************************************************************/ + +Datastore::Datastore(const nsACString& aOrigin, + uint32_t aPrivateBrowsingId, + int64_t aUsage, + int64_t aSizeOfKeys, + int64_t aSizeOfItems, + already_AddRefed&& aDirectoryLock, + already_AddRefed&& aConnection, + already_AddRefed&& aQuotaObject, + nsDataHashtable& aValues, + nsTArray& aOrderedItems) + : mDirectoryLock(std::move(aDirectoryLock)) + , mConnection(std::move(aConnection)) + , mQuotaObject(std::move(aQuotaObject)) + , mOrigin(aOrigin) + , mPrivateBrowsingId(aPrivateBrowsingId) + , mUsage(aUsage) + , mUpdateBatchUsage(-1) + , mSizeOfKeys(aSizeOfKeys) + , mSizeOfItems(aSizeOfItems) + , mClosed(false) +#ifdef DEBUG + , mInUpdateBatch(false) +#endif +{ + AssertIsOnBackgroundThread(); + + mValues.SwapElements(aValues); + mOrderedItems.SwapElements(aOrderedItems); +} + +Datastore::~Datastore() +{ + AssertIsOnBackgroundThread(); + MOZ_ASSERT(mClosed); +} + +void +Datastore::Close() +{ + AssertIsOnBackgroundThread(); + MOZ_ASSERT(!mClosed); + MOZ_ASSERT(!mDatabases.Count()); + MOZ_ASSERT(mDirectoryLock); + + mClosed = true; + + if (IsPersistent()) { + MOZ_ASSERT(mConnection); + MOZ_ASSERT(mQuotaObject); + + // We can't release the directory lock and unregister itself from the + // hashtable until the connection is fully closed. + nsCOMPtr callback = + NewRunnableMethod("dom::Datastore::ConnectionClosedCallback", + this, + &Datastore::ConnectionClosedCallback); + mConnection->Close(callback); + } else { + MOZ_ASSERT(!mConnection); + MOZ_ASSERT(!mQuotaObject); + + // There's no connection, so it's safe to release the directory lock and + // unregister itself from the hashtable. + + mDirectoryLock = nullptr; + + CleanupMetadata(); + } +} + +void +Datastore::WaitForConnectionToComplete(nsIRunnable* aCallback) +{ + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aCallback); + MOZ_ASSERT(!mCompleteCallback); + MOZ_ASSERT(mClosed); + + mCompleteCallback = aCallback; +} + +void +Datastore::NoteLivePrepareDatastoreOp(PrepareDatastoreOp* aPrepareDatastoreOp) +{ + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aPrepareDatastoreOp); + MOZ_ASSERT(!mPrepareDatastoreOps.GetEntry(aPrepareDatastoreOp)); + MOZ_ASSERT(mDirectoryLock); + MOZ_ASSERT(!mClosed); + + mPrepareDatastoreOps.PutEntry(aPrepareDatastoreOp); +} + +void +Datastore::NoteFinishedPrepareDatastoreOp( + PrepareDatastoreOp* aPrepareDatastoreOp) +{ + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aPrepareDatastoreOp); + MOZ_ASSERT(mPrepareDatastoreOps.GetEntry(aPrepareDatastoreOp)); + MOZ_ASSERT(mDirectoryLock); + MOZ_ASSERT(!mClosed); + + mPrepareDatastoreOps.RemoveEntry(aPrepareDatastoreOp); + + MaybeClose(); +} + +void +Datastore::NoteLivePreparedDatastore(PreparedDatastore* aPreparedDatastore) +{ + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aPreparedDatastore); + MOZ_ASSERT(!mPreparedDatastores.GetEntry(aPreparedDatastore)); + MOZ_ASSERT(mDirectoryLock); + MOZ_ASSERT(!mClosed); + + mPreparedDatastores.PutEntry(aPreparedDatastore); +} + +void +Datastore::NoteFinishedPreparedDatastore(PreparedDatastore* aPreparedDatastore) +{ + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aPreparedDatastore); + MOZ_ASSERT(mPreparedDatastores.GetEntry(aPreparedDatastore)); + MOZ_ASSERT(mDirectoryLock); + MOZ_ASSERT(!mClosed); + + mPreparedDatastores.RemoveEntry(aPreparedDatastore); + + MaybeClose(); +} + +#ifdef DEBUG +bool +Datastore::HasLivePreparedDatastores() const +{ + AssertIsOnBackgroundThread(); + + return mPreparedDatastores.Count(); +} +#endif + +void +Datastore::NoteLiveDatabase(Database* aDatabase) +{ + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aDatabase); + MOZ_ASSERT(!mDatabases.GetEntry(aDatabase)); + MOZ_ASSERT(mDirectoryLock); + MOZ_ASSERT(!mClosed); + + mDatabases.PutEntry(aDatabase); +} + +void +Datastore::NoteFinishedDatabase(Database* aDatabase) +{ + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aDatabase); + MOZ_ASSERT(mDatabases.GetEntry(aDatabase)); + MOZ_ASSERT(!mActiveDatabases.GetEntry(aDatabase)); + MOZ_ASSERT(mDirectoryLock); + MOZ_ASSERT(!mClosed); + + mDatabases.RemoveEntry(aDatabase); + + MaybeClose(); +} + +#ifdef DEBUG +bool +Datastore::HasLiveDatabases() const +{ + AssertIsOnBackgroundThread(); + + return mDatabases.Count(); +} +#endif + +void +Datastore::NoteActiveDatabase(Database* aDatabase) +{ + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aDatabase); + MOZ_ASSERT(mDatabases.GetEntry(aDatabase)); + MOZ_ASSERT(!mActiveDatabases.GetEntry(aDatabase)); + MOZ_ASSERT(!mClosed); + + mActiveDatabases.PutEntry(aDatabase); +} + +void +Datastore::NoteInactiveDatabase(Database* aDatabase) +{ + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aDatabase); + MOZ_ASSERT(mDatabases.GetEntry(aDatabase)); + MOZ_ASSERT(mActiveDatabases.GetEntry(aDatabase)); + MOZ_ASSERT(!mClosed); + + mActiveDatabases.RemoveEntry(aDatabase); + + if (!mActiveDatabases.Count() && + mPendingUsageDeltas.Length()) { + int64_t finalDelta = 0; + + for (auto delta : mPendingUsageDeltas) { + finalDelta += delta; + } + + MOZ_ASSERT(finalDelta <= 0); + + if (finalDelta != 0) { + DebugOnly ok = UpdateUsage(finalDelta); + MOZ_ASSERT(ok); + } + + mPendingUsageDeltas.Clear(); + } +} + +void +Datastore::GetSnapshotInitInfo(nsTHashtable& aLoadedItems, + nsTArray& aItemInfos, + uint32_t& aTotalLength, + int64_t& aInitialUsage, + int64_t& aPeakUsage, + LSSnapshot::LoadState& aLoadState) +{ + AssertIsOnBackgroundThread(); + MOZ_ASSERT(!mClosed); + MOZ_ASSERT(!mInUpdateBatch); + +#ifdef DEBUG + int64_t sizeOfKeys = 0; + int64_t sizeOfItems = 0; + for (auto item : mOrderedItems) { + int64_t sizeOfKey = static_cast(item.key().Length()); + sizeOfKeys += sizeOfKey; + sizeOfItems += sizeOfKey + static_cast(item.value().Length()); + } + MOZ_ASSERT(mSizeOfKeys == sizeOfKeys); + MOZ_ASSERT(mSizeOfItems == sizeOfItems); +#endif + + int64_t size = 0; + if (mSizeOfKeys <= gSnapshotPrefill) { + if (mSizeOfItems <= gSnapshotPrefill) { + aItemInfos.AppendElements(mOrderedItems); + aLoadState = LSSnapshot::LoadState::AllOrderedItems; + } else { + nsString value; + for (auto item : mOrderedItems) { + if (!value.IsVoid()) { + value = item.value(); + + size += static_cast(item.key().Length()) + + static_cast(value.Length()); + + if (size <= gSnapshotPrefill) { + aLoadedItems.PutEntry(item.key()); + } else { + value.SetIsVoid(true); + } + } + + LSItemInfo* itemInfo = aItemInfos.AppendElement(); + itemInfo->key() = item.key(); + itemInfo->value() = value; + } + + aLoadState = LSSnapshot::LoadState::AllOrderedKeys; + } + } else { + for (auto iter = mValues.ConstIter(); !iter.Done(); iter.Next()) { + const nsAString& key = iter.Key(); + const nsString& value = iter.Data(); + + size += static_cast(key.Length()) + + static_cast(value.Length()); + + if (size > gSnapshotPrefill) { + break; + } + + aLoadedItems.PutEntry(key); + + LSItemInfo* itemInfo = aItemInfos.AppendElement(); + itemInfo->key() = iter.Key(); + itemInfo->value() = iter.Data(); + } + + MOZ_ASSERT(aItemInfos.Length() < mOrderedItems.Length()); + aLoadState = LSSnapshot::LoadState::Partial; + } + + aTotalLength = mValues.Count(); + + aInitialUsage = mUsage; + aPeakUsage = aInitialUsage; +} + +void +Datastore::GetItem(const nsString& aKey, nsString& aValue) const +{ + AssertIsOnBackgroundThread(); + MOZ_ASSERT(!mClosed); + + if (!mValues.Get(aKey, &aValue)) { + aValue.SetIsVoid(true); + } +} + +void +Datastore::GetKeys(nsTArray& aKeys) const +{ + AssertIsOnBackgroundThread(); + MOZ_ASSERT(!mClosed); + + for (auto item : mOrderedItems) { + aKeys.AppendElement(item.key()); + } +} + +void +Datastore::SetItem(Database* aDatabase, + const nsString& aDocumentURI, + const nsString& aKey, + const nsString& aOldValue, + const nsString& aValue) +{ + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aDatabase); + MOZ_ASSERT(!mClosed); + MOZ_ASSERT(mInUpdateBatch); + + nsString oldValue; + GetItem(aKey, oldValue); + + if (oldValue != aValue || oldValue.IsVoid() != aValue.IsVoid()) { + bool isNewItem = oldValue.IsVoid(); + + NotifySnapshots(aDatabase, aKey, oldValue, /* affectsOrder */ isNewItem); + + mValues.Put(aKey, aValue); + + if (isNewItem) { + mWriteOptimizer.AddItem(aKey, aValue); + + int64_t sizeOfKey = static_cast(aKey.Length()); + int64_t sizeOfItem = sizeOfKey + static_cast(aValue.Length()); + + mUpdateBatchUsage += sizeOfItem; + + mSizeOfKeys += sizeOfKey; + mSizeOfItems += sizeOfItem; + } else { + mWriteOptimizer.UpdateItem(aKey, aValue); + + int64_t delta = static_cast(aValue.Length()) - + static_cast(oldValue.Length()); + + mUpdateBatchUsage += delta; + + mSizeOfItems += delta; + } + + if (IsPersistent()) { + if (oldValue.IsVoid()) { + mConnection->AddItem(aKey, aValue); + } else { + mConnection->UpdateItem(aKey, aValue); + } + } + } + + NotifyObservers(aDatabase, aDocumentURI, aKey, aOldValue, aValue); +} + +void +Datastore::RemoveItem(Database* aDatabase, + const nsString& aDocumentURI, + const nsString& aKey, + const nsString& aOldValue) +{ + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aDatabase); + MOZ_ASSERT(!mClosed); + MOZ_ASSERT(mInUpdateBatch); + + nsString oldValue; + GetItem(aKey, oldValue); + + if (!oldValue.IsVoid()) { + NotifySnapshots(aDatabase, aKey, oldValue, /* aAffectsOrder */ true); + + mValues.Remove(aKey); + + mWriteOptimizer.RemoveItem(aKey); + + int64_t sizeOfKey = static_cast(aKey.Length()); + int64_t sizeOfItem = sizeOfKey + static_cast(oldValue.Length()); + + mUpdateBatchUsage -= sizeOfItem; + + mSizeOfKeys -= sizeOfKey; + mSizeOfItems -= sizeOfItem; + + if (IsPersistent()) { + mConnection->RemoveItem(aKey); + } + } + + NotifyObservers(aDatabase, aDocumentURI, aKey, aOldValue, VoidString()); +} + +void +Datastore::Clear(Database* aDatabase, + const nsString& aDocumentURI) +{ + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aDatabase); + MOZ_ASSERT(!mClosed); + MOZ_ASSERT(mInUpdateBatch); + + if (mValues.Count()) { + int64_t updateBatchUsage = mUpdateBatchUsage; + for (auto iter = mValues.ConstIter(); !iter.Done(); iter.Next()) { + const nsAString& key = iter.Key(); + const nsAString& value = iter.Data(); + + updateBatchUsage -= (static_cast(key.Length()) + + static_cast(value.Length())); + + NotifySnapshots(aDatabase, key, value, /* aAffectsOrder */ true); + } + + mValues.Clear(); + + mWriteOptimizer.Clear(); + + mUpdateBatchUsage = updateBatchUsage; + + mSizeOfKeys = 0; + mSizeOfItems = 0; + + if (IsPersistent()) { + mConnection->Clear(); + } + } + + NotifyObservers(aDatabase, + aDocumentURI, + VoidString(), + VoidString(), + VoidString()); +} + +void +Datastore::PrivateBrowsingClear() +{ + AssertIsOnBackgroundThread(); + MOZ_ASSERT(mPrivateBrowsingId); + MOZ_ASSERT(!mClosed); + MOZ_ASSERT(!mInUpdateBatch); + + if (mValues.Count()) { + MarkSnapshotsDirty(); + + mValues.Clear(); + + mOrderedItems.Clear(); + + DebugOnly ok = UpdateUsage(-mSizeOfItems); + MOZ_ASSERT(ok); + + mSizeOfKeys = 0; + mSizeOfItems = 0; + } +} + +void +Datastore::BeginUpdateBatch(int64_t aSnapshotInitialUsage) +{ + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aSnapshotInitialUsage >= 0); + MOZ_ASSERT(!mClosed); + MOZ_ASSERT(mUpdateBatchUsage == -1); + MOZ_ASSERT(!mInUpdateBatch); + + mUpdateBatchUsage = aSnapshotInitialUsage; + + if (IsPersistent()) { + mConnection->BeginUpdateBatch(); + } + +#ifdef DEBUG + mInUpdateBatch = true; +#endif +} + +int64_t +Datastore::EndUpdateBatch(int64_t aSnapshotPeakUsage) +{ + AssertIsOnBackgroundThread(); + MOZ_ASSERT(!mClosed); + MOZ_ASSERT(mInUpdateBatch); + + mWriteOptimizer.ApplyWrites(mOrderedItems); + + if (aSnapshotPeakUsage >= 0) { + int64_t delta = mUpdateBatchUsage - aSnapshotPeakUsage; + + if (mActiveDatabases.Count()) { + // We can't apply deltas while other databases are still active. + // The final delta must be zero or negative, but individual deltas can + // be positive. A positive delta can't be applied asynchronously since + // there's no way to fire the quota exceeded error event. + + mPendingUsageDeltas.AppendElement(delta); + } else { + MOZ_ASSERT(delta <= 0); + if (delta != 0) { + DebugOnly ok = UpdateUsage(delta); + MOZ_ASSERT(ok); + } + } + } + + int64_t result = mUpdateBatchUsage; + mUpdateBatchUsage = -1; + + if (IsPersistent()) { + mConnection->EndUpdateBatch(); + } + +#ifdef DEBUG + mInUpdateBatch = false; +#endif + + return result; +} + +int64_t +Datastore::RequestUpdateUsage(int64_t aRequestedSize, + int64_t aMinSize) +{ + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aRequestedSize > 0); + MOZ_ASSERT(aMinSize > 0); + + if (UpdateUsage(aRequestedSize)) { + return aRequestedSize; + } + + if (UpdateUsage(aMinSize)) { + return aMinSize; + } + + return 0; +} + +bool +Datastore::UpdateUsage(int64_t aDelta) +{ + AssertIsOnBackgroundThread(); + + // Check internal LocalStorage origin limit. + int64_t newUsage = mUsage + aDelta; + if (newUsage > gOriginLimitKB * 1024) { + return false; + } + + // Check QuotaManager limits (group and global limit). + if (IsPersistent()) { + MOZ_ASSERT(mQuotaObject); + + if (!mQuotaObject->MaybeUpdateSize(newUsage, /* aTruncate */ true)) { + return false; + } + + } + + // Quota checks passed, set new usage. + + mUsage = newUsage; + + if (IsPersistent()) { + RefPtr runnable = NS_NewRunnableFunction( + "Datastore::UpdateUsage", + [origin = mOrigin, newUsage] () { + MOZ_ASSERT(gUsages); + MOZ_ASSERT(gUsages->Contains(origin)); + gUsages->Put(origin, newUsage); + }); + + QuotaManager* quotaManager = QuotaManager::Get(); + MOZ_ASSERT(quotaManager); + + MOZ_ALWAYS_SUCCEEDS( + quotaManager->IOThread()->Dispatch(runnable, NS_DISPATCH_NORMAL)); + } + + return true; +} + +void +Datastore::MaybeClose() +{ + AssertIsOnBackgroundThread(); + + if (!mPrepareDatastoreOps.Count() && + !mPreparedDatastores.Count() && + !mDatabases.Count()) { + Close(); + } +} + +void +Datastore::ConnectionClosedCallback() +{ + AssertIsOnBackgroundThread(); + MOZ_ASSERT(mDirectoryLock); + MOZ_ASSERT(mConnection); + MOZ_ASSERT(mQuotaObject); + MOZ_ASSERT(mClosed); + + // Release the quota object first. + mQuotaObject = nullptr; + + // Now it's safe to release the directory lock and unregister itself from + // the hashtable. + + mDirectoryLock = nullptr; + mConnection = nullptr; + + CleanupMetadata(); + + if (mCompleteCallback) { + MOZ_ALWAYS_SUCCEEDS(NS_DispatchToCurrentThread(mCompleteCallback.forget())); + } +} + +void +Datastore::CleanupMetadata() +{ + AssertIsOnBackgroundThread(); + + MOZ_ASSERT(gDatastores); + MOZ_ASSERT(gDatastores->Get(mOrigin)); + gDatastores->Remove(mOrigin); + + if (!gDatastores->Count()) { + gDatastores = nullptr; + } +} + +void +Datastore::NotifySnapshots(Database* aDatabase, + const nsAString& aKey, + const nsAString& aOldValue, + bool aAffectsOrder) +{ + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aDatabase); + + for (auto iter = mDatabases.ConstIter(); !iter.Done(); iter.Next()) { + Database* database = iter.Get()->GetKey(); + if (database == aDatabase) { + continue; + } + + Snapshot* snapshot = database->GetSnapshot(); + if (snapshot) { + snapshot->SaveItem(aKey, aOldValue, aAffectsOrder); + } + } +} + +void +Datastore::MarkSnapshotsDirty() +{ + AssertIsOnBackgroundThread(); + + for (auto iter = mDatabases.ConstIter(); !iter.Done(); iter.Next()) { + Database* database = iter.Get()->GetKey(); + + Snapshot* snapshot = database->GetSnapshot(); + if (snapshot) { + snapshot->MarkDirty(); + } + } +} + +void +Datastore::NotifyObservers(Database* aDatabase, + const nsString& aDocumentURI, + const nsString& aKey, + const nsString& aOldValue, + const nsString& aNewValue) +{ + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aDatabase); + + if (!gObservers) { + return; + } + + nsTArray* array; + if (!gObservers->Get(mOrigin, &array)) { + return; + } + + MOZ_ASSERT(array); + + // We do not want to send information about events back to the content process + // that caused the change. + PBackgroundParent* databaseBackgroundActor = aDatabase->Manager(); + + for (Observer* observer : *array) { + if (observer->Manager() != databaseBackgroundActor) { + observer->Observe(aDatabase, aDocumentURI, aKey, aOldValue, aNewValue); + } + } +} + +/******************************************************************************* + * PreparedDatastore + ******************************************************************************/ + +void +PreparedDatastore::Destroy() +{ + AssertIsOnBackgroundThread(); + MOZ_ASSERT(gPreparedDatastores); + MOZ_ASSERT(gPreparedDatastores->Get(mDatastoreId)); + + nsAutoPtr preparedDatastore; + gPreparedDatastores->Remove(mDatastoreId, &preparedDatastore); + MOZ_ASSERT(preparedDatastore); +} + +// static +void +PreparedDatastore::TimerCallback(nsITimer* aTimer, void* aClosure) +{ + AssertIsOnBackgroundThread(); + + auto* self = static_cast(aClosure); + MOZ_ASSERT(self); + + self->Destroy(); +} + +/******************************************************************************* + * Database + ******************************************************************************/ + +Database::Database(const PrincipalInfo& aPrincipalInfo, + const Maybe& aContentParentId, + const nsACString& aOrigin, + uint32_t aPrivateBrowsingId) + : mSnapshot(nullptr) + , mPrincipalInfo(aPrincipalInfo) + , mContentParentId(aContentParentId) + , mOrigin(aOrigin) + , mPrivateBrowsingId(aPrivateBrowsingId) + , mAllowedToClose(false) + , mActorDestroyed(false) + , mRequestedAllowToClose(false) +#ifdef DEBUG + , mActorWasAlive(false) +#endif +{ + AssertIsOnBackgroundThread(); +} + +Database::~Database() +{ + MOZ_ASSERT_IF(mActorWasAlive, mAllowedToClose); + MOZ_ASSERT_IF(mActorWasAlive, mActorDestroyed); +} + +void +Database::SetActorAlive(Datastore* aDatastore) +{ + AssertIsOnBackgroundThread(); + MOZ_ASSERT(!mActorWasAlive); + MOZ_ASSERT(!mActorDestroyed); + +#ifdef DEBUG + mActorWasAlive = true; +#endif + + mDatastore = aDatastore; + + mDatastore->NoteLiveDatabase(this); + + if (!gLiveDatabases) { + gLiveDatabases = new LiveDatabaseArray(); + } + + gLiveDatabases->AppendElement(this); +} + +void +Database::RegisterSnapshot(Snapshot* aSnapshot) +{ + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aSnapshot); + MOZ_ASSERT(!mSnapshot); + MOZ_ASSERT(!mAllowedToClose); + + // Only one snapshot at a time is currently supported. + mSnapshot = aSnapshot; + + mDatastore->NoteActiveDatabase(this); +} + +void +Database::UnregisterSnapshot(Snapshot* aSnapshot) +{ + MOZ_ASSERT(aSnapshot); + MOZ_ASSERT(mSnapshot == aSnapshot); + + mSnapshot = nullptr; + + mDatastore->NoteInactiveDatabase(this); +} + +void +Database::RequestAllowToClose() +{ + AssertIsOnBackgroundThread(); + + if (mRequestedAllowToClose) { + return; + } + + mRequestedAllowToClose = true; + + // Send the RequestAllowToClose message to the child to avoid racing with the + // child actor. Except the case when the actor was already destroyed. + if (mActorDestroyed) { + MOZ_ASSERT(mAllowedToClose); + } else { + Unused << SendRequestAllowToClose(); + } +} + +void +Database::AllowToClose() +{ + AssertIsOnBackgroundThread(); + MOZ_ASSERT(!mAllowedToClose); + MOZ_ASSERT(mDatastore); + + mAllowedToClose = true; + + mDatastore->NoteFinishedDatabase(this); + + mDatastore = nullptr; + + MOZ_ASSERT(gLiveDatabases); + gLiveDatabases->RemoveElement(this); + + if (gLiveDatabases->IsEmpty()) { + gLiveDatabases = nullptr; + } +} + +void +Database::ActorDestroy(ActorDestroyReason aWhy) +{ + AssertIsOnBackgroundThread(); + MOZ_ASSERT(!mActorDestroyed); + + mActorDestroyed = true; + + if (!mAllowedToClose) { + AllowToClose(); + } +} + +mozilla::ipc::IPCResult +Database::RecvDeleteMe() +{ + AssertIsOnBackgroundThread(); + MOZ_ASSERT(!mActorDestroyed); + + IProtocol* mgr = Manager(); + if (!PBackgroundLSDatabaseParent::Send__delete__(this)) { + return IPC_FAIL_NO_REASON(mgr); + } + return IPC_OK(); +} + +mozilla::ipc::IPCResult +Database::RecvAllowToClose() +{ + AssertIsOnBackgroundThread(); + + if (NS_WARN_IF(mAllowedToClose)) { + ASSERT_UNLESS_FUZZING(); + return IPC_FAIL_NO_REASON(this); + } + + AllowToClose(); + + return IPC_OK(); +} + +PBackgroundLSSnapshotParent* +Database::AllocPBackgroundLSSnapshotParent(const nsString& aDocumentURI, + const bool& aIncreasePeakUsage, + const int64_t& aRequestedSize, + const int64_t& aMinSize, + LSSnapshotInitInfo* aInitInfo) +{ + AssertIsOnBackgroundThread(); + + if (NS_WARN_IF(aIncreasePeakUsage && aRequestedSize <= 0)) { + ASSERT_UNLESS_FUZZING(); + return nullptr; + } + + if (NS_WARN_IF(aIncreasePeakUsage && aMinSize <= 0)) { + ASSERT_UNLESS_FUZZING(); + return nullptr; + } + + if (NS_WARN_IF(mAllowedToClose)) { + ASSERT_UNLESS_FUZZING(); + return nullptr; + } + + RefPtr snapshot = new Snapshot(this, aDocumentURI); + + // Transfer ownership to IPDL. + return snapshot.forget().take(); +} + +mozilla::ipc::IPCResult +Database::RecvPBackgroundLSSnapshotConstructor( + PBackgroundLSSnapshotParent* aActor, + const nsString& aDocumentURI, + const bool& aIncreasePeakUsage, + const int64_t& aRequestedSize, + const int64_t& aMinSize, + LSSnapshotInitInfo* aInitInfo) +{ + AssertIsOnBackgroundThread(); + MOZ_ASSERT_IF(aIncreasePeakUsage, aRequestedSize > 0); + MOZ_ASSERT_IF(aIncreasePeakUsage, aMinSize > 0); + MOZ_ASSERT(aInitInfo); + MOZ_ASSERT(!mAllowedToClose); + + auto* snapshot = static_cast(aActor); + + // TODO: This can be optimized depending on which operation triggers snapshot + // creation. For example clear() doesn't need to receive items at all. + nsTHashtable loadedItems; + nsTArray itemInfos; + uint32_t totalLength; + int64_t initialUsage; + int64_t peakUsage; + LSSnapshot::LoadState loadState; + mDatastore->GetSnapshotInitInfo(loadedItems, + itemInfos, + totalLength, + initialUsage, + peakUsage, + loadState); + + if (aIncreasePeakUsage) { + int64_t size = mDatastore->RequestUpdateUsage(aRequestedSize, aMinSize); + peakUsage += size; + } + + snapshot->Init(loadedItems, totalLength, initialUsage, peakUsage, loadState); + + RegisterSnapshot(snapshot); + + aInitInfo->itemInfos() = std::move(itemInfos); + aInitInfo->totalLength() = totalLength; + aInitInfo->initialUsage() = initialUsage; + aInitInfo->peakUsage() = peakUsage; + aInitInfo->loadState() = loadState; + + return IPC_OK(); +} + +bool +Database::DeallocPBackgroundLSSnapshotParent( + PBackgroundLSSnapshotParent* aActor) +{ + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aActor); + + // Transfer ownership back from IPDL. + RefPtr actor = dont_AddRef(static_cast(aActor)); + + return true; +} + +/******************************************************************************* + * Snapshot + ******************************************************************************/ + +Snapshot::Snapshot(Database* aDatabase, + const nsAString& aDocumentURI) + : mDatabase(aDatabase) + , mDatastore(aDatabase->GetDatastore()) + , mDocumentURI(aDocumentURI) + , mTotalLength(0) + , mUsage(-1) + , mPeakUsage(-1) + , mSavedKeys(false) + , mActorDestroyed(false) + , mFinishReceived(false) + , mLoadedReceived(false) + , mLoadedAllItems(false) + , mLoadKeysReceived(false) + , mSentMarkDirty(false) +{ + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aDatabase); +} + +Snapshot::~Snapshot() +{ + MOZ_ASSERT(mActorDestroyed); + MOZ_ASSERT(mFinishReceived); +} + +void +Snapshot::SaveItem(const nsAString& aKey, + const nsAString& aOldValue, + bool aAffectsOrder) +{ + AssertIsOnBackgroundThread(); + + MarkDirty(); + + if (mLoadedAllItems) { + return; + } + + if (!mLoadedItems.GetEntry(aKey) && !mUnknownItems.GetEntry(aKey)) { + nsString oldValue(aOldValue); + mValues.LookupForAdd(aKey).OrInsert([oldValue]() { + return oldValue; + }); + } + + if (aAffectsOrder && !mSavedKeys && !mLoadKeysReceived) { + mDatastore->GetKeys(mKeys); + mSavedKeys = true; + } +} + +void +Snapshot::MarkDirty() +{ + AssertIsOnBackgroundThread(); + + if (!mSentMarkDirty) { + Unused << SendMarkDirty(); + mSentMarkDirty = true; + } +} + +void +Snapshot::Finish() +{ + AssertIsOnBackgroundThread(); + MOZ_ASSERT(mDatabase); + MOZ_ASSERT(mDatastore); + MOZ_ASSERT(!mFinishReceived); + + mDatastore->BeginUpdateBatch(mUsage); + + mDatastore->EndUpdateBatch(mPeakUsage); + + mDatabase->UnregisterSnapshot(this); + + mFinishReceived = true; +} + +void +Snapshot::ActorDestroy(ActorDestroyReason aWhy) +{ + AssertIsOnBackgroundThread(); + MOZ_ASSERT(!mActorDestroyed); + + mActorDestroyed = true; + + if (!mFinishReceived) { + Finish(); + } +} + +mozilla::ipc::IPCResult +Snapshot::RecvDeleteMe() +{ + AssertIsOnBackgroundThread(); + MOZ_ASSERT(!mActorDestroyed); + + IProtocol* mgr = Manager(); + if (!PBackgroundLSSnapshotParent::Send__delete__(this)) { + return IPC_FAIL_NO_REASON(mgr); + } + return IPC_OK(); +} + +mozilla::ipc::IPCResult +Snapshot::RecvCheckpoint(nsTArray&& aWriteInfos) +{ + AssertIsOnBackgroundThread(); + MOZ_ASSERT(mUsage >= 0); + MOZ_ASSERT(mPeakUsage >= mUsage); + + if (NS_WARN_IF(aWriteInfos.IsEmpty())) { + ASSERT_UNLESS_FUZZING(); + return IPC_FAIL_NO_REASON(this); + } + + mDatastore->BeginUpdateBatch(mUsage); + + for (uint32_t index = 0; index < aWriteInfos.Length(); index++) { + const LSWriteInfo& writeInfo = aWriteInfos[index]; + switch (writeInfo.type()) { + case LSWriteInfo::TLSSetItemInfo: { + const LSSetItemInfo& info = writeInfo.get_LSSetItemInfo(); + + mDatastore->SetItem(mDatabase, + mDocumentURI, + info.key(), + info.oldValue(), + info.value()); + + break; + } + + case LSWriteInfo::TLSRemoveItemInfo: { + const LSRemoveItemInfo& info = writeInfo.get_LSRemoveItemInfo(); + + mDatastore->RemoveItem(mDatabase, + mDocumentURI, + info.key(), + info.oldValue()); + + break; + } + + case LSWriteInfo::TLSClearInfo: { + mDatastore->Clear(mDatabase, mDocumentURI); + + break; + } + + default: + MOZ_CRASH("Should never get here!"); + } + } + + mUsage = mDatastore->EndUpdateBatch(-1); + + return IPC_OK(); +} + +mozilla::ipc::IPCResult +Snapshot::RecvFinish() +{ + AssertIsOnBackgroundThread(); + + if (NS_WARN_IF(mFinishReceived)) { + ASSERT_UNLESS_FUZZING(); + return IPC_FAIL_NO_REASON(this); + } + + Finish(); + + return IPC_OK(); +} + +mozilla::ipc::IPCResult +Snapshot::RecvLoaded() +{ + AssertIsOnBackgroundThread(); + + if (NS_WARN_IF(mFinishReceived)) { + ASSERT_UNLESS_FUZZING(); + return IPC_FAIL_NO_REASON(this); + } + + if (NS_WARN_IF(mLoadedReceived)) { + ASSERT_UNLESS_FUZZING(); + return IPC_FAIL_NO_REASON(this); + } + + if (NS_WARN_IF(mLoadedAllItems)) { + ASSERT_UNLESS_FUZZING(); + return IPC_FAIL_NO_REASON(this); + } + + if (NS_WARN_IF(mLoadKeysReceived)) { + ASSERT_UNLESS_FUZZING(); + return IPC_FAIL_NO_REASON(this); + } + + mLoadedReceived = true; + + mLoadedItems.Clear(); + mUnknownItems.Clear(); + mValues.Clear(); + mKeys.Clear(); + mLoadedAllItems = true; + mLoadKeysReceived = true; + + return IPC_OK(); +} + +mozilla::ipc::IPCResult +Snapshot::RecvLoadItem(const nsString& aKey, + nsString* aValue) +{ + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aValue); + MOZ_ASSERT(mDatastore); + + if (NS_WARN_IF(mFinishReceived)) { + ASSERT_UNLESS_FUZZING(); + return IPC_FAIL_NO_REASON(this); + } + + if (NS_WARN_IF(mLoadedReceived)) { + ASSERT_UNLESS_FUZZING(); + return IPC_FAIL_NO_REASON(this); + } + + if (NS_WARN_IF(mLoadedAllItems)) { + ASSERT_UNLESS_FUZZING(); + return IPC_FAIL_NO_REASON(this); + } + + if (mLoadedItems.GetEntry(aKey) || mUnknownItems.GetEntry(aKey)) { + ASSERT_UNLESS_FUZZING(); + return IPC_FAIL_NO_REASON(this); + } + + if (auto entry = mValues.Lookup(aKey)) { + *aValue = entry.Data(); + entry.Remove(); + } else { + mDatastore->GetItem(aKey, *aValue); + } + + if (aValue->IsVoid()) { + mUnknownItems.PutEntry(aKey); + } else { + mLoadedItems.PutEntry(aKey); + + if (mLoadedItems.Count() == mTotalLength) { + mLoadedItems.Clear(); + mUnknownItems.Clear(); +#ifdef DEBUG + for (auto iter = mValues.ConstIter(); !iter.Done(); iter.Next()) { + MOZ_ASSERT(iter.Data().IsVoid()); + } +#endif + mValues.Clear(); + mLoadedAllItems = true; + } + } + + return IPC_OK(); +} + +mozilla::ipc::IPCResult +Snapshot::RecvLoadKeys(nsTArray* aKeys) +{ + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aKeys); + MOZ_ASSERT(mDatastore); + + if (NS_WARN_IF(mFinishReceived)) { + ASSERT_UNLESS_FUZZING(); + return IPC_FAIL_NO_REASON(this); + } + + if (NS_WARN_IF(mLoadedReceived)) { + ASSERT_UNLESS_FUZZING(); + return IPC_FAIL_NO_REASON(this); + } + + if (NS_WARN_IF(mLoadKeysReceived)) { + ASSERT_UNLESS_FUZZING(); + return IPC_FAIL_NO_REASON(this); + } + + mLoadKeysReceived = true; + + if (mSavedKeys) { + aKeys->AppendElements(std::move(mKeys)); + } else { + mDatastore->GetKeys(*aKeys); + } + + return IPC_OK(); +} + +mozilla::ipc::IPCResult +Snapshot::RecvIncreasePeakUsage(const int64_t& aRequestedSize, + const int64_t& aMinSize, + int64_t* aSize) +{ + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aSize); + + if (NS_WARN_IF(aRequestedSize <= 0)) { + ASSERT_UNLESS_FUZZING(); + return IPC_FAIL_NO_REASON(this); + } + + if (NS_WARN_IF(aMinSize <= 0)) { + ASSERT_UNLESS_FUZZING(); + return IPC_FAIL_NO_REASON(this); + } + + if (NS_WARN_IF(mFinishReceived)) { + ASSERT_UNLESS_FUZZING(); + return IPC_FAIL_NO_REASON(this); + } + + int64_t size = mDatastore->RequestUpdateUsage(aRequestedSize, aMinSize); + + mPeakUsage += size; + + *aSize = size; + + return IPC_OK(); +} + +mozilla::ipc::IPCResult +Snapshot::RecvPing() +{ + AssertIsOnBackgroundThread(); + + // Do nothing here. This is purely a sync message allowing the child to + // confirm that the actor has received previous async message. + + return IPC_OK(); +} + +/******************************************************************************* + * Observer + ******************************************************************************/ + +Observer::Observer(const nsACString& aOrigin) + : mOrigin(aOrigin) + , mActorDestroyed(false) +{ + AssertIsOnBackgroundThread(); +} + +Observer::~Observer() +{ + MOZ_ASSERT(mActorDestroyed); +} + +void +Observer::Observe(Database* aDatabase, + const nsString& aDocumentURI, + const nsString& aKey, + const nsString& aOldValue, + const nsString& aNewValue) +{ + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aDatabase); + + Unused << SendObserve(aDatabase->GetPrincipalInfo(), + aDatabase->PrivateBrowsingId(), + aDocumentURI, + aKey, + aOldValue, + aNewValue); +} + +void +Observer::ActorDestroy(ActorDestroyReason aWhy) +{ + AssertIsOnBackgroundThread(); + MOZ_ASSERT(!mActorDestroyed); + + mActorDestroyed = true; + + MOZ_ASSERT(gObservers); + + nsTArray* array; + gObservers->Get(mOrigin, &array); + MOZ_ASSERT(array); + + array->RemoveElement(this); + + if (array->IsEmpty()) { + gObservers->Remove(mOrigin); + } + + if (!gObservers->Count()) { + gObservers = nullptr; + } +} + +mozilla::ipc::IPCResult +Observer::RecvDeleteMe() +{ + AssertIsOnBackgroundThread(); + MOZ_ASSERT(!mActorDestroyed); + + IProtocol* mgr = Manager(); + if (!PBackgroundLSObserverParent::Send__delete__(this)) { + return IPC_FAIL_NO_REASON(mgr); + } + return IPC_OK(); +} + +/******************************************************************************* + * LSRequestBase + ******************************************************************************/ + +LSRequestBase::LSRequestBase(nsIEventTarget* aMainEventTarget) + : mMainEventTarget(aMainEventTarget) + , mState(State::Initial) +{ +} + +LSRequestBase::~LSRequestBase() +{ + MOZ_ASSERT_IF(MayProceedOnNonOwningThread(), + mState == State::Initial || mState == State::Completed); +} + +void +LSRequestBase::Dispatch() +{ + AssertIsOnOwningThread(); + + mState = State::Opening; + + if (mMainEventTarget) { + MOZ_ALWAYS_SUCCEEDS(mMainEventTarget->Dispatch(this, NS_DISPATCH_NORMAL)); + } else { + MOZ_ALWAYS_SUCCEEDS(NS_DispatchToMainThread(this)); + } +} + +nsresult +LSRequestBase::NestedRun() +{ + return NS_OK; +} + +void +LSRequestBase::SendReadyMessage() +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(mState == State::SendingReadyMessage); + + if (NS_WARN_IF(QuotaClient::IsShuttingDownOnBackgroundThread()) || + !MayProceed()) { + MaybeSetFailureCode(NS_ERROR_FAILURE); + } + + if (MayProceed()) { + Unused << SendReady(); + + mState = State::WaitingForFinish; + } else { + Cleanup(); + + mState = State::Completed; + } +} + +void +LSRequestBase::SendResults() +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(mState == State::SendingResults); + + if (NS_WARN_IF(QuotaClient::IsShuttingDownOnBackgroundThread()) || + !MayProceed()) { + MaybeSetFailureCode(NS_ERROR_FAILURE); + } + + if (MayProceed()) { + LSRequestResponse response; + + if (NS_SUCCEEDED(ResultCode())) { + GetResponse(response); + } else { + response = ResultCode(); + } + + Unused << + PBackgroundLSRequestParent::Send__delete__(this, response); + } + + Cleanup(); + + mState = State::Completed; +} + +NS_IMETHODIMP +LSRequestBase::Run() +{ + nsresult rv; + + switch (mState) { + case State::Opening: + rv = Open(); + break; + + case State::Nesting: + rv = NestedRun(); + break; + + case State::SendingReadyMessage: + SendReadyMessage(); + return NS_OK; + + case State::SendingResults: + SendResults(); + return NS_OK; + + default: + MOZ_CRASH("Bad state!"); + } + + if (NS_WARN_IF(NS_FAILED(rv)) && mState != State::SendingReadyMessage) { + MaybeSetFailureCode(rv); + + // Must set mState before dispatching otherwise we will race with the owning + // thread. + mState = State::SendingReadyMessage; + + if (IsOnOwningThread()) { + SendReadyMessage(); + } else { + MOZ_ALWAYS_SUCCEEDS( + OwningEventTarget()->Dispatch(this, NS_DISPATCH_NORMAL)); + } + } + + return NS_OK; +} + +void +LSRequestBase::ActorDestroy(ActorDestroyReason aWhy) +{ + AssertIsOnOwningThread(); + + NoteComplete(); +} + +mozilla::ipc::IPCResult +LSRequestBase::RecvCancel() +{ + AssertIsOnOwningThread(); + + IProtocol* mgr = Manager(); + if (!PBackgroundLSRequestParent::Send__delete__(this, NS_ERROR_FAILURE)) { + return IPC_FAIL_NO_REASON(mgr); + } + + return IPC_OK(); +} + +mozilla::ipc::IPCResult +LSRequestBase::RecvFinish() +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(mState == State::WaitingForFinish); + + mState = State::SendingResults; + + // This LSRequestBase can only be held alive by the IPDL. Run() can end up + // with clearing that last reference. So we need to add a self reference here. + RefPtr kungFuDeathGrip = this; + + MOZ_ALWAYS_SUCCEEDS(this->Run()); + + return IPC_OK(); +} + +/******************************************************************************* + * PrepareDatastoreOp + ******************************************************************************/ + +PrepareDatastoreOp::PrepareDatastoreOp( + nsIEventTarget* aMainEventTarget, + already_AddRefed aContentParent, + const LSRequestParams& aParams) + : LSRequestBase(aMainEventTarget) + , mMainEventTarget(aMainEventTarget) + , mContentParent(std::move(aContentParent)) + , mLoadDataOp(nullptr) + , mParams(aParams.get_LSRequestPrepareDatastoreParams()) + , mPrivateBrowsingId(0) + , mUsage(0) + , mSizeOfKeys(0) + , mSizeOfItems(0) + , mNestedState(NestedState::BeforeNesting) + , mDatabaseNotAvailable(false) + , mRequestedDirectoryLock(false) + , mInvalidated(false) +#ifdef DEBUG + , mDEBUGUsage(0) +#endif +{ + MOZ_ASSERT(aParams.type() == + LSRequestParams::TLSRequestPrepareDatastoreParams); + + if (mContentParent) { + mContentParentId = Some(mContentParent->ChildID()); + } +} + +PrepareDatastoreOp::~PrepareDatastoreOp() +{ + MOZ_ASSERT(!mDirectoryLock); + MOZ_ASSERT_IF(MayProceedOnNonOwningThread(), + mState == State::Initial || mState == State::Completed); + MOZ_ASSERT(!mLoadDataOp); +} + +nsresult +PrepareDatastoreOp::Open() +{ + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(mState == State::Opening); + MOZ_ASSERT(mNestedState == NestedState::BeforeNesting); + + // Swap this to the stack now to ensure that we release it on this thread. + RefPtr contentParent; + mContentParent.swap(contentParent); + + if (NS_WARN_IF(QuotaClient::IsShuttingDownOnNonBackgroundThread()) || + !MayProceedOnNonOwningThread()) { + return NS_ERROR_FAILURE; + } + + const PrincipalInfo& principalInfo = mParams.principalInfo(); + + if (principalInfo.type() == PrincipalInfo::TSystemPrincipalInfo) { + QuotaManager::GetInfoForChrome(&mSuffix, &mGroup, &mOrigin); + } else { + MOZ_ASSERT(principalInfo.type() == PrincipalInfo::TContentPrincipalInfo); + + nsresult rv; + nsCOMPtr principal = + PrincipalInfoToPrincipal(principalInfo, &rv); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = QuotaManager::GetInfoFromPrincipal(principal, + &mSuffix, + &mGroup, + &mMainThreadOrigin); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = principal->GetPrivateBrowsingId(&mPrivateBrowsingId); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + mArchivedOriginScope = ArchivedOriginScope::CreateFromOrigin(principal); + if (NS_WARN_IF(!mArchivedOriginScope)) { + return NS_ERROR_FAILURE; + } + } + + // This service has to be started on the main thread currently. + nsCOMPtr ss; + if (NS_WARN_IF(!(ss = do_GetService(MOZ_STORAGE_SERVICE_CONTRACTID)))) { + return NS_ERROR_FAILURE; + } + + nsresult rv = QuotaClient::RegisterObservers(OwningEventTarget()); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + mState = State::Nesting; + mNestedState = NestedState::CheckExistingOperations; + + MOZ_ALWAYS_SUCCEEDS(OwningEventTarget()->Dispatch(this, NS_DISPATCH_NORMAL)); + + return NS_OK; +} + +nsresult +PrepareDatastoreOp::CheckExistingOperations() +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(mState == State::Nesting); + MOZ_ASSERT(mNestedState == NestedState::CheckExistingOperations); + MOZ_ASSERT(gPrepareDatastoreOps); + + if (NS_WARN_IF(QuotaClient::IsShuttingDownOnBackgroundThread()) || + !MayProceed()) { + return NS_ERROR_FAILURE; + } + + // Normally it's safe to access member variables without a mutex because even + // though we hop between threads, the variables are never accessed by multiple + // threads at the same time. + // However, the methods OriginIsKnown and Origin can be called at any time. + // So we have to make sure the member variable is set on the same thread as + // those methods are called. + mOrigin = mMainThreadOrigin; + + MOZ_ASSERT(!mOrigin.IsEmpty()); + + mNestedState = NestedState::CheckClosingDatastore; + + // See if this PrepareDatastoreOp needs to wait. + bool foundThis = false; + for (uint32_t index = gPrepareDatastoreOps->Length(); index > 0; index--) { + PrepareDatastoreOp* existingOp = (*gPrepareDatastoreOps)[index - 1]; + + if (existingOp == this) { + foundThis = true; + continue; + } + + if (foundThis && existingOp->Origin() == mOrigin) { + // Only one op can be delayed. + MOZ_ASSERT(!existingOp->mDelayedOp); + existingOp->mDelayedOp = this; + + return NS_OK; + } + } + + nsresult rv = CheckClosingDatastoreInternal(); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + return NS_OK; +} + +nsresult +PrepareDatastoreOp::CheckClosingDatastore() +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(mState == State::Nesting); + MOZ_ASSERT(mNestedState == NestedState::CheckClosingDatastore); + + if (NS_WARN_IF(QuotaClient::IsShuttingDownOnBackgroundThread()) || + !MayProceed()) { + return NS_ERROR_FAILURE; + } + + nsresult rv = CheckClosingDatastoreInternal(); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + return NS_OK; +} + +nsresult +PrepareDatastoreOp::CheckClosingDatastoreInternal() +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(mState == State::Nesting); + MOZ_ASSERT(mNestedState == NestedState::CheckClosingDatastore); + MOZ_ASSERT(!QuotaClient::IsShuttingDownOnBackgroundThread()); + MOZ_ASSERT(MayProceed()); + + mNestedState = NestedState::PreparationPending; + + RefPtr datastore; + if (gDatastores && + (datastore = gDatastores->Get(mOrigin)) && + datastore->IsClosed()) { + datastore->WaitForConnectionToComplete(this); + + return NS_OK; + } + + nsresult rv = BeginDatastorePreparationInternal(); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + return NS_OK; +} + +nsresult +PrepareDatastoreOp::BeginDatastorePreparation() +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(mState == State::Nesting); + MOZ_ASSERT(mNestedState == NestedState::PreparationPending); + + if (NS_WARN_IF(QuotaClient::IsShuttingDownOnBackgroundThread()) || + !MayProceed()) { + return NS_ERROR_FAILURE; + } + + nsresult rv = BeginDatastorePreparationInternal(); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + return NS_OK; +} + +nsresult +PrepareDatastoreOp::BeginDatastorePreparationInternal() +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(mState == State::Nesting); + MOZ_ASSERT(mNestedState == NestedState::PreparationPending); + MOZ_ASSERT(!QuotaClient::IsShuttingDownOnBackgroundThread()); + MOZ_ASSERT(MayProceed()); + + if (gDatastores && (mDatastore = gDatastores->Get(mOrigin))) { + MOZ_ASSERT(!mDatastore->IsClosed()); + + mDatastore->NoteLivePrepareDatastoreOp(this); + + FinishNesting(); + + return NS_OK; + } + + if (QuotaManager::Get()) { + nsresult rv = OpenDirectory(); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + return NS_OK; + } + + mNestedState = NestedState::QuotaManagerPending; + QuotaManager::GetOrCreate(this, mMainEventTarget); + + return NS_OK; +} + +nsresult +PrepareDatastoreOp::QuotaManagerOpen() +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(mState == State::Nesting); + MOZ_ASSERT(mNestedState == NestedState::QuotaManagerPending); + + if (NS_WARN_IF(QuotaClient::IsShuttingDownOnBackgroundThread()) || + !MayProceed()) { + return NS_ERROR_FAILURE; + } + + if (NS_WARN_IF(!QuotaManager::Get())) { + return NS_ERROR_FAILURE; + } + + nsresult rv = OpenDirectory(); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + return NS_OK; +} + +nsresult +PrepareDatastoreOp::OpenDirectory() +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(mState == State::Nesting); + MOZ_ASSERT(mNestedState == NestedState::PreparationPending || + mNestedState == NestedState::QuotaManagerPending); + MOZ_ASSERT(!mOrigin.IsEmpty()); + MOZ_ASSERT(!mDirectoryLock); + MOZ_ASSERT(!QuotaClient::IsShuttingDownOnBackgroundThread()); + MOZ_ASSERT(MayProceed()); + MOZ_ASSERT(QuotaManager::Get()); + + mNestedState = NestedState::DirectoryOpenPending; + QuotaManager::Get()->OpenDirectory(PERSISTENCE_TYPE_DEFAULT, + mGroup, + mOrigin, + mozilla::dom::quota::Client::LS, + /* aExclusive */ false, + this); + + mRequestedDirectoryLock = true; + + return NS_OK; +} + +void +PrepareDatastoreOp::SendToIOThread() +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(mState == State::Nesting); + MOZ_ASSERT(mNestedState == NestedState::DirectoryOpenPending); + MOZ_ASSERT(!QuotaClient::IsShuttingDownOnBackgroundThread()); + MOZ_ASSERT(MayProceed()); + + // Skip all disk related stuff and transition to SendingReadyMessage if we + // are preparing a datastore for private browsing. + // Note that we do use a directory lock for private browsing even though we + // don't do any stuff on disk. The thing is that without a directory lock, + // quota manager wouldn't call AbortOperations for our private browsing + // origin when a clear origin operation is requested. AbortOperations + // requests all databases to close and the datastore is destroyed in the end. + // Any following LocalStorage API call will trigger preparation of a new + // (empty) datastore. + if (mPrivateBrowsingId) { + FinishNesting(); + + return; + } + + QuotaManager* quotaManager = QuotaManager::Get(); + MOZ_ASSERT(quotaManager); + + // Must set this before dispatching otherwise we will race with the IO thread. + mNestedState = NestedState::DatabaseWorkOpen; + + MOZ_ALWAYS_SUCCEEDS( + quotaManager->IOThread()->Dispatch(this, NS_DISPATCH_NORMAL)); +} + +nsresult +PrepareDatastoreOp::DatabaseWork() +{ + AssertIsOnIOThread(); + MOZ_ASSERT(mArchivedOriginScope); + MOZ_ASSERT(mState == State::Nesting); + MOZ_ASSERT(mNestedState == NestedState::DatabaseWorkOpen); + + if (NS_WARN_IF(QuotaClient::IsShuttingDownOnNonBackgroundThread()) || + !MayProceedOnNonOwningThread()) { + return NS_ERROR_FAILURE; + } + + QuotaManager* quotaManager = QuotaManager::Get(); + MOZ_ASSERT(quotaManager); + + nsresult rv; + + if (!gArchivedOrigins) { + rv = LoadArchivedOrigins(); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + MOZ_ASSERT(gArchivedOrigins); + } + + bool hasDataForMigration = mArchivedOriginScope->HasMatches(gArchivedOrigins); + + bool createIfNotExists = mParams.createIfNotExists() || hasDataForMigration; + + nsCOMPtr directoryEntry; + rv = quotaManager->EnsureOriginIsInitialized(PERSISTENCE_TYPE_DEFAULT, + mSuffix, + mGroup, + mOrigin, + createIfNotExists, + getter_AddRefs(directoryEntry)); + if (rv == NS_ERROR_NOT_AVAILABLE) { + return DatabaseNotAvailable(); + } + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = directoryEntry->Append(NS_LITERAL_STRING(LS_DIRECTORY_NAME)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = EnsureDirectoryEntry(directoryEntry, + createIfNotExists, + /* aIsDirectory */ true); + if (rv == NS_ERROR_NOT_AVAILABLE) { + return DatabaseNotAvailable(); + } + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = directoryEntry->Append(NS_LITERAL_STRING(DATA_FILE_NAME)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + bool alreadyExisted; + rv = EnsureDirectoryEntry(directoryEntry, + createIfNotExists, + /* aIsDirectory */ false, + &alreadyExisted); + if (rv == NS_ERROR_NOT_AVAILABLE) { + return DatabaseNotAvailable(); + } + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (alreadyExisted) { + MOZ_ASSERT(gUsages); + MOZ_ASSERT(gUsages->Get(mOrigin, &mUsage)); + } else { + MOZ_ASSERT(mUsage == 0); + InitUsageForOrigin(mOrigin, mUsage); + } + + rv = directoryEntry->GetPath(mDatabaseFilePath); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + nsCOMPtr connection; + rv = CreateStorageConnection(directoryEntry, + mOrigin, + getter_AddRefs(connection)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = VerifyDatabaseInformation(connection); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (hasDataForMigration) { + MOZ_ASSERT(mUsage == 0); + + rv = AttachArchiveDatabase(quotaManager->GetStoragePath(), connection); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + int64_t newUsage; + rv = GetUsage(connection, mArchivedOriginScope, &newUsage); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + RefPtr quotaObject = GetQuotaObject(); + MOZ_ASSERT(quotaObject); + + if (!quotaObject->MaybeUpdateSize(newUsage, /* aTruncate */ true)) { + return NS_ERROR_FILE_NO_DEVICE_SPACE; + } + + mozStorageTransaction transaction(connection, false, + mozIStorageConnection::TRANSACTION_IMMEDIATE); + + nsCOMPtr stmt; + rv = connection->CreateStatement(NS_LITERAL_CSTRING( + "INSERT INTO data (key, value) " + "SELECT key, value " + "FROM webappsstore2 " + "WHERE originKey = :originKey " + "AND originAttributes = :originAttributes;" + + ), getter_AddRefs(stmt)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = mArchivedOriginScope->BindToStatement(stmt); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = stmt->Execute(); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = connection->CreateStatement(NS_LITERAL_CSTRING( + "DELETE FROM webappsstore2 " + "WHERE originKey = :originKey " + "AND originAttributes = :originAttributes;" + ), getter_AddRefs(stmt)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = mArchivedOriginScope->BindToStatement(stmt); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = stmt->Execute(); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = transaction.Commit(); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = DetachArchiveDatabase(connection); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + MOZ_ASSERT(gArchivedOrigins); + MOZ_ASSERT(mArchivedOriginScope->HasMatches(gArchivedOrigins)); + mArchivedOriginScope->RemoveMatches(gArchivedOrigins); + + mUsage = newUsage; + + MOZ_ASSERT(gUsages); + MOZ_ASSERT(gUsages->Contains(mOrigin)); + gUsages->Put(mOrigin, newUsage); + } + + nsCOMPtr shadowConnection; + if (!gInitializedShadowStorage) { + rv = CreateShadowStorageConnection(quotaManager->GetBasePath(), + getter_AddRefs(shadowConnection)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + gInitializedShadowStorage = true; + } + + // Must close connections before dispatching otherwise we might race with the + // connection thread which needs to open the same databases. + MOZ_ALWAYS_SUCCEEDS(connection->Close()); + + if (shadowConnection) { + MOZ_ALWAYS_SUCCEEDS(shadowConnection->Close()); + } + + // Must set this before dispatching otherwise we will race with the owning + // thread. + mNestedState = NestedState::BeginLoadData; + + rv = OwningEventTarget()->Dispatch(this, NS_DISPATCH_NORMAL); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + return NS_OK; +} + +nsresult +PrepareDatastoreOp::DatabaseNotAvailable() +{ + AssertIsOnIOThread(); + MOZ_ASSERT(mState == State::Nesting); + MOZ_ASSERT(mNestedState == NestedState::DatabaseWorkOpen); + + mDatabaseNotAvailable = true; + + nsresult rv = FinishNestingOnNonOwningThread(); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + return NS_OK; +} + +nsresult +PrepareDatastoreOp::EnsureDirectoryEntry(nsIFile* aEntry, + bool aCreateIfNotExists, + bool aIsDirectory, + bool* aAlreadyExisted) +{ + AssertIsOnIOThread(); + MOZ_ASSERT(aEntry); + + bool exists; + nsresult rv = aEntry->Exists(&exists); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (!exists) { + if (!aCreateIfNotExists) { + return NS_ERROR_NOT_AVAILABLE; + } + + if (aIsDirectory) { + rv = aEntry->Create(nsIFile::DIRECTORY_TYPE, 0755); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } + } +#ifdef DEBUG + else { + bool isDirectory; + MOZ_ASSERT(NS_SUCCEEDED(aEntry->IsDirectory(&isDirectory))); + MOZ_ASSERT(isDirectory == aIsDirectory); + } +#endif + + if (aAlreadyExisted) { + *aAlreadyExisted = exists; + } + return NS_OK; +} + +nsresult +PrepareDatastoreOp::VerifyDatabaseInformation(mozIStorageConnection* aConnection) +{ + AssertIsOnIOThread(); + MOZ_ASSERT(aConnection); + + nsCOMPtr stmt; + nsresult rv = aConnection->CreateStatement(NS_LITERAL_CSTRING( + "SELECT origin " + "FROM database" + ), getter_AddRefs(stmt)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + bool hasResult; + rv = stmt->ExecuteStep(&hasResult); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (NS_WARN_IF(!hasResult)) { + return NS_ERROR_FILE_CORRUPTED; + } + + nsCString origin; + rv = stmt->GetUTF8String(0, origin); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (NS_WARN_IF(!QuotaManager::AreOriginsEqualOnDisk(mOrigin, origin))) { + return NS_ERROR_FILE_CORRUPTED; + } + + return NS_OK; +} + +already_AddRefed +PrepareDatastoreOp::GetQuotaObject() +{ + MOZ_ASSERT(IsOnOwningThread() || IsOnIOThread()); + MOZ_ASSERT(!mGroup.IsEmpty()); + MOZ_ASSERT(!mOrigin.IsEmpty()); + MOZ_ASSERT(!mDatabaseFilePath.IsEmpty()); + + QuotaManager* quotaManager = QuotaManager::Get(); + MOZ_ASSERT(quotaManager); + + RefPtr quotaObject = + quotaManager->GetQuotaObject(PERSISTENCE_TYPE_DEFAULT, + mGroup, + mOrigin, + mDatabaseFilePath, + mUsage); + MOZ_ASSERT(quotaObject); + + return quotaObject.forget(); +} + +nsresult +PrepareDatastoreOp::BeginLoadData() +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(mState == State::Nesting); + MOZ_ASSERT(mNestedState == NestedState::BeginLoadData); + MOZ_ASSERT(!mConnection); + + if (NS_WARN_IF(QuotaClient::IsShuttingDownOnBackgroundThread()) || + !MayProceed()) { + return NS_ERROR_FAILURE; + } + + if (!gConnectionThread) { + gConnectionThread = new ConnectionThread(); + } + + mConnection = + gConnectionThread->CreateConnection(mOrigin, + mDatabaseFilePath, + std::move(mArchivedOriginScope)); + MOZ_ASSERT(mConnection); + + // Must set this before dispatching otherwise we will race with the + // connection thread. + mNestedState = NestedState::DatabaseWorkLoadData; + + // Can't assign to mLoadDataOp directly since that's a weak reference and + // LoadDataOp is reference counted. + RefPtr loadDataOp = new LoadDataOp(this); + + // This add refs loadDataOp. + mConnection->Dispatch(loadDataOp); + + // This is cleared in LoadDataOp::Cleanup() before the load data op is + // destroyed. + mLoadDataOp = loadDataOp; + + return NS_OK; +} + +void +PrepareDatastoreOp::FinishNesting() +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(mState == State::Nesting); + + // The caller holds a strong reference to us, no need for a self reference + // before calling Run(). + + mState = State::SendingReadyMessage; + mNestedState = NestedState::AfterNesting; + + MOZ_ALWAYS_SUCCEEDS(Run()); +} + +nsresult +PrepareDatastoreOp::FinishNestingOnNonOwningThread() +{ + MOZ_ASSERT(!IsOnOwningThread()); + MOZ_ASSERT(mState == State::Nesting); + + // Must set mState before dispatching otherwise we will race with the owning + // thread. + mState = State::SendingReadyMessage; + mNestedState = NestedState::AfterNesting; + + nsresult rv = OwningEventTarget()->Dispatch(this, NS_DISPATCH_NORMAL); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + return NS_OK; +} + +nsresult +PrepareDatastoreOp::NestedRun() +{ + nsresult rv; + + switch (mNestedState) { + case NestedState::CheckExistingOperations: + rv = CheckExistingOperations(); + break; + + case NestedState::CheckClosingDatastore: + rv = CheckClosingDatastore(); + break; + + case NestedState::PreparationPending: + rv = BeginDatastorePreparation(); + break; + + case NestedState::QuotaManagerPending: + rv = QuotaManagerOpen(); + break; + + case NestedState::DatabaseWorkOpen: + rv = DatabaseWork(); + break; + + case NestedState::BeginLoadData: + rv = BeginLoadData(); + break; + + default: + MOZ_CRASH("Bad state!"); + } + + if (NS_WARN_IF(NS_FAILED(rv))) { + mNestedState = NestedState::AfterNesting; + + return rv; + } + + return NS_OK; +} + +void +PrepareDatastoreOp::GetResponse(LSRequestResponse& aResponse) +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(mState == State::SendingResults); + MOZ_ASSERT(NS_SUCCEEDED(ResultCode())); + + if (mDatabaseNotAvailable) { + MOZ_ASSERT(!mParams.createIfNotExists()); + + LSRequestPrepareDatastoreResponse prepareDatastoreResponse; + prepareDatastoreResponse.datastoreId() = null_t(); + + aResponse = prepareDatastoreResponse; + + return; + } + + if (!mDatastore) { + MOZ_ASSERT(mUsage == mDEBUGUsage); + + RefPtr quotaObject; + + if (mPrivateBrowsingId == 0) { + quotaObject = GetQuotaObject(); + MOZ_ASSERT(quotaObject); + } + + mDatastore = new Datastore(mOrigin, + mPrivateBrowsingId, + mUsage, + mSizeOfKeys, + mSizeOfItems, + mDirectoryLock.forget(), + mConnection.forget(), + quotaObject.forget(), + mValues, + mOrderedItems); + + mDatastore->NoteLivePrepareDatastoreOp(this); + + if (!gDatastores) { + gDatastores = new DatastoreHashtable(); + } + + MOZ_ASSERT(!gDatastores->Get(mOrigin)); + gDatastores->Put(mOrigin, mDatastore); + } + + uint64_t datastoreId = ++gLastDatastoreId; + + nsAutoPtr preparedDatastore( + new PreparedDatastore(mDatastore, + mContentParentId, + mOrigin, + datastoreId, + /* aForPreload */ !mParams.createIfNotExists())); + + if (!gPreparedDatastores) { + gPreparedDatastores = new PreparedDatastoreHashtable(); + } + gPreparedDatastores->Put(datastoreId, preparedDatastore); + + if (mInvalidated) { + preparedDatastore->Invalidate(); + } + + preparedDatastore.forget(); + + LSRequestPrepareDatastoreResponse prepareDatastoreResponse; + prepareDatastoreResponse.datastoreId() = datastoreId; + + aResponse = prepareDatastoreResponse; +} + +void +PrepareDatastoreOp::Cleanup() +{ + AssertIsOnOwningThread(); + + if (mDatastore) { + MOZ_ASSERT(!mDirectoryLock); + MOZ_ASSERT(!mConnection); + + if (NS_FAILED(ResultCode())) { + MOZ_ASSERT(!mDatastore->IsClosed()); + MOZ_ASSERT(!mDatastore->HasLiveDatabases()); + MOZ_ASSERT(!mDatastore->HasLivePreparedDatastores()); + mDatastore->Close(); + } + + // Make sure to release the datastore on this thread. + + mDatastore->NoteFinishedPrepareDatastoreOp(this); + + mDatastore = nullptr; + + CleanupMetadata(); + } else if (mConnection) { + // If we have a connection then the operation must have failed and there + // must be a directory lock too. + MOZ_ASSERT(NS_FAILED(ResultCode())); + MOZ_ASSERT(mDirectoryLock); + + // We must close the connection on the connection thread before releasing + // it on this thread. The directory lock can't be released either. + nsCOMPtr callback = + NewRunnableMethod("dom::OpenDatabaseOp::ConnectionClosedCallback", + this, + &PrepareDatastoreOp::ConnectionClosedCallback); + + mConnection->Close(callback); + } else { + // If we don't have a connection, but we do have a directory lock then the + // operation must have failed or we were preloading a datastore and there + // was no physical database on disk. + MOZ_ASSERT_IF(mDirectoryLock, + NS_FAILED(ResultCode()) || mDatabaseNotAvailable); + + // There's no connection, so it's safe to release the directory lock and + // unregister itself from the array. + + mDirectoryLock = nullptr; + + CleanupMetadata(); + } +} + +void +PrepareDatastoreOp::ConnectionClosedCallback() +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(NS_FAILED(ResultCode())); + MOZ_ASSERT(mDirectoryLock); + MOZ_ASSERT(mConnection); + + mConnection = nullptr; + mDirectoryLock = nullptr; + + CleanupMetadata(); +} + +void +PrepareDatastoreOp::CleanupMetadata() +{ + AssertIsOnOwningThread(); + + if (mDelayedOp) { + MOZ_ALWAYS_SUCCEEDS(NS_DispatchToCurrentThread(mDelayedOp.forget())); + } + + MOZ_ASSERT(gPrepareDatastoreOps); + gPrepareDatastoreOps->RemoveElement(this); + + if (gPrepareDatastoreOps->IsEmpty()) { + gPrepareDatastoreOps = nullptr; + } +} + +NS_IMPL_ISUPPORTS_INHERITED0(PrepareDatastoreOp, LSRequestBase) + +void +PrepareDatastoreOp::ActorDestroy(ActorDestroyReason aWhy) +{ + AssertIsOnOwningThread(); + + LSRequestBase::ActorDestroy(aWhy); + + if (mLoadDataOp) { + mLoadDataOp->NoteComplete(); + } +} + +void +PrepareDatastoreOp::DirectoryLockAcquired(DirectoryLock* aLock) +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(mState == State::Nesting); + MOZ_ASSERT(mNestedState == NestedState::DirectoryOpenPending); + MOZ_ASSERT(!mDirectoryLock); + + if (NS_WARN_IF(QuotaClient::IsShuttingDownOnBackgroundThread()) || + !MayProceed()) { + MaybeSetFailureCode(NS_ERROR_FAILURE); + + FinishNesting(); + + return; + } + + mDirectoryLock = aLock; + + SendToIOThread(); +} + +void +PrepareDatastoreOp::DirectoryLockFailed() +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(mState == State::Nesting); + MOZ_ASSERT(mNestedState == NestedState::DirectoryOpenPending); + MOZ_ASSERT(!mDirectoryLock); + + MaybeSetFailureCode(NS_ERROR_FAILURE); + + FinishNesting(); +} + +nsresult +PrepareDatastoreOp:: +LoadDataOp::DoDatastoreWork() +{ + AssertIsOnConnectionThread(); + MOZ_ASSERT(mConnection); + MOZ_ASSERT(mPrepareDatastoreOp); + MOZ_ASSERT(mPrepareDatastoreOp->mState == State::Nesting); + MOZ_ASSERT(mPrepareDatastoreOp->mNestedState == + NestedState::DatabaseWorkLoadData); + + if (NS_WARN_IF(QuotaClient::IsShuttingDownOnNonBackgroundThread()) || + !MayProceedOnNonOwningThread()) { + return NS_ERROR_FAILURE; + } + + Connection::CachedStatement stmt; + nsresult rv = mConnection->GetCachedStatement(NS_LITERAL_CSTRING( + "SELECT key, value " + "FROM data;"), + &stmt); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + bool hasResult; + while (NS_SUCCEEDED(rv = stmt->ExecuteStep(&hasResult)) && hasResult) { + nsString key; + rv = stmt->GetString(0, key); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + nsString value; + rv = stmt->GetString(1, value); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + mPrepareDatastoreOp->mValues.Put(key, value); + auto item = mPrepareDatastoreOp->mOrderedItems.AppendElement(); + item->key() = key; + item->value() = value; + mPrepareDatastoreOp->mSizeOfKeys += key.Length(); + mPrepareDatastoreOp->mSizeOfItems += key.Length() + value.Length(); +#ifdef DEBUG + mPrepareDatastoreOp->mDEBUGUsage += key.Length() + value.Length(); +#endif + } + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + return NS_OK; +} + +void +PrepareDatastoreOp:: +LoadDataOp::OnSuccess() +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(mPrepareDatastoreOp); + MOZ_ASSERT(mPrepareDatastoreOp->mState == State::Nesting); + MOZ_ASSERT(mPrepareDatastoreOp->mNestedState == + NestedState::DatabaseWorkLoadData); + MOZ_ASSERT(mPrepareDatastoreOp->mLoadDataOp == this); + + mPrepareDatastoreOp->FinishNesting(); +} + +void +PrepareDatastoreOp:: +LoadDataOp::OnFailure(nsresult aResultCode) +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(mPrepareDatastoreOp); + MOZ_ASSERT(mPrepareDatastoreOp->mState == State::Nesting); + MOZ_ASSERT(mPrepareDatastoreOp->mNestedState == + NestedState::DatabaseWorkLoadData); + MOZ_ASSERT(mPrepareDatastoreOp->mLoadDataOp == this); + + mPrepareDatastoreOp->SetFailureCode(aResultCode); + + mPrepareDatastoreOp->FinishNesting(); +} + +void +PrepareDatastoreOp:: +LoadDataOp::Cleanup() +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(mPrepareDatastoreOp); + MOZ_ASSERT(mPrepareDatastoreOp->mLoadDataOp == this); + + mPrepareDatastoreOp->mLoadDataOp = nullptr; + mPrepareDatastoreOp = nullptr; + + ConnectionDatastoreOperationBase::Cleanup(); +} + +/******************************************************************************* + * PrepareObserverOp + ******************************************************************************/ + +PrepareObserverOp::PrepareObserverOp(nsIEventTarget* aMainEventTarget, + const LSRequestParams& aParams) + : LSRequestBase(aMainEventTarget) + , mParams(aParams.get_LSRequestPrepareObserverParams()) +{ + MOZ_ASSERT(aParams.type() == + LSRequestParams::TLSRequestPrepareObserverParams); +} + +nsresult +PrepareObserverOp::Open() +{ + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(mState == State::Opening); + + if (NS_WARN_IF(QuotaClient::IsShuttingDownOnNonBackgroundThread()) || + !MayProceedOnNonOwningThread()) { + return NS_ERROR_FAILURE; + } + + const PrincipalInfo& principalInfo = mParams.principalInfo(); + + if (principalInfo.type() == PrincipalInfo::TSystemPrincipalInfo) { + QuotaManager::GetInfoForChrome(nullptr, nullptr, &mOrigin); + } else { + MOZ_ASSERT(principalInfo.type() == PrincipalInfo::TContentPrincipalInfo); + + nsresult rv; + nsCOMPtr principal = + PrincipalInfoToPrincipal(principalInfo, &rv); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = QuotaManager::GetInfoFromPrincipal(principal, + nullptr, + nullptr, + &mOrigin); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } + + mState = State::SendingReadyMessage; + MOZ_ALWAYS_SUCCEEDS(OwningEventTarget()->Dispatch(this, NS_DISPATCH_NORMAL)); + + return NS_OK; +} + +void +PrepareObserverOp::GetResponse(LSRequestResponse& aResponse) +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(mState == State::SendingResults); + MOZ_ASSERT(NS_SUCCEEDED(ResultCode())); + + uint64_t observerId = ++gLastObserverId; + + RefPtr observer = new Observer(mOrigin); + + if (!gPreparedObsevers) { + gPreparedObsevers = new PreparedObserverHashtable(); + } + gPreparedObsevers->Put(observerId, observer); + + LSRequestPrepareObserverResponse prepareObserverResponse; + prepareObserverResponse.observerId() = observerId; + + aResponse = prepareObserverResponse; +} + +/******************************************************************************* ++ * LSSimpleRequestBase ++ ******************************************************************************/ + +LSSimpleRequestBase::LSSimpleRequestBase() + : mState(State::Initial) +{ +} + +LSSimpleRequestBase::~LSSimpleRequestBase() +{ + MOZ_ASSERT_IF(MayProceedOnNonOwningThread(), + mState == State::Initial || mState == State::Completed); +} + +void +LSSimpleRequestBase::Dispatch() +{ + AssertIsOnOwningThread(); + + mState = State::Opening; + + MOZ_ALWAYS_SUCCEEDS(NS_DispatchToMainThread(this)); +} + +void +LSSimpleRequestBase::SendResults() +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(mState == State::SendingResults); + + if (NS_WARN_IF(QuotaClient::IsShuttingDownOnBackgroundThread()) || + !MayProceed()) { + MaybeSetFailureCode(NS_ERROR_FAILURE); + } + + if (MayProceed()) { + LSSimpleRequestResponse response; + + if (NS_SUCCEEDED(ResultCode())) { + GetResponse(response); + } else { + response = ResultCode(); + } + + Unused << + PBackgroundLSSimpleRequestParent::Send__delete__(this, response); + } + + mState = State::Completed; +} + +NS_IMETHODIMP +LSSimpleRequestBase::Run() +{ + nsresult rv; + + switch (mState) { + case State::Opening: + rv = Open(); + break; + + case State::SendingResults: + SendResults(); + return NS_OK; + + default: + MOZ_CRASH("Bad state!"); + } + + if (NS_WARN_IF(NS_FAILED(rv)) && mState != State::SendingResults) { + MaybeSetFailureCode(rv); + + // Must set mState before dispatching otherwise we will race with the owning + // thread. + mState = State::SendingResults; + + if (IsOnOwningThread()) { + SendResults(); + } else { + MOZ_ALWAYS_SUCCEEDS( + OwningEventTarget()->Dispatch(this, NS_DISPATCH_NORMAL)); + } + } + + return NS_OK; +} + +void +LSSimpleRequestBase::ActorDestroy(ActorDestroyReason aWhy) +{ + AssertIsOnOwningThread(); + + NoteComplete(); +} + +/******************************************************************************* + * PreloadedOp + ******************************************************************************/ + +PreloadedOp::PreloadedOp(const LSSimpleRequestParams& aParams) + : mParams(aParams.get_LSSimpleRequestPreloadedParams()) +{ + MOZ_ASSERT(aParams.type() == + LSSimpleRequestParams::TLSSimpleRequestPreloadedParams); +} + +nsresult +PreloadedOp::Open() +{ + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(mState == State::Opening); + + if (NS_WARN_IF(QuotaClient::IsShuttingDownOnNonBackgroundThread()) || + !MayProceedOnNonOwningThread()) { + return NS_ERROR_FAILURE; + } + + const PrincipalInfo& principalInfo = mParams.principalInfo(); + + if (principalInfo.type() == PrincipalInfo::TSystemPrincipalInfo) { + QuotaManager::GetInfoForChrome(nullptr, nullptr, &mOrigin); + } else { + MOZ_ASSERT(principalInfo.type() == PrincipalInfo::TContentPrincipalInfo); + + nsresult rv; + nsCOMPtr principal = + PrincipalInfoToPrincipal(principalInfo, &rv); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = QuotaManager::GetInfoFromPrincipal(principal, + nullptr, + nullptr, + &mOrigin); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } + + mState = State::SendingResults; + MOZ_ALWAYS_SUCCEEDS(OwningEventTarget()->Dispatch(this, NS_DISPATCH_NORMAL)); + + return NS_OK; +} + +void +PreloadedOp::GetResponse(LSSimpleRequestResponse& aResponse) +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(mState == State::SendingResults); + MOZ_ASSERT(NS_SUCCEEDED(ResultCode())); + + bool preloaded; + RefPtr datastore; + if (gDatastores && + (datastore = gDatastores->Get(mOrigin)) && + !datastore->IsClosed()) { + preloaded = true; + } else { + preloaded = false; + } + + LSSimpleRequestPreloadedResponse preloadedResponse; + preloadedResponse.preloaded() = preloaded; + + aResponse = preloadedResponse; +} + +/******************************************************************************* + * ArchivedOriginScope + ******************************************************************************/ + +// static +ArchivedOriginScope* +ArchivedOriginScope::CreateFromOrigin(nsIPrincipal* aPrincipal) +{ + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(aPrincipal); + + nsCString originAttrSuffix; + nsCString originKey; + nsresult rv = GenerateOriginKey(aPrincipal, originAttrSuffix, originKey); + if (NS_WARN_IF(NS_FAILED(rv))) { + return nullptr; + } + + return new ArchivedOriginScope(std::move(Origin(originAttrSuffix, + originKey))); +} + +// static +ArchivedOriginScope* +ArchivedOriginScope::CreateFromPrefix(nsIPrincipal* aPrincipal) +{ + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(aPrincipal); + + nsCString originAttrSuffix; + nsCString originKey; + nsresult rv = GenerateOriginKey(aPrincipal, originAttrSuffix, originKey); + if (NS_WARN_IF(NS_FAILED(rv))) { + return nullptr; + } + + return new ArchivedOriginScope(std::move(Prefix(originKey))); +} + +// static +ArchivedOriginScope* +ArchivedOriginScope::CreateFromPattern(const OriginAttributesPattern& aPattern) +{ + return new ArchivedOriginScope(std::move(Pattern(aPattern))); +} + +// static +ArchivedOriginScope* +ArchivedOriginScope::CreateFromNull() +{ + return new ArchivedOriginScope(std::move(Null())); +} + +void +ArchivedOriginScope::GetBindingClause(nsACString& aBindingClause) const +{ + struct Matcher + { + nsACString* mBindingClause; + + explicit Matcher(nsACString* aBindingClause) + : mBindingClause(aBindingClause) + { } + + void + match(const Origin& aOrigin) { + *mBindingClause = NS_LITERAL_CSTRING( + " WHERE originKey = :originKey " + "AND originAttributes = :originAttributes" + ); + } + + void + match(const Prefix& aPrefix) { + *mBindingClause = NS_LITERAL_CSTRING( + " WHERE originKey = :originKey" + ); + } + + void + match(const Pattern& aPattern) { + *mBindingClause = NS_LITERAL_CSTRING( + " WHERE originAttributes MATCH :originAttributesPattern" + ); + } + + void + match(const Null& aNull) { + *mBindingClause = EmptyCString(); + } + }; + + mData.match(Matcher(&aBindingClause)); +} + +nsresult +ArchivedOriginScope::BindToStatement(mozIStorageStatement* aStmt) const +{ + MOZ_ASSERT(IsOnIOThread() || IsOnConnectionThread()); + MOZ_ASSERT(aStmt); + + struct Matcher + { + mozIStorageStatement* mStmt; + + explicit Matcher(mozIStorageStatement* aStmt) + : mStmt(aStmt) + { } + + nsresult + match(const Origin& aOrigin) { + nsresult rv = mStmt->BindUTF8StringByName(NS_LITERAL_CSTRING("originKey"), + aOrigin.OriginNoSuffix()); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = mStmt->BindUTF8StringByName(NS_LITERAL_CSTRING("originAttributes"), + aOrigin.OriginSuffix()); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + return NS_OK; + } + + nsresult + match(const Prefix& aPrefix) { + nsresult rv = mStmt->BindUTF8StringByName(NS_LITERAL_CSTRING("originKey"), + aPrefix.OriginNoSuffix()); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + return NS_OK; + } + + nsresult + match(const Pattern& aPattern) { + nsresult rv = mStmt->BindUTF8StringByName( + NS_LITERAL_CSTRING("originAttributesPattern"), + NS_LITERAL_CSTRING("pattern1")); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + return NS_OK; + } + + nsresult + match(const Null& aNull) { + return NS_OK; + } + }; + + nsresult rv = mData.match(Matcher(aStmt)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + return NS_OK; +} + +bool +ArchivedOriginScope::HasMatches(ArchivedOriginHashtable* aHashtable) const +{ + AssertIsOnIOThread(); + MOZ_ASSERT(aHashtable); + + struct Matcher + { + ArchivedOriginHashtable* mHashtable; + + explicit Matcher(ArchivedOriginHashtable* aHashtable) + : mHashtable(aHashtable) + { } + + bool + match(const Origin& aOrigin) { + nsCString hashKey = GetArchivedOriginHashKey(aOrigin.OriginSuffix(), + aOrigin.OriginNoSuffix()); + + ArchivedOriginInfo* archivedOriginInfo; + return mHashtable->Get(hashKey, &archivedOriginInfo); + } + + bool + match(const Prefix& aPrefix) { + for (auto iter = mHashtable->ConstIter(); !iter.Done(); iter.Next()) { + ArchivedOriginInfo* archivedOriginInfo = iter.Data(); + + if (archivedOriginInfo->mOriginNoSuffix == aPrefix.OriginNoSuffix()) { + return true; + } + } + + return false; + } + + bool + match(const Pattern& aPattern) { + for (auto iter = mHashtable->ConstIter(); !iter.Done(); iter.Next()) { + ArchivedOriginInfo* archivedOriginInfo = iter.Data(); + + if (aPattern.GetPattern().Matches( + archivedOriginInfo->mOriginAttributes)) { + return true; + } + } + + return false; + } + + bool + match(const Null& aNull) { + return mHashtable->Count(); + } + }; + + return mData.match(Matcher(aHashtable)); +} + +void +ArchivedOriginScope::RemoveMatches(ArchivedOriginHashtable* aHashtable) const +{ + AssertIsOnIOThread(); + MOZ_ASSERT(aHashtable); + + struct Matcher + { + ArchivedOriginHashtable* mHashtable; + + explicit Matcher(ArchivedOriginHashtable* aHashtable) + : mHashtable(aHashtable) + { } + + void + match(const Origin& aOrigin) { + nsCString hashKey = GetArchivedOriginHashKey(aOrigin.OriginSuffix(), + aOrigin.OriginNoSuffix()); + + mHashtable->Remove(hashKey); + } + + void + match(const Prefix& aPrefix) { + for (auto iter = mHashtable->Iter(); !iter.Done(); iter.Next()) { + ArchivedOriginInfo* archivedOriginInfo = iter.Data(); + + if (archivedOriginInfo->mOriginNoSuffix == aPrefix.OriginNoSuffix()) { + iter.Remove(); + } + } + } + + void + match(const Pattern& aPattern) { + for (auto iter = mHashtable->Iter(); !iter.Done(); iter.Next()) { + ArchivedOriginInfo* archivedOriginInfo = iter.Data(); + + if (aPattern.GetPattern().Matches( + archivedOriginInfo->mOriginAttributes)) { + iter.Remove(); + } + } + } + + void + match(const Null& aNull) { + mHashtable->Clear(); + } + }; + + mData.match(Matcher(aHashtable)); +} + +/******************************************************************************* + * ArchivedOriginScopeHelper + ******************************************************************************/ + +nsresult +ArchivedOriginScopeHelper::BlockAndReturnArchivedOriginScope( + nsAutoPtr& aArchivedOriginScope) +{ + AssertIsOnIOThread(); + + MOZ_ALWAYS_SUCCEEDS(NS_DispatchToMainThread(this)); + + mozilla::MonitorAutoLock lock(mMonitor); + while (mWaiting) { + lock.Wait(); + } + + if (NS_WARN_IF(NS_FAILED(mMainThreadResultCode))) { + return mMainThreadResultCode; + } + + aArchivedOriginScope = std::move(mArchivedOriginScope); + return NS_OK; +} + +nsresult +ArchivedOriginScopeHelper::RunOnMainThread() +{ + MOZ_ASSERT(NS_IsMainThread()); + + nsCOMPtr uri; + nsresult rv = NS_NewURI(getter_AddRefs(uri), mSpec); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + nsCOMPtr principal = + BasePrincipal::CreateCodebasePrincipal(uri, mAttrs); + if (NS_WARN_IF(!principal)) { + return NS_ERROR_FAILURE; + } + + if (mPrefix) { + mArchivedOriginScope = ArchivedOriginScope::CreateFromPrefix(principal); + } else { + mArchivedOriginScope = ArchivedOriginScope::CreateFromOrigin(principal); + } + if (NS_WARN_IF(!mArchivedOriginScope)) { + return NS_ERROR_FAILURE; + } + + return NS_OK; +} + +NS_IMETHODIMP +ArchivedOriginScopeHelper::Run() +{ + MOZ_ASSERT(NS_IsMainThread()); + + nsresult rv = RunOnMainThread(); + if (NS_WARN_IF(NS_FAILED(rv))) { + mMainThreadResultCode = rv; + } + + mozilla::MonitorAutoLock lock(mMonitor); + MOZ_ASSERT(mWaiting); + + mWaiting = false; + lock.Notify(); + + return NS_OK; +} + +/******************************************************************************* + * QuotaClient + ******************************************************************************/ + +QuotaClient* QuotaClient::sInstance = nullptr; +bool QuotaClient::sObserversRegistered = false; + +QuotaClient::QuotaClient() + : mShadowDatabaseMutex("LocalStorage mShadowDatabaseMutex") + , mShutdownRequested(false) +{ + AssertIsOnBackgroundThread(); + MOZ_ASSERT(!sInstance, "We expect this to be a singleton!"); + + sInstance = this; +} + +QuotaClient::~QuotaClient() +{ + AssertIsOnBackgroundThread(); + MOZ_ASSERT(sInstance == this, "We expect this to be a singleton!"); + + sInstance = nullptr; +} + +mozilla::dom::quota::Client::Type +QuotaClient::GetType() +{ + return QuotaClient::LS; +} + +// static +nsresult +QuotaClient::RegisterObservers(nsIEventTarget* aBackgroundEventTarget) +{ + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(aBackgroundEventTarget); + + if (!sObserversRegistered) { + nsCOMPtr obs = services::GetObserverService(); + if (NS_WARN_IF(!obs)) { + return NS_ERROR_FAILURE; + } + + nsCOMPtr observer = new Observer(aBackgroundEventTarget); + + nsresult rv = + obs->AddObserver(observer, kPrivateBrowsingObserverTopic, false); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (NS_FAILED(Preferences::AddAtomicUintVarCache(&gOriginLimitKB, + kDefaultQuotaPref, + kDefaultOriginLimitKB))) { + NS_WARNING("Unable to respond to default quota pref changes!"); + } + + Preferences::RegisterCallbackAndCall(ShadowWritesPrefChangedCallback, + kShadowWritesPref); + + Preferences::RegisterCallbackAndCall(SnapshotPrefillPrefChangedCallback, + kSnapshotPrefillPref); + + sObserversRegistered = true; + } + + return NS_OK; +} + +nsresult +QuotaClient::InitOrigin(PersistenceType aPersistenceType, + const nsACString& aGroup, + const nsACString& aOrigin, + const AtomicBool& aCanceled, + UsageInfo* aUsageInfo) +{ + AssertIsOnIOThread(); + MOZ_ASSERT(aPersistenceType == PERSISTENCE_TYPE_DEFAULT); + + QuotaManager* quotaManager = QuotaManager::Get(); + MOZ_ASSERT(quotaManager); + + nsCOMPtr directory; + nsresult rv = quotaManager->GetDirectoryForOrigin(aPersistenceType, + aOrigin, + getter_AddRefs(directory)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + MOZ_ASSERT(directory); + + rv = directory->Append(NS_LITERAL_STRING(LS_DIRECTORY_NAME)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + bool exists; +#ifdef DEBUG + rv = directory->Exists(&exists); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + MOZ_ASSERT(exists); +#endif + + nsCOMPtr file; + rv = directory->Clone(getter_AddRefs(file)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = file->Append(NS_LITERAL_STRING(DATA_FILE_NAME)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = file->Exists(&exists); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (exists) { + bool isDirectory; + rv = file->IsDirectory(&isDirectory); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (NS_WARN_IF(isDirectory)) { + return NS_ERROR_FAILURE; + } + + // TODO: Use a special file that contains logical size of the database. + // For now, get the usage from the database. + + nsCOMPtr connection; + rv = CreateStorageConnection(file, aOrigin, getter_AddRefs(connection)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + int64_t usage; + rv = GetUsage(connection, /* aArchivedOriginScope */ nullptr, &usage); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + InitUsageForOrigin(aOrigin, usage); + + aUsageInfo->AppendToDatabaseUsage(uint64_t(usage)); + } + + // Report unknown files, don't fail, just warn. + + nsCOMPtr directoryEntries; + rv = directory->GetDirectoryEntries(getter_AddRefs(directoryEntries)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (!directoryEntries) { + return NS_OK; + } + + while (true) { + if (aCanceled) { + break; + } + + nsCOMPtr file; + rv = directoryEntries->GetNextFile(getter_AddRefs(file)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (!file) { + break; + } + + nsString leafName; + rv = file->GetLeafName(leafName); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (leafName.EqualsLiteral(DATA_FILE_NAME)) { + // Don't need to check if it is a directory or file. We did that above. + continue; + } + + if (leafName.EqualsLiteral(JOURNAL_FILE_NAME)) { + bool isDirectory; + rv = file->IsDirectory(&isDirectory); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (!isDirectory) { + continue; + } + } + + LS_WARNING("Something (%s) in the directory that doesn't belong!", \ + NS_ConvertUTF16toUTF8(leafName).get()); + } + + return NS_OK; +} + +nsresult +QuotaClient::GetUsageForOrigin(PersistenceType aPersistenceType, + const nsACString& aGroup, + const nsACString& aOrigin, + const AtomicBool& aCanceled, + UsageInfo* aUsageInfo) +{ + AssertIsOnIOThread(); + MOZ_ASSERT(aPersistenceType == PERSISTENCE_TYPE_DEFAULT); + MOZ_ASSERT(aUsageInfo); + + // We can't open the database at this point, since it can be already used + // by the connection thread. Use the cached value instead. + + if (gUsages) { + int64_t usage; + if (gUsages->Get(aOrigin, &usage)) { + aUsageInfo->AppendToDatabaseUsage(usage); + } + } + + return NS_OK; +} + +nsresult +QuotaClient::AboutToClearOrigins( + const Nullable& aPersistenceType, + const OriginScope& aOriginScope) +{ + AssertIsOnIOThread(); + + // This method is not called when the clearing is triggered by the eviction + // process. It's on purpose to avoid a problem with the origin access time + // which can be described as follows: + // When there's a storage pressure condition and quota manager starts + // collecting origins for eviction, there can be an origin that hasn't been + // touched for long time. However, the old implementation of local storage + // could have touched the origin only recently and the new implementation + // hasn't had a chance to create a new per origin database for it yet (the + // data is still in the archive database), so the origin access time hasn't + // been updated either. In the end, the origin would be evicted despite the + // fact that there was recent local storage activity. + // So this method clears the archived data and shadow database entries for + // given origin scope, but only if it's a privacy-related origin clearing. + + if (!aPersistenceType.IsNull() && + aPersistenceType.Value() != PERSISTENCE_TYPE_DEFAULT) { + return NS_OK; + } + + bool shadowWrites = gShadowWrites; + + nsAutoPtr archivedOriginScope; + nsresult rv = CreateArchivedOriginScope(aOriginScope, archivedOriginScope); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (!gArchivedOrigins) { + rv = LoadArchivedOrigins(); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + MOZ_ASSERT(gArchivedOrigins); + } + + bool hasDataForRemoval = archivedOriginScope->HasMatches(gArchivedOrigins); + + QuotaManager* quotaManager = QuotaManager::Get(); + MOZ_ASSERT(quotaManager); + + nsString basePath = quotaManager->GetBasePath(); + + { + MutexAutoLock shadowDatabaseLock(mShadowDatabaseMutex); + + nsCOMPtr connection; + if (gInitializedShadowStorage) { + rv = GetShadowStorageConnection(basePath, getter_AddRefs(connection)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } else { + rv = CreateShadowStorageConnection(basePath, getter_AddRefs(connection)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + gInitializedShadowStorage = true; + } + + if (hasDataForRemoval) { + rv = AttachArchiveDatabase(quotaManager->GetStoragePath(), connection); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } + + if (archivedOriginScope->IsPattern()) { + nsCOMPtr function( + new MatchFunction(archivedOriginScope->GetPattern())); + + rv = connection->CreateFunction(NS_LITERAL_CSTRING("match"), 2, function); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } + + nsCOMPtr stmt; + rv = connection->CreateStatement(NS_LITERAL_CSTRING( + "BEGIN IMMEDIATE;" + ), getter_AddRefs(stmt)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = stmt->Execute(); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (shadowWrites) { + rv = PerformDelete(connection, + NS_LITERAL_CSTRING("main"), + archivedOriginScope); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } + + if (hasDataForRemoval) { + rv = PerformDelete(connection, + NS_LITERAL_CSTRING("archive"), + archivedOriginScope); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } + + rv = connection->CreateStatement(NS_LITERAL_CSTRING( + "COMMIT;" + ), getter_AddRefs(stmt)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = stmt->Execute(); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + stmt = nullptr; + + if (archivedOriginScope->IsPattern()) { + rv = connection->RemoveFunction(NS_LITERAL_CSTRING("match")); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } + + if (hasDataForRemoval) { + rv = DetachArchiveDatabase(connection); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + MOZ_ASSERT(gArchivedOrigins); + MOZ_ASSERT(archivedOriginScope->HasMatches(gArchivedOrigins)); + archivedOriginScope->RemoveMatches(gArchivedOrigins); + } + + rv = connection->Close(); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } + + if (aOriginScope.IsNull()) { + nsCOMPtr shadowFile; + rv = GetShadowFile(basePath, getter_AddRefs(shadowFile)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = shadowFile->Remove(false); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + gInitializedShadowStorage = false; + } + + return NS_OK; +} + +void +QuotaClient::OnOriginClearCompleted(PersistenceType aPersistenceType, + const nsACString& aOrigin) +{ + AssertIsOnIOThread(); + + if (aPersistenceType != PERSISTENCE_TYPE_DEFAULT) { + return; + } + + if (gUsages) { + gUsages->Remove(aOrigin); + } +} + +void +QuotaClient::ReleaseIOThreadObjects() +{ + AssertIsOnIOThread(); + + gUsages = nullptr; + + // Delete archived origins hashtable since QuotaManager clears the whole + // storage directory including ls-archive.sqlite. + + gArchivedOrigins = nullptr; +} + +void +QuotaClient::AbortOperations(const nsACString& aOrigin) +{ + AssertIsOnBackgroundThread(); + + // A PrepareDatastoreOp object could already acquire a directory lock for + // the given origin. Its last step is creation of a Datastore object (which + // will take ownership of the directory lock) and a PreparedDatastore object + // which keeps the Datastore alive until a database actor is created. + // We need to invalidate the PreparedDatastore object when it's created, + // otherwise the Datastore object can block the origin clear operation for + // long time. It's not a problem that we don't fail the PrepareDatastoreOp + // immediatelly (avoiding the creation of the Datastore and PreparedDatastore + // object). We will call RequestAllowToClose on the database actor once it's + // created and the child actor will respond by sending AllowToClose which + // will close the Datastore on the parent side (the closing releases the + // directory lock). + + if (gPrepareDatastoreOps) { + for (PrepareDatastoreOp* prepareDatastoreOp : *gPrepareDatastoreOps) { + MOZ_ASSERT(prepareDatastoreOp); + + // Explicitely check if a directory lock has been requested. + // Origin clearing can't be blocked by this PrepareDatastoreOp if it + // hasn't requested a directory lock yet, so we can just ignore it. + // This will also guarantee that PrepareDatastoreOp has a known origin. + // And it also ensures that the ordering is right. Without the check we + // could invalidate ops whose directory locks were requested after we + // requested a directory lock for origin clearing. + if (!prepareDatastoreOp->RequestedDirectoryLock()) { + continue; + } + + MOZ_ASSERT(prepareDatastoreOp->OriginIsKnown()); + + if (aOrigin.IsVoid() || prepareDatastoreOp->Origin() == aOrigin) { + prepareDatastoreOp->Invalidate(); + } + } + } + + if (gPreparedDatastores) { + for (auto iter = gPreparedDatastores->ConstIter(); + !iter.Done(); + iter.Next()) { + PreparedDatastore* preparedDatastore = iter.Data(); + MOZ_ASSERT(preparedDatastore); + + if (aOrigin.IsVoid() || preparedDatastore->Origin() == aOrigin) { + preparedDatastore->Invalidate(); + } + } + } + + if (gLiveDatabases) { + for (Database* database : *gLiveDatabases) { + if (aOrigin.IsVoid() || database->Origin() == aOrigin) { + // TODO: This just allows the database to close, but we can actually + // set a flag to abort any existing operations, so we can + // eventually close faster. + + database->RequestAllowToClose(); + } + } + } +} + +void +QuotaClient::AbortOperationsForProcess(ContentParentId aContentParentId) +{ + AssertIsOnBackgroundThread(); + + if (gLiveDatabases) { + for (Database* database : *gLiveDatabases) { + if (database->IsOwnedByProcess(aContentParentId)) { + database->RequestAllowToClose(); + } + } + } +} + +void +QuotaClient::StartIdleMaintenance() +{ + AssertIsOnBackgroundThread(); +} + +void +QuotaClient::StopIdleMaintenance() +{ + AssertIsOnBackgroundThread(); +} + +void +QuotaClient::ShutdownWorkThreads() +{ + AssertIsOnBackgroundThread(); + MOZ_ASSERT(!mShutdownRequested); + + mShutdownRequested = true; + + // gPrepareDatastoreOps are short lived objects running a state machine. + // The shutdown flag is checked between states, so we don't have to notify + // all the objects here. + // Allocation of a new PrepareDatastoreOp object is prevented once the + // shutdown flag is set. + // When the last PrepareDatastoreOp finishes, the gPrepareDatastoreOps array + // is destroyed. + + if (gPreparedDatastores) { + gPreparedDatastores->Clear(); + gPreparedDatastores = nullptr; + } + + if (gLiveDatabases) { + for (Database* database : *gLiveDatabases) { + database->RequestAllowToClose(); + } + } + + if (gPreparedObsevers) { + gPreparedObsevers->Clear(); + gPreparedObsevers = nullptr; + } + + // This should release any local storage related quota objects or directory + // locks. + MOZ_ALWAYS_TRUE(SpinEventLoopUntil([&]() { + // Don't have to check gPreparedDatastores since we nulled it out above. + return !gPrepareDatastoreOps && !gDatastores && !gLiveDatabases; + })); + + // And finally, shutdown the connection thread. + if (gConnectionThread) { + gConnectionThread->Shutdown(); + + gConnectionThread = nullptr; + } +} + +nsresult +QuotaClient::CreateArchivedOriginScope( + const OriginScope& aOriginScope, + nsAutoPtr& aArchivedOriginScope) +{ + AssertIsOnIOThread(); + + nsresult rv; + + nsAutoPtr archivedOriginScope; + + if (aOriginScope.IsOrigin()) { + nsCString spec; + OriginAttributes attrs; + if (NS_WARN_IF(!QuotaManager::ParseOrigin(aOriginScope.GetOrigin(), + spec, + &attrs))) { + return NS_ERROR_FAILURE; + } + + RefPtr helper = + new ArchivedOriginScopeHelper(spec, attrs, /* aPrefix */ false); + + rv = helper->BlockAndReturnArchivedOriginScope(archivedOriginScope); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } else if (aOriginScope.IsPrefix()) { + nsCString spec; + OriginAttributes attrs; + if (NS_WARN_IF(!QuotaManager::ParseOrigin(aOriginScope.GetOriginNoSuffix(), + spec, + &attrs))) { + return NS_ERROR_FAILURE; + } + + RefPtr helper = + new ArchivedOriginScopeHelper(spec, attrs, /* aPrefix */ true); + + rv = helper->BlockAndReturnArchivedOriginScope(archivedOriginScope); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } else if (aOriginScope.IsPattern()) { + archivedOriginScope = + ArchivedOriginScope::CreateFromPattern(aOriginScope.GetPattern()); + } else { + MOZ_ASSERT(aOriginScope.IsNull()); + + archivedOriginScope = ArchivedOriginScope::CreateFromNull(); + } + + MOZ_ASSERT(archivedOriginScope); + + aArchivedOriginScope = std::move(archivedOriginScope); + return NS_OK; +} + +nsresult +QuotaClient::PerformDelete(mozIStorageConnection* aConnection, + const nsACString& aSchemaName, + ArchivedOriginScope* aArchivedOriginScope) const +{ + AssertIsOnIOThread(); + MOZ_ASSERT(aConnection); + MOZ_ASSERT(aArchivedOriginScope); + + nsresult rv; + + nsCString bindingClause; + aArchivedOriginScope->GetBindingClause(bindingClause); + + nsCOMPtr stmt; + rv = aConnection->CreateStatement(NS_LITERAL_CSTRING( + "DELETE FROM ") + aSchemaName + NS_LITERAL_CSTRING(".webappsstore2") + + bindingClause + NS_LITERAL_CSTRING(";"), + getter_AddRefs(stmt)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aArchivedOriginScope->BindToStatement(stmt); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = stmt->Execute(); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + return NS_OK; +} + +NS_IMETHODIMP +QuotaClient:: +ClearPrivateBrowsingRunnable::Run() +{ + AssertIsOnBackgroundThread(); + + if (gDatastores) { + for (auto iter = gDatastores->ConstIter(); !iter.Done(); iter.Next()) { + Datastore* datastore = iter.Data(); + MOZ_ASSERT(datastore); + + if (datastore->PrivateBrowsingId()) { + datastore->PrivateBrowsingClear(); + } + } + } + + return NS_OK; +} + +NS_IMPL_ISUPPORTS(QuotaClient::Observer, nsIObserver) + +NS_IMETHODIMP +QuotaClient:: +Observer::Observe(nsISupports* aSubject, + const char* aTopic, + const char16_t* aData) +{ + MOZ_ASSERT(NS_IsMainThread()); + + if (!strcmp(aTopic, kPrivateBrowsingObserverTopic)) { + RefPtr runnable = + new ClearPrivateBrowsingRunnable(); + + MOZ_ALWAYS_SUCCEEDS( + mBackgroundEventTarget->Dispatch(runnable, NS_DISPATCH_NORMAL)); + + return NS_OK; + } + + NS_WARNING("Unknown observer topic!"); + return NS_OK; +} + +NS_IMPL_ISUPPORTS(QuotaClient::MatchFunction, mozIStorageFunction) + +NS_IMETHODIMP +QuotaClient:: +MatchFunction::OnFunctionCall(mozIStorageValueArray* aFunctionArguments, + nsIVariant** aResult) +{ + AssertIsOnIOThread(); + MOZ_ASSERT(aFunctionArguments); + MOZ_ASSERT(aResult); + + nsCString suffix; + nsresult rv = aFunctionArguments->GetUTF8String(1, suffix); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + OriginAttributes oa; + if (NS_WARN_IF(!oa.PopulateFromSuffix(suffix))) { + return NS_ERROR_FAILURE; + } + + bool result = mPattern.Matches(oa); + + RefPtr outVar(new nsVariant()); + rv = outVar->SetAsBool(result); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + outVar.forget(aResult); + return NS_OK; +} + +} // namespace dom +} // namespace mozilla diff --git a/dom/localstorage/ActorsParent.h b/dom/localstorage/ActorsParent.h new file mode 100644 index 000000000000..b59a2ee138ac --- /dev/null +++ b/dom/localstorage/ActorsParent.h @@ -0,0 +1,94 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/. */ + +#ifndef mozilla_dom_localstorage_ActorsParent_h +#define mozilla_dom_localstorage_ActorsParent_h + +namespace mozilla { + +namespace ipc { + +class PBackgroundParent; +class PrincipalInfo; + +} // namespace ipc + +namespace dom { + +class LSRequestParams; +class LSSimpleRequestParams; +class PBackgroundLSDatabaseParent; +class PBackgroundLSObserverParent; +class PBackgroundLSRequestParent; +class PBackgroundLSSimpleRequestParent; + +namespace quota { + +class Client; + +} // namespace quota + +PBackgroundLSDatabaseParent* +AllocPBackgroundLSDatabaseParent( + const mozilla::ipc::PrincipalInfo& aPrincipalInfo, + const uint32_t& aPrivateBrowsingId, + const uint64_t& aDatastoreId); + +bool +RecvPBackgroundLSDatabaseConstructor( + PBackgroundLSDatabaseParent* aActor, + const mozilla::ipc::PrincipalInfo& aPrincipalInfo, + const uint32_t& aPrivateBrowsingId, + const uint64_t& aDatastoreId); + +bool +DeallocPBackgroundLSDatabaseParent(PBackgroundLSDatabaseParent* aActor); + +PBackgroundLSObserverParent* +AllocPBackgroundLSObserverParent(const uint64_t& aObserverId); + +bool +RecvPBackgroundLSObserverConstructor(PBackgroundLSObserverParent* aActor, + const uint64_t& aObservereId); + +bool +DeallocPBackgroundLSObserverParent(PBackgroundLSObserverParent* aActor); + +PBackgroundLSRequestParent* +AllocPBackgroundLSRequestParent( + mozilla::ipc::PBackgroundParent* aBackgroundActor, + const LSRequestParams& aParams); + +bool +RecvPBackgroundLSRequestConstructor(PBackgroundLSRequestParent* aActor, + const LSRequestParams& aParams); + +bool +DeallocPBackgroundLSRequestParent(PBackgroundLSRequestParent* aActor); + +PBackgroundLSSimpleRequestParent* +AllocPBackgroundLSSimpleRequestParent(const LSSimpleRequestParams& aParams); + +bool +RecvPBackgroundLSSimpleRequestConstructor( + PBackgroundLSSimpleRequestParent* aActor, + const LSSimpleRequestParams& aParams); + +bool +DeallocPBackgroundLSSimpleRequestParent( + PBackgroundLSSimpleRequestParent* aActor); + +namespace localstorage { + +already_AddRefed +CreateQuotaClient(); + +} // namespace localstorage + +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_localstorage_ActorsParent_h diff --git a/dom/localstorage/LSDatabase.cpp b/dom/localstorage/LSDatabase.cpp new file mode 100644 index 000000000000..6464dca1736b --- /dev/null +++ b/dom/localstorage/LSDatabase.cpp @@ -0,0 +1,364 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=2 et sw=2 tw=80: */ +/* 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/. */ + +#include "LSDatabase.h" + +namespace mozilla { +namespace dom { + +namespace { + +typedef nsDataHashtable LSDatabaseHashtable; + +StaticAutoPtr gLSDatabases; + +} // namespace + +LSDatabase::LSDatabase(const nsACString& aOrigin) + : mActor(nullptr) + , mSnapshot(nullptr) + , mOrigin(aOrigin) + , mAllowedToClose(false) + , mRequestedAllowToClose(false) +{ + AssertIsOnOwningThread(); + + if (!gLSDatabases) { + gLSDatabases = new LSDatabaseHashtable(); + } + + MOZ_ASSERT(!gLSDatabases->Get(mOrigin)); + gLSDatabases->Put(mOrigin, this); +} + +LSDatabase::~LSDatabase() +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(!mSnapshot); + + if (!mAllowedToClose) { + AllowToClose(); + } + + if (mActor) { + mActor->SendDeleteMeInternal(); + MOZ_ASSERT(!mActor, "SendDeleteMeInternal should have cleared!"); + } +} + +// static +LSDatabase* +LSDatabase::Get(const nsACString& aOrigin) +{ + return gLSDatabases ? gLSDatabases->Get(aOrigin) : nullptr; +} + +void +LSDatabase::SetActor(LSDatabaseChild* aActor) +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(aActor); + MOZ_ASSERT(!mActor); + + mActor = aActor; +} + +void +LSDatabase::RequestAllowToClose() +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(!mRequestedAllowToClose); + + mRequestedAllowToClose = true; + + if (mSnapshot) { + mSnapshot->MarkDirty(); + } else { + AllowToClose(); + } +} + +void +LSDatabase::NoteFinishedSnapshot(LSSnapshot* aSnapshot) +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(aSnapshot == mSnapshot); + + mSnapshot = nullptr; + + if (mRequestedAllowToClose) { + AllowToClose(); + } +} + +nsresult +LSDatabase::GetLength(LSObject* aObject, + uint32_t* aResult) +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(aObject); + MOZ_ASSERT(mActor); + MOZ_ASSERT(!mAllowedToClose); + + nsresult rv = EnsureSnapshot(aObject); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = mSnapshot->GetLength(aResult); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + return NS_OK; +} + +nsresult +LSDatabase::GetKey(LSObject* aObject, + uint32_t aIndex, + nsAString& aResult) +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(aObject); + MOZ_ASSERT(mActor); + MOZ_ASSERT(!mAllowedToClose); + + nsresult rv = EnsureSnapshot(aObject); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = mSnapshot->GetKey(aIndex, aResult); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + return NS_OK; +} + +nsresult +LSDatabase::GetItem(LSObject* aObject, + const nsAString& aKey, + nsAString& aResult) +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(aObject); + MOZ_ASSERT(mActor); + MOZ_ASSERT(!mAllowedToClose); + + nsresult rv = EnsureSnapshot(aObject); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = mSnapshot->GetItem(aKey, aResult); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + return NS_OK; +} + +nsresult +LSDatabase::GetKeys(LSObject* aObject, + nsTArray& aKeys) +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(aObject); + MOZ_ASSERT(mActor); + MOZ_ASSERT(!mAllowedToClose); + + nsresult rv = EnsureSnapshot(aObject); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = mSnapshot->GetKeys(aKeys); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + return NS_OK; +} + +nsresult +LSDatabase::SetItem(LSObject* aObject, + const nsAString& aKey, + const nsAString& aValue, + LSNotifyInfo& aNotifyInfo) +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(aObject); + MOZ_ASSERT(mActor); + MOZ_ASSERT(!mAllowedToClose); + + nsresult rv = EnsureSnapshot(aObject); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = mSnapshot->SetItem(aKey, aValue, aNotifyInfo); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + return NS_OK; +} + +nsresult +LSDatabase::RemoveItem(LSObject* aObject, + const nsAString& aKey, + LSNotifyInfo& aNotifyInfo) +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(aObject); + MOZ_ASSERT(mActor); + MOZ_ASSERT(!mAllowedToClose); + + nsresult rv = EnsureSnapshot(aObject); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = mSnapshot->RemoveItem(aKey, aNotifyInfo); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + return NS_OK; +} + +nsresult +LSDatabase::Clear(LSObject* aObject, + LSNotifyInfo& aNotifyInfo) +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(aObject); + MOZ_ASSERT(mActor); + MOZ_ASSERT(!mAllowedToClose); + + nsresult rv = EnsureSnapshot(aObject); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = mSnapshot->Clear(aNotifyInfo); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + return NS_OK; +} + +nsresult +LSDatabase::BeginExplicitSnapshot(LSObject* aObject) +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(aObject); + MOZ_ASSERT(mActor); + MOZ_ASSERT(!mAllowedToClose); + + if (mSnapshot) { + return NS_ERROR_ALREADY_INITIALIZED; + } + + nsresult rv = EnsureSnapshot(aObject, /* aExplicit */ true); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + return NS_OK; +} + +nsresult +LSDatabase::EndExplicitSnapshot(LSObject* aObject) +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(aObject); + MOZ_ASSERT(mActor); + MOZ_ASSERT(!mAllowedToClose); + + if (!mSnapshot) { + return NS_ERROR_NOT_INITIALIZED; + } + + MOZ_ASSERT(mSnapshot->Explicit()); + + nsresult rv = mSnapshot->End(); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + return NS_OK; +} + +nsresult +LSDatabase::EnsureSnapshot(LSObject* aObject, + bool aExplicit) +{ + MOZ_ASSERT(aObject); + MOZ_ASSERT(mActor); + MOZ_ASSERT_IF(mSnapshot, !aExplicit); + MOZ_ASSERT(!mAllowedToClose); + + if (mSnapshot) { + return NS_OK; + } + + RefPtr snapshot = new LSSnapshot(this); + + LSSnapshotChild* actor = new LSSnapshotChild(snapshot); + + LSSnapshotInitInfo initInfo; + bool ok = + mActor->SendPBackgroundLSSnapshotConstructor(actor, + aObject->DocumentURI(), + /* increasePeakUsage */ true, + /* requestedSize */ 131072, + /* minSize */ 4096, + &initInfo); + if (NS_WARN_IF(!ok)) { + return NS_ERROR_FAILURE; + } + + snapshot->SetActor(actor); + + // This add refs snapshot. + nsresult rv = snapshot->Init(initInfo, aExplicit); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + // This is cleared in LSSnapshot::Run() before the snapshot is destroyed. + mSnapshot = snapshot; + + return NS_OK; +} + +void +LSDatabase::AllowToClose() +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(!mAllowedToClose); + MOZ_ASSERT(!mSnapshot); + + mAllowedToClose = true; + + if (mActor) { + mActor->SendAllowToClose(); + } + + MOZ_ASSERT(gLSDatabases); + MOZ_ASSERT(gLSDatabases->Get(mOrigin)); + gLSDatabases->Remove(mOrigin); + + if (!gLSDatabases->Count()) { + gLSDatabases = nullptr; + } +} + +} // namespace dom +} // namespace mozilla diff --git a/dom/localstorage/LSDatabase.h b/dom/localstorage/LSDatabase.h new file mode 100644 index 000000000000..ec7435b789fc --- /dev/null +++ b/dom/localstorage/LSDatabase.h @@ -0,0 +1,120 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/. */ + +#ifndef mozilla_dom_localstorage_LSDatabase_h +#define mozilla_dom_localstorage_LSDatabase_h + +namespace mozilla { +namespace dom { + +class LSDatabaseChild; +class LSSnapshot; + +class LSDatabase final +{ + LSDatabaseChild* mActor; + + LSSnapshot* mSnapshot; + + const nsCString mOrigin; + + bool mAllowedToClose; + bool mRequestedAllowToClose; + +public: + explicit LSDatabase(const nsACString& aOrigin); + + static LSDatabase* + Get(const nsACString& aOrigin); + + NS_INLINE_DECL_REFCOUNTING(LSDatabase) + + void + AssertIsOnOwningThread() const + { + NS_ASSERT_OWNINGTHREAD(LSDatabase); + } + + void + SetActor(LSDatabaseChild* aActor); + + void + ClearActor() + { + AssertIsOnOwningThread(); + MOZ_ASSERT(mActor); + + mActor = nullptr; + } + + bool + IsAllowedToClose() const + { + AssertIsOnOwningThread(); + + return mAllowedToClose; + } + + void + RequestAllowToClose(); + + void + NoteFinishedSnapshot(LSSnapshot* aSnapshot); + + nsresult + GetLength(LSObject* aObject, + uint32_t* aResult); + + nsresult + GetKey(LSObject* aObject, + uint32_t aIndex, + nsAString& aResult); + + nsresult + GetItem(LSObject* aObject, + const nsAString& aKey, + nsAString& aResult); + + nsresult + GetKeys(LSObject* aObject, + nsTArray& aKeys); + + nsresult + SetItem(LSObject* aObject, + const nsAString& aKey, + const nsAString& aValue, + LSNotifyInfo& aNotifyInfo); + + nsresult + RemoveItem(LSObject* aObject, + const nsAString& aKey, + LSNotifyInfo& aNotifyInfo); + + nsresult + Clear(LSObject* aObject, + LSNotifyInfo& aNotifyInfo); + + nsresult + BeginExplicitSnapshot(LSObject* aObject); + + nsresult + EndExplicitSnapshot(LSObject* aObject); + +private: + ~LSDatabase(); + + nsresult + EnsureSnapshot(LSObject* aObject, + bool aExplicit = false); + + void + AllowToClose(); +}; + +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_localstorage_LSDatabase_h diff --git a/dom/localstorage/LSObject.cpp b/dom/localstorage/LSObject.cpp new file mode 100644 index 000000000000..3203818ee599 --- /dev/null +++ b/dom/localstorage/LSObject.cpp @@ -0,0 +1,1140 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=2 et sw=2 tw=80: */ +/* 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/. */ + +#include "LSObject.h" + +#include "ActorsChild.h" +#include "IPCBlobInputStreamThread.h" +#include "LocalStorageCommon.h" +#include "mozilla/ThreadEventQueue.h" +#include "mozilla/dom/quota/QuotaManager.h" +#include "mozilla/ipc/BackgroundChild.h" +#include "mozilla/ipc/BackgroundUtils.h" +#include "mozilla/ipc/PBackgroundChild.h" +#include "nsContentUtils.h" +#include "nsIScriptObjectPrincipal.h" +#include "nsThread.h" + +namespace mozilla { +namespace dom { + +namespace { + +class RequestHelper; + +StaticMutex gRequestHelperMutex; +RequestHelper* gRequestHelper = nullptr; + +/** + * Main-thread helper that implements the blocking logic required by + * LocalStorage's synchronous semantics. StartAndReturnResponse pushes an + * event queue which is a new event target and spins its nested event loop until + * a result is received or an abort is necessary due to a PContent-managed sync + * IPC message being received. Note that because the event queue is its own + * event target, there is no re-entrancy. Normal main-thread runnables will not + * get a chance to run. See StartAndReturnResponse() for info on this choice. + * + * The normal life-cycle of this method looks like: + * - Main Thread: LSObject::DoRequestSynchronously creates a RequestHelper and + * invokes StartAndReturnResponse(). It pushes the event queue and Dispatches + * the RequestHelper to the DOM File Thread. + * - DOM File Thread: RequestHelper::Run is called, invoking Start() which + * invokes LSObject::StartRequest, which gets-or-creates the PBackground actor + * if necessary (which may dispatch a runnable to the nested event queue on + * the main thread), sends LSRequest constructor which is provided with a + * callback reference to the RequestHelper. State advances to ResponsePending. + * - DOM File Thread:: LSRequestChild::Recv__delete__ is received, which invokes + * RequestHelepr::OnResponse, advancing the state to Finishing and dispatching + * RequestHelper to its own nested event target. + * - Main Thread: RequestHelper::Run is called, invoking Finish() which advances + * the state to Complete and sets mWaiting to false, allowing the nested event + * loop being spun by StartAndReturnResponse to cease spinning and return the + * received response. + * + * See LocalStorageCommon.h for high-level context and method comments for + * low-level details. + */ +class RequestHelper final + : public Runnable + , public LSRequestChildCallback +{ + enum class State + { + /** + * The RequestHelper has been created and dispatched to the DOM File Thread. + */ + Initial, + /** + * Start() has been invoked on the DOM File Thread and + * LSObject::StartRequest has been invoked from there, sending an IPC + * message to PBackground to service the request. We stay in this state + * until a response is received. + */ + ResponsePending, + /** + * A response has been received and RequestHelper has been dispatched back + * to the nested event loop to call Finish(). + */ + Finishing, + /** + * Finish() has been called on the main thread. The nested event loop will + * terminate imminently and the received response returned to the caller of + * StartAndReturnResponse. + */ + Complete + }; + + // The object we are issuing a request on behalf of. Present because of the + // need to invoke LSObject::StartRequest off the main thread. Dropped on + // return to the main-thread in Finish(). + RefPtr mObject; + // The thread the RequestHelper was created on. This should be the main + // thread. + nsCOMPtr mOwningEventTarget; + // The pushed event queue that we use to spin the event loop without + // processing any of the events dispatched at the mOwningEventTarget (which + // would result in re-entrancy and violate LocalStorage semantics). + nsCOMPtr mNestedEventTarget; + // The IPC actor handling the request with standard IPC allocation rules. + // Our reference is nulled in OnResponse which corresponds to the actor's + // __destroy__ method. + LSRequestChild* mActor; + const LSRequestParams mParams; + LSRequestResponse mResponse; + nsresult mResultCode; + State mState; + // Control flag for the nested event loop; once set to false, the loop ends. + bool mWaiting; + +public: + RequestHelper(LSObject* aObject, + const LSRequestParams& aParams) + : Runnable("dom::RequestHelper") + , mObject(aObject) + , mOwningEventTarget(GetCurrentThreadEventTarget()) + , mActor(nullptr) + , mParams(aParams) + , mResultCode(NS_OK) + , mState(State::Initial) + , mWaiting(true) + { + StaticMutexAutoLock lock(gRequestHelperMutex); + gRequestHelper = this; + } + + bool + IsOnOwningThread() const + { + MOZ_ASSERT(mOwningEventTarget); + + bool current; + return + NS_SUCCEEDED(mOwningEventTarget->IsOnCurrentThread(¤t)) && current; + } + + void + AssertIsOnOwningThread() const + { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(IsOnOwningThread()); + } + + // Used for requests from the parent process to the parent process; in that + // case we want ActorsParent to know our event-target and this is better than + // trying to tunnel the pointer through IPC. + const nsCOMPtr& + GetSyncLoopEventTarget() const + { + MOZ_ASSERT(XRE_IsParentProcess()); + + return mNestedEventTarget; + } + + nsresult + StartAndReturnResponse(LSRequestResponse& aResponse); + + nsresult + CancelOnAnyThread(); + +private: + ~RequestHelper() + { + StaticMutexAutoLock lock(gRequestHelperMutex); + gRequestHelper = nullptr; + } + + nsresult + Start(); + + void + Finish(); + + NS_DECL_ISUPPORTS_INHERITED + + NS_DECL_NSIRUNNABLE + + // LSRequestChildCallback + void + OnResponse(const LSRequestResponse& aResponse) override; +}; + +} // namespace + +LSObject::LSObject(nsPIDOMWindowInner* aWindow, + nsIPrincipal* aPrincipal) + : Storage(aWindow, aPrincipal) + , mPrivateBrowsingId(0) + , mInExplicitSnapshot(false) +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(NextGenLocalStorageEnabled()); +} + +LSObject::~LSObject() +{ + AssertIsOnOwningThread(); + + DropObserver(); +} + +// static +nsresult +LSObject::CreateForWindow(nsPIDOMWindowInner* aWindow, + Storage** aStorage) +{ + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(aWindow); + MOZ_ASSERT(aStorage); + MOZ_ASSERT(NextGenLocalStorageEnabled()); + MOZ_ASSERT(nsContentUtils::StorageAllowedForWindow(aWindow) > + nsContentUtils::StorageAccess::eDeny); + + nsCOMPtr sop = do_QueryInterface(aWindow); + MOZ_ASSERT(sop); + + nsCOMPtr principal = sop->GetPrincipal(); + if (NS_WARN_IF(!principal)) { + return NS_ERROR_FAILURE; + } + + if (nsContentUtils::IsSystemPrincipal(principal)) { + return NS_ERROR_NOT_AVAILABLE; + } + + // localStorage is not available on some pages on purpose, for example + // about:home. Match the old implementation by using GenerateOriginKey + // for the check. + nsCString dummyOriginAttrSuffix; + nsCString dummyOriginKey; + nsresult rv = GenerateOriginKey(principal, + dummyOriginAttrSuffix, + dummyOriginKey); + if (NS_FAILED(rv)) { + return NS_ERROR_NOT_AVAILABLE; + } + + nsAutoPtr principalInfo(new PrincipalInfo()); + rv = PrincipalToPrincipalInfo(principal, principalInfo); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + MOZ_ASSERT(principalInfo->type() == PrincipalInfo::TContentPrincipalInfo); + + nsCString origin; + rv = QuotaManager::GetInfoFromPrincipal(principal, nullptr, nullptr, &origin); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + uint32_t privateBrowsingId; + rv = principal->GetPrivateBrowsingId(&privateBrowsingId); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + nsString documentURI; + if (nsCOMPtr doc = aWindow->GetExtantDoc()) { + rv = doc->GetDocumentURI(documentURI); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } + + RefPtr object = new LSObject(aWindow, principal); + object->mPrincipalInfo = std::move(principalInfo); + object->mPrivateBrowsingId = privateBrowsingId; + object->mOrigin = origin; + object->mDocumentURI = documentURI; + + object.forget(aStorage); + return NS_OK; +} + +// static +nsresult +LSObject::CreateForPrincipal(nsPIDOMWindowInner* aWindow, + nsIPrincipal* aPrincipal, + const nsAString& aDocumentURI, + bool aPrivate, + LSObject** aObject) +{ + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(aPrincipal); + MOZ_ASSERT(aObject); + + nsCString dummyOriginAttrSuffix; + nsCString dummyOriginKey; + nsresult rv = GenerateOriginKey(aPrincipal, + dummyOriginAttrSuffix, + dummyOriginKey); + if (NS_FAILED(rv)) { + return NS_ERROR_NOT_AVAILABLE; + } + + nsAutoPtr principalInfo(new PrincipalInfo()); + rv = PrincipalToPrincipalInfo(aPrincipal, principalInfo); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + MOZ_ASSERT(principalInfo->type() == PrincipalInfo::TContentPrincipalInfo || + principalInfo->type() == PrincipalInfo::TSystemPrincipalInfo); + + nsCString origin; + + if (principalInfo->type() == PrincipalInfo::TSystemPrincipalInfo) { + QuotaManager::GetInfoForChrome(nullptr, nullptr, &origin); + } else { + rv = QuotaManager::GetInfoFromPrincipal(aPrincipal, + nullptr, + nullptr, + &origin); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } + + RefPtr object = new LSObject(aWindow, aPrincipal); + object->mPrincipalInfo = std::move(principalInfo); + object->mPrivateBrowsingId = aPrivate ? 1 : 0; + object->mOrigin = origin; + object->mDocumentURI = aDocumentURI; + + object.forget(aObject); + return NS_OK; +} + +// static +already_AddRefed +LSObject::GetSyncLoopEventTarget() +{ + RefPtr helper; + + { + StaticMutexAutoLock lock(gRequestHelperMutex); + helper = gRequestHelper; + } + + nsCOMPtr target; + if (helper) { + target = helper->GetSyncLoopEventTarget(); + } + + return target.forget(); +} + +// static +void +LSObject::CancelSyncLoop() +{ + RefPtr helper; + + { + StaticMutexAutoLock lock(gRequestHelperMutex); + helper = gRequestHelper; + } + + if (helper) { + Unused << NS_WARN_IF(NS_FAILED(helper->CancelOnAnyThread())); + } +} + +LSRequestChild* +LSObject::StartRequest(nsIEventTarget* aMainEventTarget, + const LSRequestParams& aParams, + LSRequestChildCallback* aCallback) +{ + AssertIsOnDOMFileThread(); + + PBackgroundChild* backgroundActor = + BackgroundChild::GetOrCreateForCurrentThread(aMainEventTarget); + if (NS_WARN_IF(!backgroundActor)) { + return nullptr; + } + + LSRequestChild* actor = new LSRequestChild(aCallback); + + backgroundActor->SendPBackgroundLSRequestConstructor(actor, aParams); + + return actor; +} + +Storage::StorageType +LSObject::Type() const +{ + AssertIsOnOwningThread(); + + return eLocalStorage; +} + +bool +LSObject::IsForkOf(const Storage* aStorage) const +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(aStorage); + + if (aStorage->Type() != eLocalStorage) { + return false; + } + + return static_cast(aStorage)->mOrigin == mOrigin; +} + +int64_t +LSObject::GetOriginQuotaUsage() const +{ + AssertIsOnOwningThread(); + + // It's not necessary to return an actual value here. This method is + // implemented only because the SessionStore currently needs it to cap the + // amount of data it persists to disk (via nsIDOMWindowUtils.getStorageUsage). + // Any callers that want to know about storage usage should be asking + // QuotaManager directly. + // + // Note: This may change as LocalStorage is repurposed to be the new + // SessionStorage backend. + return 0; +} + +uint32_t +LSObject::GetLength(nsIPrincipal& aSubjectPrincipal, + ErrorResult& aError) +{ + AssertIsOnOwningThread(); + + if (!CanUseStorage(aSubjectPrincipal)) { + aError.Throw(NS_ERROR_DOM_SECURITY_ERR); + return 0; + } + + nsresult rv = EnsureDatabase(); + if (NS_WARN_IF(NS_FAILED(rv))) { + aError.Throw(rv); + return 0; + } + + uint32_t result; + rv = mDatabase->GetLength(this, &result); + if (NS_WARN_IF(NS_FAILED(rv))) { + aError.Throw(rv); + return 0; + } + + return result; +} + +void +LSObject::Key(uint32_t aIndex, + nsAString& aResult, + nsIPrincipal& aSubjectPrincipal, + ErrorResult& aError) +{ + AssertIsOnOwningThread(); + + if (!CanUseStorage(aSubjectPrincipal)) { + aError.Throw(NS_ERROR_DOM_SECURITY_ERR); + return; + } + + nsresult rv = EnsureDatabase(); + if (NS_WARN_IF(NS_FAILED(rv))) { + aError.Throw(rv); + return; + } + + nsString result; + rv = mDatabase->GetKey(this, aIndex, result); + if (NS_WARN_IF(NS_FAILED(rv))) { + aError.Throw(rv); + return; + } + + aResult = result; +} + +void +LSObject::GetItem(const nsAString& aKey, + nsAString& aResult, + nsIPrincipal& aSubjectPrincipal, + ErrorResult& aError) +{ + AssertIsOnOwningThread(); + + if (!CanUseStorage(aSubjectPrincipal)) { + aError.Throw(NS_ERROR_DOM_SECURITY_ERR); + return; + } + + nsresult rv = EnsureDatabase(); + if (NS_WARN_IF(NS_FAILED(rv))) { + aError.Throw(rv); + return; + } + + nsString result; + rv = mDatabase->GetItem(this, aKey, result); + if (NS_WARN_IF(NS_FAILED(rv))) { + aError.Throw(rv); + return; + } + + aResult = result; +} + +void +LSObject::GetSupportedNames(nsTArray& aNames) +{ + AssertIsOnOwningThread(); + + if (!CanUseStorage(*nsContentUtils::SubjectPrincipal())) { + // Return just an empty array. + aNames.Clear(); + return; + } + + nsresult rv = EnsureDatabase(); + if (NS_WARN_IF(NS_FAILED(rv))) { + return; + } + + rv = mDatabase->GetKeys(this, aNames); + if (NS_WARN_IF(NS_FAILED(rv))) { + return; + } +} + +void +LSObject::SetItem(const nsAString& aKey, + const nsAString& aValue, + nsIPrincipal& aSubjectPrincipal, + ErrorResult& aError) +{ + AssertIsOnOwningThread(); + + if (!CanUseStorage(aSubjectPrincipal)) { + aError.Throw(NS_ERROR_DOM_SECURITY_ERR); + return; + } + + nsresult rv = EnsureDatabase(); + if (NS_WARN_IF(NS_FAILED(rv))) { + aError.Throw(rv); + return; + } + + LSNotifyInfo info; + rv = mDatabase->SetItem(this, aKey, aValue, info); + if (rv == NS_ERROR_FILE_NO_DEVICE_SPACE) { + rv = NS_ERROR_DOM_QUOTA_EXCEEDED_ERR; + } + if (NS_WARN_IF(NS_FAILED(rv))) { + aError.Throw(rv); + return; + } + + if (info.changed()) { + OnChange(aKey, info.oldValue(), aValue); + } +} + +void +LSObject::RemoveItem(const nsAString& aKey, + nsIPrincipal& aSubjectPrincipal, + ErrorResult& aError) +{ + AssertIsOnOwningThread(); + + if (!CanUseStorage(aSubjectPrincipal)) { + aError.Throw(NS_ERROR_DOM_SECURITY_ERR); + return; + } + + nsresult rv = EnsureDatabase(); + if (NS_WARN_IF(NS_FAILED(rv))) { + aError.Throw(rv); + return; + } + + LSNotifyInfo info; + rv = mDatabase->RemoveItem(this, aKey, info); + if (NS_WARN_IF(NS_FAILED(rv))) { + aError.Throw(rv); + return; + } + + if (info.changed()) { + OnChange(aKey, info.oldValue(), VoidString()); + } +} + +void +LSObject::Clear(nsIPrincipal& aSubjectPrincipal, + ErrorResult& aError) +{ + AssertIsOnOwningThread(); + + if (!CanUseStorage(aSubjectPrincipal)) { + aError.Throw(NS_ERROR_DOM_SECURITY_ERR); + return; + } + + nsresult rv = EnsureDatabase(); + if (NS_WARN_IF(NS_FAILED(rv))) { + aError.Throw(rv); + return; + } + + LSNotifyInfo info; + rv = mDatabase->Clear(this, info); + if (NS_WARN_IF(NS_FAILED(rv))) { + aError.Throw(rv); + return; + } + + if (info.changed()) { + OnChange(VoidString(), VoidString(), VoidString()); + } +} + +void +LSObject::Open(nsIPrincipal& aSubjectPrincipal, + ErrorResult& aError) +{ + AssertIsOnOwningThread(); + + if (!CanUseStorage(aSubjectPrincipal)) { + aError.Throw(NS_ERROR_DOM_SECURITY_ERR); + return; + } + + nsresult rv = EnsureDatabase(); + if (NS_WARN_IF(NS_FAILED(rv))) { + aError.Throw(rv); + return; + } +} + +void +LSObject::Close(nsIPrincipal& aSubjectPrincipal, + ErrorResult& aError) +{ + AssertIsOnOwningThread(); + + if (!CanUseStorage(aSubjectPrincipal)) { + aError.Throw(NS_ERROR_DOM_SECURITY_ERR); + return; + } + + DropDatabase(); +} + +void +LSObject::BeginExplicitSnapshot(nsIPrincipal& aSubjectPrincipal, + ErrorResult& aError) +{ + AssertIsOnOwningThread(); + + if (!CanUseStorage(aSubjectPrincipal)) { + aError.Throw(NS_ERROR_DOM_SECURITY_ERR); + return; + } + + if (mInExplicitSnapshot) { + aError.Throw(NS_ERROR_ALREADY_INITIALIZED); + return; + } + + nsresult rv = EnsureDatabase(); + if (NS_WARN_IF(NS_FAILED(rv))) { + aError.Throw(rv); + return; + } + + rv = mDatabase->BeginExplicitSnapshot(this); + if (NS_WARN_IF(NS_FAILED(rv))) { + aError.Throw(rv); + return; + } + + mInExplicitSnapshot = true; +} + +void +LSObject::EndExplicitSnapshot(nsIPrincipal& aSubjectPrincipal, + ErrorResult& aError) +{ + AssertIsOnOwningThread(); + + if (!CanUseStorage(aSubjectPrincipal)) { + aError.Throw(NS_ERROR_DOM_SECURITY_ERR); + return; + } + + if (!mInExplicitSnapshot) { + aError.Throw(NS_ERROR_NOT_INITIALIZED); + return; + } + + nsresult rv = EndExplicitSnapshotInternal(); + if (NS_WARN_IF(NS_FAILED(rv))) { + aError.Throw(rv); + return; + } +} + +NS_IMPL_ADDREF_INHERITED(LSObject, Storage) +NS_IMPL_RELEASE_INHERITED(LSObject, Storage) + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(LSObject) +NS_INTERFACE_MAP_END_INHERITING(Storage) + +NS_IMPL_CYCLE_COLLECTION_CLASS(LSObject) + +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(LSObject, Storage) + tmp->AssertIsOnOwningThread(); +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(LSObject, Storage) + tmp->AssertIsOnOwningThread(); + tmp->DropDatabase(); +NS_IMPL_CYCLE_COLLECTION_UNLINK_END + +nsresult +LSObject::DoRequestSynchronously(const LSRequestParams& aParams, + LSRequestResponse& aResponse) +{ + // We don't need this yet, but once the request successfully finishes, it's + // too late to initialize PBackground child on the owning thread, because + // it can fail and parent would keep an extra strong ref to the datastore or + // observer. + PBackgroundChild* backgroundActor = + BackgroundChild::GetOrCreateForCurrentThread(); + if (NS_WARN_IF(!backgroundActor)) { + return NS_ERROR_FAILURE; + } + + RefPtr helper = new RequestHelper(this, aParams); + + // This will start and finish the request on the DOM File thread. + // The owning thread is synchronously blocked while the request is + // asynchronously processed on the DOM File thread. + nsresult rv = helper->StartAndReturnResponse(aResponse); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (aResponse.type() == LSRequestResponse::Tnsresult) { + nsresult errorCode = aResponse.get_nsresult(); + + if (errorCode == NS_ERROR_FILE_NO_DEVICE_SPACE) { + errorCode = NS_ERROR_DOM_QUOTA_EXCEEDED_ERR; + } + + return errorCode; + } + + return NS_OK; +} + +nsresult +LSObject::EnsureDatabase() +{ + AssertIsOnOwningThread(); + + if (mDatabase && !mDatabase->IsAllowedToClose()) { + return NS_OK; + } + + mDatabase = LSDatabase::Get(mOrigin); + + if (mDatabase) { + MOZ_ASSERT(!mDatabase->IsAllowedToClose()); + return NS_OK; + } + + // We don't need this yet, but once the request successfully finishes, it's + // too late to initialize PBackground child on the owning thread, because + // it can fail and parent would keep an extra strong ref to the datastore. + PBackgroundChild* backgroundActor = + BackgroundChild::GetOrCreateForCurrentThread(); + if (NS_WARN_IF(!backgroundActor)) { + return NS_ERROR_FAILURE; + } + + LSRequestPrepareDatastoreParams params; + params.principalInfo() = *mPrincipalInfo; + params.createIfNotExists() = true; + + LSRequestResponse response; + + nsresult rv = DoRequestSynchronously(params, response); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + MOZ_ASSERT(response.type() == + LSRequestResponse::TLSRequestPrepareDatastoreResponse); + + const LSRequestPrepareDatastoreResponse& prepareDatastoreResponse = + response.get_LSRequestPrepareDatastoreResponse(); + + const NullableDatastoreId& datastoreId = prepareDatastoreResponse.datastoreId(); + + MOZ_ASSERT(datastoreId.type() == NullableDatastoreId::Tuint64_t); + + // The datastore is now ready on the parent side (prepared by the asynchronous + // request on the DOM File thread). + // Let's create a direct connection to the datastore (through a database + // actor) from the owning thread. + // Note that we now can't error out, otherwise parent will keep an extra + // strong reference to the datastore. + + RefPtr database = new LSDatabase(mOrigin); + + LSDatabaseChild* actor = new LSDatabaseChild(database); + + MOZ_ALWAYS_TRUE( + backgroundActor->SendPBackgroundLSDatabaseConstructor(actor, + *mPrincipalInfo, + mPrivateBrowsingId, + datastoreId)); + + database->SetActor(actor); + + mDatabase = std::move(database); + + return NS_OK; +} + +void +LSObject::DropDatabase() +{ + AssertIsOnOwningThread(); + + if (mInExplicitSnapshot) { + nsresult rv = EndExplicitSnapshotInternal(); + Unused << NS_WARN_IF(NS_FAILED(rv)); + } + + mDatabase = nullptr; +} + +nsresult +LSObject::EnsureObserver() +{ + AssertIsOnOwningThread(); + + if (mObserver) { + return NS_OK; + } + + mObserver = LSObserver::Get(mOrigin); + + if (mObserver) { + return NS_OK; + } + + LSRequestPrepareObserverParams params; + params.principalInfo() = *mPrincipalInfo; + + LSRequestResponse response; + + nsresult rv = DoRequestSynchronously(params, response); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + MOZ_ASSERT(response.type() == + LSRequestResponse::TLSRequestPrepareObserverResponse); + + const LSRequestPrepareObserverResponse& prepareObserverResponse = + response.get_LSRequestPrepareObserverResponse(); + + uint64_t observerId = prepareObserverResponse.observerId(); + + // The obsserver is now ready on the parent side (prepared by the asynchronous + // request on the DOM File thread). + // Let's create a direct connection to the observer (through an observer + // actor) from the owning thread. + // Note that we now can't error out, otherwise parent will keep an extra + // strong reference to the observer. + + PBackgroundChild* backgroundActor = BackgroundChild::GetForCurrentThread(); + MOZ_ASSERT(backgroundActor); + + RefPtr observer = new LSObserver(mOrigin); + + LSObserverChild* actor = new LSObserverChild(observer); + + MOZ_ALWAYS_TRUE( + backgroundActor->SendPBackgroundLSObserverConstructor(actor, observerId)); + + observer->SetActor(actor); + + mObserver = std::move(observer); + + return NS_OK; +} + +void +LSObject::DropObserver() +{ + AssertIsOnOwningThread(); + + if (mObserver) { + mObserver = nullptr; + } +} + +void +LSObject::OnChange(const nsAString& aKey, + const nsAString& aOldValue, + const nsAString& aNewValue) +{ + AssertIsOnOwningThread(); + + NotifyChange(/* aStorage */ this, + Principal(), + aKey, + aOldValue, + aNewValue, + /* aStorageType */ kLocalStorageType, + mDocumentURI, + /* aIsPrivate */ !!mPrivateBrowsingId, + /* aImmediateDispatch */ false); +} + +nsresult +LSObject::EndExplicitSnapshotInternal() +{ + AssertIsOnOwningThread(); + + // Can be only called if the mInExplicitSnapshot flag is true. + // An explicit snapshot must have been created. + MOZ_ASSERT(mInExplicitSnapshot); + + // If an explicit snapshot have been created then mDatabase must be not null. + // DropDatabase could be called in the meatime, but that would set + // mInExplicitSnapshot to false. EnsureDatabase could be called in the + // meantime too, but that can't set mDatabase to null or to a new value. See + // the comment below. + MOZ_ASSERT(mDatabase); + + // Existence of a snapshot prevents the database from allowing to close. See + // LSDatabase::RequestAllowToClose and LSDatabase::NoteFinishedSnapshot. + // If the database is not allowed to close then mDatabase could not have been + // nulled out or set to a new value. See EnsureDatabase. + MOZ_ASSERT(!mDatabase->IsAllowedToClose()); + + nsresult rv = mDatabase->EndExplicitSnapshot(this); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + mInExplicitSnapshot = false; + + return NS_OK; +} + +void +LSObject::LastRelease() +{ + AssertIsOnOwningThread(); + + DropDatabase(); +} + +nsresult +RequestHelper::StartAndReturnResponse(LSRequestResponse& aResponse) +{ + AssertIsOnOwningThread(); + + // Normally, we would use the standard way of blocking the thread using + // a monitor. + // The problem is that BackgroundChild::GetOrCreateForCurrentThread() + // called on the DOM File thread may dispatch a runnable to the main + // thread to finish initialization of PBackground. A monitor would block + // the main thread and the runnable would never get executed causing the + // helper to be stuck in a wait loop. + // However, BackgroundChild::GetOrCreateForCurrentThread() supports passing + // a custom main event target, so we can create a nested event target and + // spin the event loop. Nothing can dispatch to the nested event target + // except BackgroundChild::GetOrCreateForCurrentThread(), so spinning of the + // event loop can't fire any other events. + // This way the thread is synchronously blocked in a safe manner and the + // runnable gets executed. + { + auto thread = static_cast(NS_GetCurrentThread()); + + auto queue = + static_cast*>(thread->EventQueue()); + + mNestedEventTarget = queue->PushEventQueue(); + MOZ_ASSERT(mNestedEventTarget); + + auto autoPopEventQueue = mozilla::MakeScopeExit([&] { + queue->PopEventQueue(mNestedEventTarget); + }); + + nsCOMPtr domFileThread = + IPCBlobInputStreamThread::GetOrCreate(); + if (NS_WARN_IF(!domFileThread)) { + return NS_ERROR_FAILURE; + } + + nsresult rv = domFileThread->Dispatch(this, NS_DISPATCH_NORMAL); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + MOZ_ALWAYS_TRUE(SpinEventLoopUntil([&]() { + return !mWaiting; + })); + } + + if (NS_WARN_IF(NS_FAILED(mResultCode))) { + return mResultCode; + } + + aResponse = std::move(mResponse); + return NS_OK; +} + +nsresult +RequestHelper::CancelOnAnyThread() +{ + RefPtr self = this; + + RefPtr runnable = NS_NewRunnableFunction( + "RequestHelper::CancelOnAnyThread", + [self] () { + LSRequestChild* actor = self->mActor; + if (actor && !actor->Finishing()) { + actor->SendCancel(); + } + }); + + nsCOMPtr domFileThread = + IPCBlobInputStreamThread::GetOrCreate(); + if (NS_WARN_IF(!domFileThread)) { + return NS_ERROR_FAILURE; + } + + nsresult rv = domFileThread->Dispatch(runnable, NS_DISPATCH_NORMAL); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + return NS_OK; +} + +nsresult +RequestHelper::Start() +{ + AssertIsOnDOMFileThread(); + MOZ_ASSERT(mState == State::Initial); + + mState = State::ResponsePending; + + LSRequestChild* actor = + mObject->StartRequest(mNestedEventTarget, mParams, this); + if (NS_WARN_IF(!actor)) { + return NS_ERROR_FAILURE; + } + + mActor = actor; + + return NS_OK; +} + +void +RequestHelper::Finish() +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(mState == State::Finishing); + + mObject = nullptr; + + mWaiting = false; + + mState = State::Complete; +} + +NS_IMPL_ISUPPORTS_INHERITED0(RequestHelper, Runnable) + +NS_IMETHODIMP +RequestHelper::Run() +{ + nsresult rv; + + switch (mState) { + case State::Initial: + rv = Start(); + break; + + case State::Finishing: + Finish(); + return NS_OK; + + default: + MOZ_CRASH("Bad state!"); + } + + if (NS_WARN_IF(NS_FAILED(rv)) && mState != State::Finishing) { + if (NS_SUCCEEDED(mResultCode)) { + mResultCode = rv; + } + + mState = State::Finishing; + + if (IsOnOwningThread()) { + Finish(); + } else { + MOZ_ALWAYS_SUCCEEDS(mNestedEventTarget->Dispatch(this, + NS_DISPATCH_NORMAL)); + } + } + + return NS_OK; +} + +void +RequestHelper::OnResponse(const LSRequestResponse& aResponse) +{ + AssertIsOnDOMFileThread(); + MOZ_ASSERT(mState == State::ResponsePending); + + mActor = nullptr; + + mResponse = aResponse; + + mState = State::Finishing; + MOZ_ALWAYS_SUCCEEDS(mNestedEventTarget->Dispatch(this, NS_DISPATCH_NORMAL)); +} + +} // namespace dom +} // namespace mozilla diff --git a/dom/localstorage/LSObject.h b/dom/localstorage/LSObject.h new file mode 100644 index 000000000000..b8311a036118 --- /dev/null +++ b/dom/localstorage/LSObject.h @@ -0,0 +1,262 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/. */ + +#ifndef mozilla_dom_localstorage_LSObject_h +#define mozilla_dom_localstorage_LSObject_h + +#include "mozilla/dom/Storage.h" + +class nsGlobalWindowInner; +class nsIPrincipal; +class nsPIDOMWindowInner; + +namespace mozilla { + +class ErrorResult; + +namespace ipc { + +class PrincipalInfo; + +} // namespace ipc + +namespace dom { + +class LSDatabase; +class LSObjectChild; +class LSObserver; +class LSRequestChild; +class LSRequestChildCallback; +class LSRequestParams; +class LSRequestResponse; + +/** + * Backs the WebIDL `Storage` binding; all content LocalStorage calls are + * handled by this class. + * + * ## Semantics under e10s / multi-process ## + * + * A snapshot mechanism used in conjuction with stable points ensures that JS + * run-to-completion semantics are experienced even if the same origin is + * concurrently accessing LocalStorage across multiple content processes. + * + * ### Snapshot Consistency ### + * + * An LSSnapshot is created locally whenever the contents of LocalStorage are + * about to be read or written (including length). This synchronously + * establishes a corresponding Snapshot in PBackground in the parent process. + * An effort is made to send as much data from the parent process as possible, + * so sites using a small/reasonable amount of LocalStorage data will have it + * sent to the content process for immediate access. Sites with greater + * LocalStorage usage may only have some of the information relayed. In that + * case, the parent Snapshot will ensure that it retains the exact state of the + * parent Datastore at the moment the Snapshot was created. + */ +class LSObject final + : public Storage +{ + typedef mozilla::ipc::PrincipalInfo PrincipalInfo; + + friend nsGlobalWindowInner; + + nsAutoPtr mPrincipalInfo; + + RefPtr mDatabase; + RefPtr mObserver; + + uint32_t mPrivateBrowsingId; + nsCString mOrigin; + nsString mDocumentURI; + + bool mInExplicitSnapshot; + +public: + /** + * The normal creation path invoked by nsGlobalWindowInner. + */ + static nsresult + CreateForWindow(nsPIDOMWindowInner* aWindow, + Storage** aStorage); + + /** + * nsIDOMStorageManager creation path for use in testing logic. Supports the + * system principal where CreateForWindow does not. This is also why aPrivate + * exists separate from the principal; because the system principal can never + * be mutated to have a private browsing id even though it can be used in a + * window/document marked as private browsing. That's a legacy issue that is + * being dealt with, but it's why it exists here. + */ + static nsresult + CreateForPrincipal(nsPIDOMWindowInner* aWindow, + nsIPrincipal* aPrincipal, + const nsAString& aDocumentURI, + bool aPrivate, + LSObject** aObject); + + /** + * Used for requests from the parent process to the parent process; in that + * case we want ActorsParent to know our event-target and this is better than + * trying to tunnel the pointer through IPC. + */ + static already_AddRefed + GetSyncLoopEventTarget(); + + /** + * Helper invoked by ContentChild::OnChannelReceivedMessage when a sync IPC + * message is received. This will be invoked on the IPC I/O thread and it's + * necessary to unblock the main thread when this happens to avoid the + * potential for browser deadlock. This should only occur in (ugly) testing + * scenarios where CPOWs are in use. + * + * Cancellation will result in the underlying LSRequest being explicitly + * canceled, resulting in the parent sending an NS_ERROR_FAILURE result. + */ + static void + CancelSyncLoop(); + + void + AssertIsOnOwningThread() const + { + NS_ASSERT_OWNINGTHREAD(LSObject); + } + + const nsString& + DocumentURI() const + { + return mDocumentURI; + } + + LSRequestChild* + StartRequest(nsIEventTarget* aMainEventTarget, + const LSRequestParams& aParams, + LSRequestChildCallback* aCallback); + + // Storage overrides. + StorageType + Type() const override; + + bool + IsForkOf(const Storage* aStorage) const override; + + int64_t + GetOriginQuotaUsage() const override; + + uint32_t + GetLength(nsIPrincipal& aSubjectPrincipal, + ErrorResult& aError) override; + + void + Key(uint32_t aIndex, + nsAString& aResult, + nsIPrincipal& aSubjectPrincipal, + ErrorResult& aError) override; + + void + GetItem(const nsAString& aKey, + nsAString& aResult, + nsIPrincipal& aSubjectPrincipal, + ErrorResult& aError) override; + + void + GetSupportedNames(nsTArray& aNames) override; + + void + SetItem(const nsAString& aKey, + const nsAString& aValue, + nsIPrincipal& aSubjectPrincipal, + ErrorResult& aError) override; + + void + RemoveItem(const nsAString& aKey, + nsIPrincipal& aSubjectPrincipal, + ErrorResult& aError) override; + + void + Clear(nsIPrincipal& aSubjectPrincipal, + ErrorResult& aError) override; + + ////////////////////////////////////////////////////////////////////////////// + // Testing Methods: See Storage.h + void + Open(nsIPrincipal& aSubjectPrincipal, + ErrorResult& aError) override; + + void + Close(nsIPrincipal& aSubjectPrincipal, + ErrorResult& aError) override; + + void + BeginExplicitSnapshot(nsIPrincipal& aSubjectPrincipal, + ErrorResult& aError) override; + + void + EndExplicitSnapshot(nsIPrincipal& aSubjectPrincipal, + ErrorResult& aError) override; + + ////////////////////////////////////////////////////////////////////////////// + + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(LSObject, Storage) + +private: + LSObject(nsPIDOMWindowInner* aWindow, + nsIPrincipal* aPrincipal); + + ~LSObject(); + + nsresult + DoRequestSynchronously(const LSRequestParams& aParams, + LSRequestResponse& aResponse); + + nsresult + EnsureDatabase(); + + void + DropDatabase(); + + /** + * Invoked by nsGlobalWindowInner whenever a new "storage" event listener is + * added to the window in order to ensure that "storage" events are received + * from other processes. (`LSObject::OnChange` directly invokes + * `Storage::NotifyChange` to notify in-process listeners.) + * + * If this is the first request in the process for an observer for this + * origin, this will trigger a RequestHelper-mediated synchronous LSRequest + * to prepare a new observer in the parent process and also construction of + * corresponding actors, which will result in the observer being fully + * registered in the parent process. + */ + nsresult + EnsureObserver(); + + /** + * Invoked by nsGlobalWindowInner whenever its last "storage" event listener + * is removed. + */ + void + DropObserver(); + + /** + * Internal helper method used by mutation methods that wraps the call to + * Storage::NotifyChange to generate same-process "storage" events. + */ + void + OnChange(const nsAString& aKey, + const nsAString& aOldValue, + const nsAString& aNewValue); + + nsresult + EndExplicitSnapshotInternal(); + + // Storage overrides. + void + LastRelease() override; +}; + +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_localstorage_LSObject_h diff --git a/dom/localstorage/LSObserver.cpp b/dom/localstorage/LSObserver.cpp new file mode 100644 index 000000000000..8953dc23103c --- /dev/null +++ b/dom/localstorage/LSObserver.cpp @@ -0,0 +1,76 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=2 et sw=2 tw=80: */ +/* 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/. */ + +#include "LSObserver.h" + +#include "mozilla/ipc/BackgroundChild.h" +#include "mozilla/ipc/BackgroundUtils.h" +#include "mozilla/ipc/PBackgroundChild.h" +#include "nsContentUtils.h" +#include "nsIScriptObjectPrincipal.h" + +namespace mozilla { +namespace dom { + +namespace { + +typedef nsDataHashtable LSObserverHashtable; + +StaticAutoPtr gLSObservers; + +} // namespace + +LSObserver::LSObserver(const nsACString& aOrigin) + : mActor(nullptr) + , mOrigin(aOrigin) +{ + AssertIsOnOwningThread(); + + if (!gLSObservers) { + gLSObservers = new LSObserverHashtable(); + } + + MOZ_ASSERT(!gLSObservers->Get(mOrigin)); + gLSObservers->Put(mOrigin, this); +} + +LSObserver::~LSObserver() +{ + AssertIsOnOwningThread(); + + if (mActor) { + mActor->SendDeleteMeInternal(); + MOZ_ASSERT(!mActor, "SendDeleteMeInternal should have cleared!"); + } + + MOZ_ASSERT(gLSObservers); + MOZ_ASSERT(gLSObservers->Get(mOrigin)); + gLSObservers->Remove(mOrigin); + + if (!gLSObservers->Count()) { + gLSObservers = nullptr; + } +} + +// static +LSObserver* +LSObserver::Get(const nsACString& aOrigin) +{ + return gLSObservers ? gLSObservers->Get(aOrigin) : nullptr; +} + +void +LSObserver::SetActor(LSObserverChild* aActor) +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(aActor); + MOZ_ASSERT(!mActor); + + mActor = aActor; +} + +} // namespace dom +} // namespace mozilla diff --git a/dom/localstorage/LSObserver.h b/dom/localstorage/LSObserver.h new file mode 100644 index 000000000000..74a3d9aa43c1 --- /dev/null +++ b/dom/localstorage/LSObserver.h @@ -0,0 +1,74 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/. */ + +#ifndef mozilla_dom_localstorage_LSObserver_h +#define mozilla_dom_localstorage_LSObserver_h + +namespace mozilla { +namespace dom { + +class LSObserverChild; + +/** + * Effectively just a refcounted life-cycle management wrapper around + * LSObserverChild which exists to receive "storage" event information from + * other processes. (Same-process events are handled within the process, see + * `LSObject::OnChange`.) + * + * ## Lifecycle ## + * - Created by LSObject::EnsureObserver via synchronous LSRequest idiom + * whenever the first window's origin adds a "storage" event. Placed in the + * gLSObservers LSObserverHashtable for subsequent LSObject's via + * LSObserver::Get lookup. + * - The LSObserverChild directly handles "Observe" messages, shunting them + * directly to Storage::NotifyChange which does all the legwork of notifying + * windows about "storage" events. + * - Destroyed when refcount goes to zero due to all owning LSObjects being + * destroyed or having their `LSObject::DropObserver` methods invoked due to + * the last "storage" event listener being removed from the owning window. + */ +class LSObserver final +{ + friend class LSObject; + + LSObserverChild* mActor; + + const nsCString mOrigin; + +public: + explicit LSObserver(const nsACString& aOrigin); + + static LSObserver* + Get(const nsACString& aOrigin); + + NS_INLINE_DECL_REFCOUNTING(LSObserver) + + void + AssertIsOnOwningThread() const + { + NS_ASSERT_OWNINGTHREAD(LSDatabase); + } + + void + SetActor(LSObserverChild* aActor); + + void + ClearActor() + { + AssertIsOnOwningThread(); + MOZ_ASSERT(mActor); + + mActor = nullptr; + } + +private: + ~LSObserver(); +}; + +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_localstorage_LSObserver_h diff --git a/dom/localstorage/LSSnapshot.cpp b/dom/localstorage/LSSnapshot.cpp new file mode 100644 index 000000000000..882b68fde129 --- /dev/null +++ b/dom/localstorage/LSSnapshot.cpp @@ -0,0 +1,785 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=2 et sw=2 tw=80: */ +/* 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/. */ + +#include "LSSnapshot.h" + +#include "nsContentUtils.h" + +namespace mozilla { +namespace dom { + +namespace { + +const uint32_t kSnapshotTimeoutMs = 20000; + +} // namespace + +LSSnapshot::LSSnapshot(LSDatabase* aDatabase) + : mDatabase(aDatabase) + , mActor(nullptr) + , mInitLength(0) + , mLength(0) + , mExactUsage(0) + , mPeakUsage(0) + , mLoadState(LoadState::Initial) + , mExplicit(false) + , mHasPendingStableStateCallback(false) + , mHasPendingTimerCallback(false) + , mDirty(false) +#ifdef DEBUG + , mInitialized(false) + , mSentFinish(false) +#endif +{ + AssertIsOnOwningThread(); +} + +LSSnapshot::~LSSnapshot() +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(mDatabase); + MOZ_ASSERT(!mHasPendingStableStateCallback); + MOZ_ASSERT(!mHasPendingTimerCallback); + MOZ_ASSERT_IF(mInitialized, mSentFinish); + + if (mActor) { + mActor->SendDeleteMeInternal(); + MOZ_ASSERT(!mActor, "SendDeleteMeInternal should have cleared!"); + } +} + +void +LSSnapshot::SetActor(LSSnapshotChild* aActor) +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(aActor); + MOZ_ASSERT(!mActor); + + mActor = aActor; +} + +nsresult +LSSnapshot::Init(const LSSnapshotInitInfo& aInitInfo, + bool aExplicit) +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(!mSelfRef); + MOZ_ASSERT(mActor); + MOZ_ASSERT(mLoadState == LoadState::Initial); + MOZ_ASSERT(!mInitialized); + MOZ_ASSERT(!mSentFinish); + + mSelfRef = this; + + LoadState loadState = aInitInfo.loadState(); + + const nsTArray& itemInfos = aInitInfo.itemInfos(); + for (uint32_t i = 0; i < itemInfos.Length(); i++) { + const LSItemInfo& itemInfo = itemInfos[i]; + + const nsString& value = itemInfo.value(); + + if (loadState != LoadState::AllOrderedItems && !value.IsVoid()) { + mLoadedItems.PutEntry(itemInfo.key()); + } + + mValues.Put(itemInfo.key(), value); + } + + if (loadState == LoadState::Partial) { + mInitLength = aInitInfo.totalLength(); + mLength = mInitLength; + } else if (loadState == LoadState::AllOrderedKeys) { + mInitLength = aInitInfo.totalLength(); + } else { + MOZ_ASSERT(loadState == LoadState::AllOrderedItems); + } + + mExactUsage = aInitInfo.initialUsage(); + mPeakUsage = aInitInfo.peakUsage(); + + mLoadState = aInitInfo.loadState(); + + mExplicit = aExplicit; + +#ifdef DEBUG + mInitialized = true; +#endif + + if (!mExplicit) { + mTimer = NS_NewTimer(); + MOZ_ASSERT(mTimer); + + ScheduleStableStateCallback(); + } + + return NS_OK; +} + +nsresult +LSSnapshot::GetLength(uint32_t* aResult) +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(mActor); + MOZ_ASSERT(mInitialized); + MOZ_ASSERT(!mSentFinish); + + MaybeScheduleStableStateCallback(); + + if (mLoadState == LoadState::Partial) { + *aResult = mLength; + } else { + *aResult = mValues.Count(); + } + + return NS_OK; +} + +nsresult +LSSnapshot::GetKey(uint32_t aIndex, + nsAString& aResult) +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(mActor); + MOZ_ASSERT(mInitialized); + MOZ_ASSERT(!mSentFinish); + + MaybeScheduleStableStateCallback(); + + nsresult rv = EnsureAllKeys(); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + aResult.SetIsVoid(true); + for (auto iter = mValues.ConstIter(); !iter.Done(); iter.Next()) { + if (aIndex == 0) { + aResult = iter.Key(); + return NS_OK; + } + aIndex--; + } + + return NS_OK; +} + +nsresult +LSSnapshot::GetItem(const nsAString& aKey, + nsAString& aResult) +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(mActor); + MOZ_ASSERT(mInitialized); + MOZ_ASSERT(!mSentFinish); + + MaybeScheduleStableStateCallback(); + + nsString result; + nsresult rv = GetItemInternal(aKey, Optional(), result); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + aResult = result; + return NS_OK; +} + +nsresult +LSSnapshot::GetKeys(nsTArray& aKeys) +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(mActor); + MOZ_ASSERT(mInitialized); + MOZ_ASSERT(!mSentFinish); + + MaybeScheduleStableStateCallback(); + + nsresult rv = EnsureAllKeys(); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + for (auto iter = mValues.ConstIter(); !iter.Done(); iter.Next()) { + aKeys.AppendElement(iter.Key()); + } + + return NS_OK; +} + +nsresult +LSSnapshot::SetItem(const nsAString& aKey, + const nsAString& aValue, + LSNotifyInfo& aNotifyInfo) +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(mActor); + MOZ_ASSERT(mInitialized); + MOZ_ASSERT(!mSentFinish); + + MaybeScheduleStableStateCallback(); + + nsString oldValue; + nsresult rv = + GetItemInternal(aKey, Optional(nsString(aValue)), oldValue); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + bool changed; + if (oldValue == aValue && oldValue.IsVoid() == aValue.IsVoid()) { + changed = false; + } else { + changed = true; + + int64_t delta = static_cast(aValue.Length()) - + static_cast(oldValue.Length()); + + if (oldValue.IsVoid()) { + delta += static_cast(aKey.Length()); + } + + rv = UpdateUsage(delta); + if (NS_WARN_IF(NS_FAILED(rv))) { + if (oldValue.IsVoid()) { + mValues.Remove(aKey); + } else { + mValues.Put(aKey, oldValue); + } + return rv; + } + + if (oldValue.IsVoid() && mLoadState == LoadState::Partial) { + mLength++; + } + + LSSetItemInfo setItemInfo; + setItemInfo.key() = aKey; + setItemInfo.oldValue() = oldValue; + setItemInfo.value() = aValue; + + mWriteInfos.AppendElement(std::move(setItemInfo)); + } + + aNotifyInfo.changed() = changed; + aNotifyInfo.oldValue() = oldValue; + + return NS_OK; +} + +nsresult +LSSnapshot::RemoveItem(const nsAString& aKey, + LSNotifyInfo& aNotifyInfo) +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(mActor); + MOZ_ASSERT(mInitialized); + MOZ_ASSERT(!mSentFinish); + + MaybeScheduleStableStateCallback(); + + nsString oldValue; + nsresult rv = + GetItemInternal(aKey, Optional(VoidString()), oldValue); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + bool changed; + if (oldValue.IsVoid()) { + changed = false; + } else { + changed = true; + + int64_t delta = -(static_cast(aKey.Length()) + + static_cast(oldValue.Length())); + + DebugOnly rv = UpdateUsage(delta); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + + if (mLoadState == LoadState::Partial) { + mLength--; + } + + LSRemoveItemInfo removeItemInfo; + removeItemInfo.key() = aKey; + removeItemInfo.oldValue() = oldValue; + + mWriteInfos.AppendElement(std::move(removeItemInfo)); + } + + aNotifyInfo.changed() = changed; + aNotifyInfo.oldValue() = oldValue; + + return NS_OK; +} + +nsresult +LSSnapshot::Clear(LSNotifyInfo& aNotifyInfo) +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(mActor); + MOZ_ASSERT(mInitialized); + MOZ_ASSERT(!mSentFinish); + + MaybeScheduleStableStateCallback(); + + uint32_t length; + if (mLoadState == LoadState::Partial) { + length = mLength; + MOZ_ASSERT(length); + + MOZ_ALWAYS_TRUE(mActor->SendLoaded()); + + mLoadedItems.Clear(); + mUnknownItems.Clear(); + mLength = 0; + mLoadState = LoadState::AllOrderedItems; + } else { + length = mValues.Count(); + } + + bool changed; + if (!length) { + changed = false; + } else { + changed = true; + + DebugOnly rv = UpdateUsage(-mExactUsage); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + + mValues.Clear(); + + LSClearInfo clearInfo; + + mWriteInfos.AppendElement(std::move(clearInfo)); + } + + aNotifyInfo.changed() = changed; + + return NS_OK; +} + +void +LSSnapshot::MarkDirty() +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(mActor); + MOZ_ASSERT(mInitialized); + MOZ_ASSERT(!mSentFinish); + + if (mDirty) { + return; + } + + mDirty = true; + + if (!mExplicit && !mHasPendingStableStateCallback) { + CancelTimer(); + + MOZ_ALWAYS_SUCCEEDS(Checkpoint()); + + MOZ_ALWAYS_SUCCEEDS(Finish()); + } else { + MOZ_ASSERT(!mHasPendingTimerCallback); + } +} + +nsresult +LSSnapshot::End() +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(mActor); + MOZ_ASSERT(mExplicit); + MOZ_ASSERT(!mHasPendingStableStateCallback); + MOZ_ASSERT(!mHasPendingTimerCallback); + MOZ_ASSERT(mInitialized); + MOZ_ASSERT(!mSentFinish); + + nsresult rv = Checkpoint(); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + RefPtr kungFuDeathGrip = this; + + rv = Finish(); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (NS_WARN_IF(!mActor->SendPing())) { + return NS_ERROR_FAILURE; + } + + return NS_OK; +} + +void +LSSnapshot::ScheduleStableStateCallback() +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(mTimer); + MOZ_ASSERT(!mExplicit); + MOZ_ASSERT(!mHasPendingStableStateCallback); + + CancelTimer(); + + nsCOMPtr runnable = this; + nsContentUtils::RunInStableState(runnable.forget()); + + mHasPendingStableStateCallback = true; +} + +void +LSSnapshot::MaybeScheduleStableStateCallback() +{ + AssertIsOnOwningThread(); + + if (!mExplicit && !mHasPendingStableStateCallback) { + ScheduleStableStateCallback(); + } else { + MOZ_ASSERT(!mHasPendingTimerCallback); + } +} + +nsresult +LSSnapshot::GetItemInternal(const nsAString& aKey, + const Optional& aValue, + nsAString& aResult) +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(mActor); + MOZ_ASSERT(mInitialized); + MOZ_ASSERT(!mSentFinish); + + nsString result; + + switch (mLoadState) { + case LoadState::Partial: { + if (mValues.Get(aKey, &result)) { + MOZ_ASSERT(!result.IsVoid()); + } else if (mLoadedItems.GetEntry(aKey) || mUnknownItems.GetEntry(aKey)) { + result.SetIsVoid(true); + } else { + if (NS_WARN_IF(!mActor->SendLoadItem(nsString(aKey), &result))) { + return NS_ERROR_FAILURE; + } + + if (result.IsVoid()) { + mUnknownItems.PutEntry(aKey); + } else { + mLoadedItems.PutEntry(aKey); + mValues.Put(aKey, result); + + if (mLoadedItems.Count() == mInitLength) { + mLoadedItems.Clear(); + mUnknownItems.Clear(); + mLength = 0; + mLoadState = LoadState::AllUnorderedItems; + } + } + } + + if (aValue.WasPassed()) { + const nsString& value = aValue.Value(); + if (!value.IsVoid()) { + mValues.Put(aKey, value); + } else if (!result.IsVoid()) { + mValues.Remove(aKey); + } + } + + break; + } + + case LoadState::AllOrderedKeys: { + if (mValues.Get(aKey, &result)) { + if (result.IsVoid()) { + if (NS_WARN_IF(!mActor->SendLoadItem(nsString(aKey), &result))) { + return NS_ERROR_FAILURE; + } + + MOZ_ASSERT(!result.IsVoid()); + + mLoadedItems.PutEntry(aKey); + mValues.Put(aKey, result); + + if (mLoadedItems.Count() == mInitLength) { + mLoadedItems.Clear(); + MOZ_ASSERT(mLength == 0); + mLoadState = LoadState::AllOrderedItems; + } + } + } else { + result.SetIsVoid(true); + } + + if (aValue.WasPassed()) { + const nsString& value = aValue.Value(); + if (!value.IsVoid()) { + mValues.Put(aKey, value); + } else if (!result.IsVoid()) { + mValues.Remove(aKey); + } + } + + break; + } + + case LoadState::AllUnorderedItems: + case LoadState::AllOrderedItems: { + if (aValue.WasPassed()) { + const nsString& value = aValue.Value(); + if (!value.IsVoid()) { + auto entry = mValues.LookupForAdd(aKey); + if (entry) { + result = entry.Data(); + entry.Data() = value; + } else { + result.SetIsVoid(true); + entry.OrInsert([value]() { return value; }); + } + } else { + if (auto entry = mValues.Lookup(aKey)) { + result = entry.Data(); + MOZ_ASSERT(!result.IsVoid()); + entry.Remove(); + } else { + result.SetIsVoid(true); + } + } + } else { + if (mValues.Get(aKey, &result)) { + MOZ_ASSERT(!result.IsVoid()); + } else { + result.SetIsVoid(true); + } + } + + break; + } + + default: + MOZ_CRASH("Bad state!"); + } + + aResult = result; + return NS_OK; +} + +nsresult +LSSnapshot::EnsureAllKeys() +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(mActor); + MOZ_ASSERT(mInitialized); + MOZ_ASSERT(!mSentFinish); + MOZ_ASSERT(mLoadState != LoadState::Initial); + + if (mLoadState == LoadState::AllOrderedKeys || + mLoadState == LoadState::AllOrderedItems) { + return NS_OK; + } + + nsTArray keys; + if (NS_WARN_IF(!mActor->SendLoadKeys(&keys))) { + return NS_ERROR_FAILURE; + } + + nsDataHashtable newValues; + + for (auto key : keys) { + newValues.Put(key, VoidString()); + } + + for (uint32_t index = 0; index < mWriteInfos.Length(); index++) { + const LSWriteInfo& writeInfo = mWriteInfos[index]; + + switch (writeInfo.type()) { + case LSWriteInfo::TLSSetItemInfo: { + newValues.Put(writeInfo.get_LSSetItemInfo().key(), VoidString()); + break; + } + case LSWriteInfo::TLSRemoveItemInfo: { + newValues.Remove(writeInfo.get_LSRemoveItemInfo().key()); + break; + } + case LSWriteInfo::TLSClearInfo: { + newValues.Clear(); + break; + } + + default: + MOZ_CRASH("Should never get here!"); + } + } + + MOZ_ASSERT_IF(mLoadState == LoadState::AllUnorderedItems, + newValues.Count() == mValues.Count()); + + for (auto iter = newValues.Iter(); !iter.Done(); iter.Next()) { + nsString value; + if (mValues.Get(iter.Key(), &value)) { + iter.Data() = value; + } + } + + mValues.SwapElements(newValues); + + if (mLoadState == LoadState::Partial) { + mUnknownItems.Clear(); + mLength = 0; + mLoadState = LoadState::AllOrderedKeys; + } else { + MOZ_ASSERT(mLoadState == LoadState::AllUnorderedItems); + + MOZ_ASSERT(mUnknownItems.Count() == 0); + MOZ_ASSERT(mLength == 0); + mLoadState = LoadState::AllOrderedItems; + } + + return NS_OK; +} + +nsresult +LSSnapshot::UpdateUsage(int64_t aDelta) +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(mDatabase); + MOZ_ASSERT(mActor); + MOZ_ASSERT(mPeakUsage >= mExactUsage); + MOZ_ASSERT(mInitialized); + MOZ_ASSERT(!mSentFinish); + + int64_t newExactUsage = mExactUsage + aDelta; + if (newExactUsage > mPeakUsage) { + int64_t minSize = newExactUsage - mPeakUsage; + int64_t requestedSize = minSize + 4096; + int64_t size; + if (NS_WARN_IF(!mActor->SendIncreasePeakUsage(requestedSize, + minSize, + &size))) { + return NS_ERROR_FAILURE; + } + + MOZ_ASSERT(size >= 0); + + if (size == 0) { + return NS_ERROR_FILE_NO_DEVICE_SPACE; + } + + mPeakUsage += size; + } + + mExactUsage = newExactUsage; + return NS_OK; +} + +nsresult +LSSnapshot::Checkpoint() +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(mActor); + MOZ_ASSERT(mInitialized); + MOZ_ASSERT(!mSentFinish); + + if (!mWriteInfos.IsEmpty()) { + MOZ_ALWAYS_TRUE(mActor->SendCheckpoint(mWriteInfos)); + + mWriteInfos.Clear(); + } + + return NS_OK; +} + +nsresult +LSSnapshot::Finish() +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(mDatabase); + MOZ_ASSERT(mActor); + MOZ_ASSERT(mInitialized); + MOZ_ASSERT(!mSentFinish); + + MOZ_ALWAYS_TRUE(mActor->SendFinish()); + + mDatabase->NoteFinishedSnapshot(this); + +#ifdef DEBUG + mSentFinish = true; +#endif + + // Clear the self reference added in Init method. + MOZ_ASSERT(mSelfRef); + mSelfRef = nullptr; + + return NS_OK; +} + +void +LSSnapshot::CancelTimer() +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(mTimer); + + if (mHasPendingTimerCallback) { + MOZ_ALWAYS_SUCCEEDS(mTimer->Cancel()); + mHasPendingTimerCallback = false; + } +} + +// static +void +LSSnapshot::TimerCallback(nsITimer* aTimer, void* aClosure) +{ + MOZ_ASSERT(aTimer); + + auto* self = static_cast(aClosure); + MOZ_ASSERT(self); + MOZ_ASSERT(self->mTimer); + MOZ_ASSERT(SameCOMIdentity(self->mTimer, aTimer)); + MOZ_ASSERT(!self->mHasPendingStableStateCallback); + MOZ_ASSERT(self->mHasPendingTimerCallback); + + self->mHasPendingTimerCallback = false; + + MOZ_ALWAYS_SUCCEEDS(self->Finish()); +} + +NS_IMPL_ISUPPORTS(LSSnapshot, nsIRunnable) + +NS_IMETHODIMP +LSSnapshot::Run() +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(!mExplicit); + MOZ_ASSERT(mHasPendingStableStateCallback); + MOZ_ASSERT(!mHasPendingTimerCallback); + + mHasPendingStableStateCallback = false; + + MOZ_ALWAYS_SUCCEEDS(Checkpoint()); + + if (mDirty || !Preferences::GetBool("dom.storage.snapshot_reusing")) { + MOZ_ALWAYS_SUCCEEDS(Finish()); + } else if (!mExplicit) { + MOZ_ASSERT(mTimer); + + MOZ_ALWAYS_SUCCEEDS( + mTimer->InitWithNamedFuncCallback(TimerCallback, + this, + kSnapshotTimeoutMs, + nsITimer::TYPE_ONE_SHOT, + "LSSnapshot::TimerCallback")); + + mHasPendingTimerCallback = true; + } + + return NS_OK; +} + +} // namespace dom +} // namespace mozilla diff --git a/dom/localstorage/LSSnapshot.h b/dom/localstorage/LSSnapshot.h new file mode 100644 index 000000000000..25fa0f7f48a5 --- /dev/null +++ b/dom/localstorage/LSSnapshot.h @@ -0,0 +1,204 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/. */ + +#ifndef mozilla_dom_localstorage_LSSnapshot_h +#define mozilla_dom_localstorage_LSSnapshot_h + +namespace mozilla { +namespace dom { + +class LSDatabase; +class LSNotifyInfo; +class LSSnapshotChild; +class LSSnapshotInitInfo; +class LSWriteInfo; + +class LSSnapshot final + : public nsIRunnable +{ +public: + /** + * The LoadState expresses what subset of information a snapshot has from the + * authoritative Datastore in the parent process. The initial snapshot is + * populated heuristically based on the size of the keys and size of the items + * (inclusive of the key value; item is key+value, not just value) of the + * entire datastore relative to the configured prefill limit (via pref + * "dom.storage.snapshot_prefill" exposed as gSnapshotPrefill in bytes). + * + * If there's less data than the limit, we send both keys and values and end + * up as AllOrderedItems. If there's enough room for all the keys but not + * all the values, we end up as AllOrderedKeys with as many values present as + * would fit. If there's not enough room for all the keys, then we end up as + * Partial with as many key-value pairs as will fit. + * + * The state AllUnorderedItems can only be reached by code getting items one + * by one. + */ + enum class LoadState + { + /** + * Class constructed, Init(LSSnapshotInitInfo) has not been invoked yet. + */ + Initial, + /** + * Some keys and their values are known. + */ + Partial, + /** + * All the keys are known in order, but some values are unknown. + */ + AllOrderedKeys, + /** + * All keys and their values are known, but in an arbitrary order. + */ + AllUnorderedItems, + /** + * All keys and their values are known and are present in their canonical + * order. This is everything, and is the preferred case. The initial + * population will send this info when the size of all items is less than + * the prefill threshold. + * + * mValues will contain all keys and values, mLoadedItems and mUnknownItems + * are unused. + */ + AllOrderedItems, + EndGuard + }; + +private: + RefPtr mSelfRef; + + RefPtr mDatabase; + + nsCOMPtr mTimer; + + LSSnapshotChild* mActor; + + nsTHashtable mLoadedItems; + nsTHashtable mUnknownItems; + nsDataHashtable mValues; + nsTArray mWriteInfos; + + uint32_t mInitLength; + uint32_t mLength; + int64_t mExactUsage; + int64_t mPeakUsage; + + LoadState mLoadState; + + bool mExplicit; + bool mHasPendingStableStateCallback; + bool mHasPendingTimerCallback; + bool mDirty; + +#ifdef DEBUG + bool mInitialized; + bool mSentFinish; +#endif + +public: + explicit LSSnapshot(LSDatabase* aDatabase); + + void + AssertIsOnOwningThread() const + { + NS_ASSERT_OWNINGTHREAD(LSSnapshot); + } + + void + SetActor(LSSnapshotChild* aActor); + + void + ClearActor() + { + AssertIsOnOwningThread(); + MOZ_ASSERT(mActor); + + mActor = nullptr; + } + + bool + Explicit() const + { + return mExplicit; + } + + nsresult + Init(const LSSnapshotInitInfo& aInitInfo, + bool aExplicit); + + nsresult + GetLength(uint32_t* aResult); + + nsresult + GetKey(uint32_t aIndex, + nsAString& aResult); + + nsresult + GetItem(const nsAString& aKey, + nsAString& aResult); + + nsresult + GetKeys(nsTArray& aKeys); + + nsresult + SetItem(const nsAString& aKey, + const nsAString& aValue, + LSNotifyInfo& aNotifyInfo); + + nsresult + RemoveItem(const nsAString& aKey, + LSNotifyInfo& aNotifyInfo); + + nsresult + Clear(LSNotifyInfo& aNotifyInfo); + + void + MarkDirty(); + + nsresult + End(); + +private: + ~LSSnapshot(); + + void + ScheduleStableStateCallback(); + + void + MaybeScheduleStableStateCallback(); + + nsresult + GetItemInternal(const nsAString& aKey, + const Optional& aValue, + nsAString& aResult); + + nsresult + EnsureAllKeys(); + + nsresult + UpdateUsage(int64_t aDelta); + + nsresult + Checkpoint(); + + nsresult + Finish(); + + void + CancelTimer(); + + static void + TimerCallback(nsITimer* aTimer, void* aClosure); + + NS_DECL_ISUPPORTS + NS_DECL_NSIRUNNABLE +}; + +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_localstorage_LSSnapshot_h diff --git a/dom/localstorage/LocalStorageCommon.cpp b/dom/localstorage/LocalStorageCommon.cpp new file mode 100644 index 000000000000..e90dce61f7c4 --- /dev/null +++ b/dom/localstorage/LocalStorageCommon.cpp @@ -0,0 +1,42 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/. */ + +#include "LocalStorageCommon.h" + +namespace mozilla { +namespace dom { + +namespace { + +Atomic gNextGenLocalStorageEnabled(-1); + +} // namespace + +const char16_t* kLocalStorageType = u"localStorage"; + +bool +NextGenLocalStorageEnabled() +{ + MOZ_ASSERT(NS_IsMainThread()); + + if (gNextGenLocalStorageEnabled == -1) { + bool enabled = Preferences::GetBool("dom.storage.next_gen", false); + gNextGenLocalStorageEnabled = enabled ? 1 : 0; + } + + return !!gNextGenLocalStorageEnabled; +} + +bool +CachedNextGenLocalStorageEnabled() +{ + MOZ_ASSERT(gNextGenLocalStorageEnabled != -1); + + return !!gNextGenLocalStorageEnabled; +} + +} // namespace dom +} // namespace mozilla diff --git a/dom/localstorage/LocalStorageCommon.h b/dom/localstorage/LocalStorageCommon.h new file mode 100644 index 000000000000..4dffbbb5cf18 --- /dev/null +++ b/dom/localstorage/LocalStorageCommon.h @@ -0,0 +1,247 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/. */ + +#ifndef mozilla_dom_localstorage_LocalStorageCommon_h +#define mozilla_dom_localstorage_LocalStorageCommon_h + +/* + * Local storage + * ~~~~~~~~~~~~~ + * + * Implementation overview + * ~~~~~~~~~~~~~~~~~~~~~~~ + * + * The implementation is based on a per principal/origin cache (datastore) + * living in the main process and synchronous calls initiated from content + * processes. + * The IPC communication is managed by database actors which link to the + * datastore. + * The synchronous blocking of the main thread is done by using a special + * technique or by using standard synchronous IPC calls. + * + * General architecture + * ~~~~~~~~~~~~~~~~~~~~ + * The current browser architecture consists of one main process and multiple + * content processes (there are other processes but for simplicity's sake, they + * are not mentioned here). The processes use the IPC communication to talk to + * each other. Local storage implementation uses the client-server model, so + * the main process manages all the data and content processes then request + * particular data from the main process. The main process is also called the + * parent or the parent side, the content process is then called the child or + * the child side. + * + * Datastores + * ~~~~~~~~~~ + * + * A datastore provides a convenient way to access data for given origin. The + * data is always preloaded into memory and indexed using a hash table. This + * enables very fast access to particular stored items. There can be only one + * datastore per origin and exists solely on the parent side. It is represented + * by the "Datastore" class. A datastore instance is a ref counted object and + * lives on the PBackground thread, it is kept alive by database objects. When + * the last database object for given origin is destroyed, the associated + * datastore object is destroyed too. + * + * Databases + * ~~~~~~~~~ + * + * A database allows direct access to a datastore from a content process. There + * can be multiple databases for the same origin, but they all share the same + * datastore. + * Databases use the PBackgroundLSDatabase IPDL protocol for IPC communication. + * Given the nature of local storage, most of PBackgroundLSDatabase messages + * are synchronous. + * + * On the parent side, the database is represented by the "Database" class that + * is a parent actor as well (implements the "PBackgroundLSDatabaseParent" + * interface). A database instance is a ref counted object and lives on the + * PBackground thread. + * All live database actors are tracked in an array. + * + * On the child side, the database is represented by the "LSDatabase" class + * that provides indirect access to a child actor. An LSDatabase instance is a + * ref counted object and lives on the main thread. + * The actual child actor is represented by the "LSDatabaseChild" class that + * implements the "PBackgroundLSDatabaseChild" interface. An "LSDatabaseChild" + * instance is not ref counted and lives on the main thread too. + * + * Synchronous blocking + * ~~~~~~~~~~~~~~~~~~~~ + * + * Local storage is synchronous in nature which means the execution can't move + * forward until there's a reply for given method call. + * Since we have to use IPC anyway, we could just always use synchronous IPC + * messages for all local storage method calls. Well, there's a problem with + * that approach. + * If the main process needs to do some off PBackground thread stuff like + * getting info from principals on the main thread or some asynchronous stuff + * like directory locking before sending a reply to a synchronous message, then + * we would have to block the thread or spin the event loop which is usually a + * bad idea, especially in the main process. + * Instead, we can use a special thread in the content process called DOM File + * thread for communication with the main process using asynchronous messages + * and synchronously block the main thread until the DOM File thread is done + * (the main thread blocking is a bit more complicated, see the comment in + * RequestHelper::StartAndReturnResponse for more details). + * Anyway, the extra hop to the DOM File thread brings another overhead and + * latency. The final solution is to use a combination of the special thread + * for complex stuff like datastore preparation and synchronous IPC messages + * sent directly from the main thread for database access when data is already + * loaded from disk into memory. + * + * Requests + * ~~~~~~~~ + * + * Requests are used to handle asynchronous high level datastore operations + * which are initiated in a content process and then processed in the parent + * process (for example, preparation of a datastore). + * Requests use the "PBackgroundLSRequest" IPDL protocol for IPC communication. + * + * On the parent side, the request is represented by the "LSRequestBase" class + * that is a parent actor as well (implements the "PBackgroundLSRequestParent" + * interface). It's an abstract class (contains pure virtual functions) so it + * can't be used to create instances. + * It also inherits from the "DatastoreOperationBase" class which is a generic + * base class for all datastore operations. The "DatastoreOperationsBase" class + * inherits from the "Runnable" class, so derived class instances are ref + * counted, can be dispatched to multiple threads and thus they are used on + * multiple threads. However, derived class instances can be created on the + * PBackground thread only. + * + * On the child side, the request is represented by the "RequestHelper" class + * that covers all the complexity needed to start a new request, handle + * responses and do safe main thread blocking at the same time. + * It inherits from the "Runnable" class, so instances are ref counted and + * they are internally used on multiple threads (specifically on the main + * thread and on the DOM File thread). Anyway, users should create and use + * instances of this class only on the main thread (apart from a special case + * when we need to cancel the request from an internal chromium IPC thread to + * prevent a dead lock involving CPOWs). + * The actual child actor is represented by the "LSRequestChild" class that + * implements the "PBackgroundLSRequestChild" interface. An "LSRequestChild" + * instance is not ref counted and lives on the DOM File thread. + * Request responses are passed using the "LSRequestChildCallback" interface. + * + * Preparation of a datastore + * ~~~~~~~~~~~~~~~~~~~~~~~~~~ + * + * The datastore preparation is needed to make sure a datastore is fully loaded + * into memory. Every datastore preparation produces a unique id (even if the + * datastore for given origin already exists). + * On the parent side, the preparation is handled by the "PrepareDatastoreOp" + * class which inherits from the "LSRequestBase" class. The preparation process + * on the parent side is quite complicated, it happens sequentially on multiple + * threads and is managed by a state machine. + * On the child side, the preparation is done in the LSObject::EnsureDatabase + * method using the "RequestHelper" class. The method starts a new preparation + * request and obtains a unique id produced by the parent (or an error code if + * the requested failed to complete). + * + * Linking databases to a datastore + * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + * + * A datastore exists only on the parent side, but it can be accessed from the + * content via database actors. Database actors are initiated on the child side + * and they need to be linked to a datastore on the parent side via an id. The + * datastore preparation process gives us the required id. + * The linking is initiated on the child side in the LSObject::EnsureDatabase + * method by calling SendPBackgroundLSDatabaseConstructor and finished in + * RecvPBackgroundLSDatabaseConstructor on the parent side. + * + * Actor migration + * ~~~~~~~~~~~~~~~ + * + * In theory, the datastore preparation request could return a database actor + * directly (instead of returning an id intended for database linking to a + * datastore). However, as it was explained above, the preparation must be done + * on the DOM File thread and database objects are used on the main thread. The + * returned actor would have to be migrated from the DOM File thread to the + * main thread and that's something which our IPDL doesn't support yet. + * + * Exposing local storage + * ~~~~~~~~~~~~~~~~~~~~~~ + * + * The implementation is exposed to the DOM via window.localStorage attribute. + * Local storage's sibling, session storage shares the same WebIDL interface + * for exposing it to web content, therefore there's an abstract class called + * "Storage" that handles some of the common DOM bindings stuff. Local storage + * specific functionality is defined in the "LSObject" derived class. + * The "LSObject" class is also a starting point for the datastore preparation + * and database linking. + * + * Local storage manager + * ~~~~~~~~~~~~~~~~~~~~~ + * + * The local storage manager exposes some of the features that need to be + * available only in the chrome code or tests. The manager is represented by + * the "LocalStorageManager2" class that implements the "nsIDOMStorageManager" + * interface. + */ + +namespace mozilla { +namespace dom { + +extern const char16_t* kLocalStorageType; + +/** + * Convenience data-structure to make it easier to track whether a value has + * changed and what its previous value was for notification purposes. Instances + * are created on the stack by LSObject and passed to LSDatabase which in turn + * passes them onto LSSnapshot for final updating/population. LSObject then + * generates an event, if appropriate. + */ +class MOZ_STACK_CLASS LSNotifyInfo +{ + bool mChanged; + nsString mOldValue; + +public: + LSNotifyInfo() + : mChanged(false) + { } + + bool + changed() const + { + return mChanged; + } + + bool& + changed() + { + return mChanged; + } + + const nsString& + oldValue() const + { + return mOldValue; + } + + nsString& + oldValue() + { + return mOldValue; + } +}; + +/** + * Main-thread-only check of LSNG being enabled, the value is latched once + * initialized so changing the preference during runtime has no effect. + */ +bool +NextGenLocalStorageEnabled(); + +/** + * Cached any-thread version of NextGenLocalStorageEnabled(). + */ +bool +CachedNextGenLocalStorageEnabled(); + +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_localstorage_LocalStorageCommon_h diff --git a/dom/localstorage/LocalStorageManager2.cpp b/dom/localstorage/LocalStorageManager2.cpp new file mode 100644 index 000000000000..6fe6785e88e5 --- /dev/null +++ b/dom/localstorage/LocalStorageManager2.cpp @@ -0,0 +1,430 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=2 et sw=2 tw=80: */ +/* 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/. */ + +#include "LocalStorageManager2.h" + +#include "LSObject.h" +#include "mozilla/dom/Promise.h" + +namespace mozilla { +namespace dom { + +namespace { + +class RequestResolver final + : public LSRequestChildCallback +{ + RefPtr mPromise; + +public: + explicit RequestResolver(Promise* aPromise) + : mPromise(aPromise) + { } + + NS_INLINE_DECL_REFCOUNTING(mozilla::dom::RequestResolver, override); + +private: + ~RequestResolver() = default; + + void + HandleResponse(nsresult aResponse); + + void + HandleResponse(const NullableDatastoreId& aDatastoreId); + + // LSRequestChildCallback + void + OnResponse(const LSRequestResponse& aResponse) override; +}; + +class SimpleRequestResolver final + : public LSSimpleRequestChildCallback +{ + RefPtr mPromise; + +public: + explicit SimpleRequestResolver(Promise* aPromise) + : mPromise(aPromise) + { } + + NS_INLINE_DECL_REFCOUNTING(SimpleRequestResolver, override); + +private: + ~SimpleRequestResolver() = default; + + void + HandleResponse(nsresult aResponse); + + void + HandleResponse(bool aResponse); + + // LSRequestChildCallback + void + OnResponse(const LSSimpleRequestResponse& aResponse) override; +}; + +nsresult +CreatePromise(JSContext* aContext, Promise** aPromise) +{ + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(aContext); + + nsIGlobalObject* global = + xpc::NativeGlobal(JS::CurrentGlobalOrNull(aContext)); + if (NS_WARN_IF(!global)) { + return NS_ERROR_FAILURE; + } + + ErrorResult result; + RefPtr promise = Promise::Create(global, result); + if (result.Failed()) { + return result.StealNSResult(); + } + + promise.forget(aPromise); + return NS_OK; +} + +nsresult +CheckedPrincipalToPrincipalInfo(nsIPrincipal* aPrincipal, + PrincipalInfo& aPrincipalInfo) +{ + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(aPrincipal); + + nsresult rv = PrincipalToPrincipalInfo(aPrincipal, &aPrincipalInfo); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (aPrincipalInfo.type() != PrincipalInfo::TContentPrincipalInfo && + aPrincipalInfo.type() != PrincipalInfo::TSystemPrincipalInfo) { + return NS_ERROR_UNEXPECTED; + } + + return NS_OK; +} + +} // namespace + +LocalStorageManager2::LocalStorageManager2() +{ + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(NextGenLocalStorageEnabled()); +} + +LocalStorageManager2::~LocalStorageManager2() +{ + MOZ_ASSERT(NS_IsMainThread()); +} + +NS_IMPL_ISUPPORTS(LocalStorageManager2, + nsIDOMStorageManager, + nsILocalStorageManager) + +NS_IMETHODIMP +LocalStorageManager2::PrecacheStorage(nsIPrincipal* aPrincipal, + Storage** _retval) +{ + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(aPrincipal); + MOZ_ASSERT(_retval); + + // This method was created as part of the e10s-ification of the old LS + // implementation to perform a preload in the content/current process. That's + // not how things work in LSNG. Instead everything happens in the parent + // process, triggered by the official preloading spot, + // ContentParent::AboutToLoadHttpFtpWyciwygDocumentForChild. + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +LocalStorageManager2::CreateStorage(mozIDOMWindow* aWindow, + nsIPrincipal* aPrincipal, + const nsAString& aDocumentURI, + bool aPrivate, + Storage** _retval) +{ + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(aPrincipal); + MOZ_ASSERT(_retval); + + nsCOMPtr inner = nsPIDOMWindowInner::From(aWindow); + + RefPtr object; + nsresult rv = LSObject::CreateForPrincipal(inner, + aPrincipal, + aDocumentURI, + aPrivate, + getter_AddRefs(object)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + object.forget(_retval); + return NS_OK; +} + +NS_IMETHODIMP +LocalStorageManager2::GetStorage(mozIDOMWindow* aWindow, + nsIPrincipal* aPrincipal, + bool aPrivate, + Storage** _retval) +{ + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(aPrincipal); + MOZ_ASSERT(_retval); + + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +LocalStorageManager2::CloneStorage(Storage* aStorageToCloneFrom) +{ + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(aStorageToCloneFrom); + + // Cloning is specific to sessionStorage; state is forked when a new tab is + // opened from an existing tab. + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +LocalStorageManager2::CheckStorage(nsIPrincipal* aPrincipal, + Storage *aStorage, + bool* _retval) +{ + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(aPrincipal); + MOZ_ASSERT(aStorage); + MOZ_ASSERT(_retval); + + // Only used by sessionStorage. + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +LocalStorageManager2::GetNextGenLocalStorageEnabled(bool* aResult) +{ + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(aResult); + + *aResult = NextGenLocalStorageEnabled(); + return NS_OK; +} + +NS_IMETHODIMP +LocalStorageManager2::Preload(nsIPrincipal* aPrincipal, + JSContext* aContext, + nsISupports** _retval) +{ + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(aPrincipal); + MOZ_ASSERT(_retval); + + nsresult rv; + + RefPtr promise; + + if (aContext) { + rv = CreatePromise(aContext, getter_AddRefs(promise)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } + + LSRequestPrepareDatastoreParams params; + params.createIfNotExists() = false; + + rv = CheckedPrincipalToPrincipalInfo(aPrincipal, + params.principalInfo()); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = StartRequest(promise, params); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + promise.forget(_retval); + return NS_OK; +} + +NS_IMETHODIMP +LocalStorageManager2::IsPreloaded(nsIPrincipal* aPrincipal, + JSContext* aContext, + nsISupports** _retval) +{ + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(aPrincipal); + MOZ_ASSERT(_retval); + + RefPtr promise; + nsresult rv = CreatePromise(aContext, getter_AddRefs(promise)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + LSSimpleRequestPreloadedParams params; + + rv = CheckedPrincipalToPrincipalInfo(aPrincipal, + params.principalInfo()); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = StartSimpleRequest(promise, params); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + promise.forget(_retval); + return NS_OK; +} + +nsresult +LocalStorageManager2::StartRequest(Promise* aPromise, + const LSRequestParams& aParams) +{ + MOZ_ASSERT(NS_IsMainThread()); + + PBackgroundChild* backgroundActor = + BackgroundChild::GetOrCreateForCurrentThread(); + if (NS_WARN_IF(!backgroundActor)) { + return NS_ERROR_FAILURE; + } + + RefPtr resolver = new RequestResolver(aPromise); + + auto actor = new LSRequestChild(resolver); + + if (!backgroundActor->SendPBackgroundLSRequestConstructor(actor, aParams)) { + return NS_ERROR_FAILURE; + } + + return NS_OK; +} + +nsresult +LocalStorageManager2::StartSimpleRequest(Promise* aPromise, + const LSSimpleRequestParams& aParams) +{ + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(aPromise); + + PBackgroundChild* backgroundActor = + BackgroundChild::GetOrCreateForCurrentThread(); + if (NS_WARN_IF(!backgroundActor)) { + return NS_ERROR_FAILURE; + } + + RefPtr resolver = new SimpleRequestResolver(aPromise); + + auto actor = new LSSimpleRequestChild(resolver); + + if (!backgroundActor->SendPBackgroundLSSimpleRequestConstructor(actor, + aParams)) { + return NS_ERROR_FAILURE; + } + + return NS_OK; +} + +void +RequestResolver::HandleResponse(nsresult aResponse) +{ + MOZ_ASSERT(NS_IsMainThread()); + + if (!mPromise) { + return; + } + + mPromise->MaybeReject(aResponse); +} + +void +RequestResolver::HandleResponse(const NullableDatastoreId& aDatastoreId) +{ + MOZ_ASSERT(NS_IsMainThread()); + + if (!mPromise) { + return; + } + + switch (aDatastoreId.type()) { + case NullableDatastoreId::Tnull_t: + mPromise->MaybeResolve(JS::NullHandleValue); + break; + + case NullableDatastoreId::Tuint64_t: + mPromise->MaybeResolve(aDatastoreId.get_uint64_t()); + break; + + default: + MOZ_CRASH("Unknown datastore id type!"); + } +} + +void +RequestResolver::OnResponse(const LSRequestResponse& aResponse) +{ + MOZ_ASSERT(NS_IsMainThread()); + + switch (aResponse.type()) { + case LSRequestResponse::Tnsresult: + HandleResponse(aResponse.get_nsresult()); + break; + + case LSRequestResponse::TLSRequestPrepareDatastoreResponse: + HandleResponse( + aResponse.get_LSRequestPrepareDatastoreResponse().datastoreId()); + break; + default: + MOZ_CRASH("Unknown response type!"); + } +} + +void +SimpleRequestResolver::HandleResponse(nsresult aResponse) +{ + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(mPromise); + + mPromise->MaybeReject(aResponse); +} + +void +SimpleRequestResolver::HandleResponse(bool aResponse) +{ + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(mPromise); + + mPromise->MaybeResolve(aResponse); +} + +void +SimpleRequestResolver::OnResponse(const LSSimpleRequestResponse& aResponse) +{ + MOZ_ASSERT(NS_IsMainThread()); + + switch (aResponse.type()) { + case LSSimpleRequestResponse::Tnsresult: + HandleResponse(aResponse.get_nsresult()); + break; + + case LSSimpleRequestResponse::TLSSimpleRequestPreloadedResponse: + HandleResponse( + aResponse.get_LSSimpleRequestPreloadedResponse().preloaded()); + break; + + default: + MOZ_CRASH("Unknown response type!"); + } +} + +} // namespace dom +} // namespace mozilla diff --git a/dom/localstorage/LocalStorageManager2.h b/dom/localstorage/LocalStorageManager2.h new file mode 100644 index 000000000000..a20837d695b3 --- /dev/null +++ b/dom/localstorage/LocalStorageManager2.h @@ -0,0 +1,69 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/. */ + +#ifndef mozilla_dom_localstorage_LocalStorageManager2_h +#define mozilla_dom_localstorage_LocalStorageManager2_h + +#include "nsIDOMStorageManager.h" +#include "nsILocalStorageManager.h" + +namespace mozilla { +namespace dom { + +class LSRequestParams; +class LSSimpleRequestParams; +class Promise; + +/** + * Under LSNG this exposes nsILocalStorageManager::Preload to ContentParent to + * trigger preloading. Otherwise, this is basically just a place for test logic + * that doesn't make sense to put directly on the Storage WebIDL interface. + * + * Previously, the nsIDOMStorageManager XPCOM interface was also used by + * nsGlobalWindowInner to interact with LocalStorage, but in these de-XPCOM + * days, we've moved to just directly reference the relevant concrete classes + * (ex: LSObject) directly. + * + * Note that testing methods are now also directly exposed on the Storage WebIDL + * interface for simplicity/sanity. + */ +class LocalStorageManager2 final + : public nsIDOMStorageManager + , public nsILocalStorageManager +{ +public: + LocalStorageManager2(); + + NS_DECL_ISUPPORTS + NS_DECL_NSIDOMSTORAGEMANAGER + NS_DECL_NSILOCALSTORAGEMANAGER + +private: + ~LocalStorageManager2(); + + /** + * Helper to trigger an LSRequest and resolve/reject the provided promise when + * the result comes in. This routine is notable because the LSRequest + * mechanism is normally used synchronously from content, but here it's + * exposed asynchronously. + */ + nsresult + StartRequest(Promise* aPromise, + const LSRequestParams& aParams); + + /** + * Helper to trigger an LSSimpleRequst and resolve/reject the provided promise + * when the result comes in. + */ + nsresult + StartSimpleRequest(Promise* aPromise, + const LSSimpleRequestParams& aParams); +}; + +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_localstorage_LocalStorageManager2_h diff --git a/dom/localstorage/PBackgroundLSDatabase.ipdl b/dom/localstorage/PBackgroundLSDatabase.ipdl new file mode 100644 index 000000000000..13765bc48c65 --- /dev/null +++ b/dom/localstorage/PBackgroundLSDatabase.ipdl @@ -0,0 +1,148 @@ +/* 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/. */ + +include protocol PBackground; +include protocol PBackgroundLSSnapshot; + +include "mozilla/dom/localstorage/SerializationHelpers.h"; + +using mozilla::dom::LSSnapshot::LoadState + from "mozilla/dom/LSSnapshot.h"; + +namespace mozilla { +namespace dom { + +/** + * LocalStorage key/value pair wire representations. `value` may be void in + * cases where there is a value but it is not being sent for memory/bandwidth + * conservation purposes. (It's not possible to have a null/undefined `value` + * as Storage is defined explicitly as a String store.) + */ +struct LSItemInfo +{ + nsString key; + nsString value; +}; + +/** + * Initial LSSnapshot state as produced by Datastore::GetSnapshotInitInfo. See + * `LSSnapshot::LoadState` for more details about the possible states and a + * high level overview. + */ +struct LSSnapshotInitInfo +{ + /** + * As many key/value or key/void pairs as the snapshot prefill byte budget + * allowed. + */ + LSItemInfo[] itemInfos; + /** + * The total number of key/value pairs in LocalStorage for this origin at the + * time the snapshot was created. (And the point of the snapshot is to + * conceptually freeze the state of the Datastore in time, so this value does + * not change despite what other LSDatabase objects get up to in other + * processes.) + */ + uint32_t totalLength; + /** + * The current amount of LocalStorage usage as measured by the summing the + * nsString Length() of both the key and the value over all stored pairs. + */ + int64_t initialUsage; + /** + * The amount of storage allowed to be used by the Snapshot without requesting + * more storage space via IncreasePeakUsage. This is the `initialUsage` plus + * 0 or more bytes of space. If space was available, the increase will be the + * `requestedSize` from the PBackgroundLSSnapshot constructor. If the + * LocalStorage usage was already close to the limit, then the fallback is the + * `minSize` requested, or 0 if there wasn't space for that. + */ + int64_t peakUsage; + // See `LSSnapshot::LoadState` in `LSSnapshot.h` + LoadState loadState; +}; + +/** + * This protocol is asynchronously created via constructor on PBackground but + * has synchronous semantics from the perspective of content on the main thread. + * The construction potentially involves waiting for disk I/O to load the + * LocalStorage data from disk as well as related QuotaManager checks, so async + * calls to PBackground are the only viable mechanism because blocking + * PBackground is not acceptable. (Note that an attempt is made to minimize any + * I/O latency by triggering preloading from + * ContentParent::AboutToLoadHttpFtpWyciwygDocumentForChild, the central place + * for pre-loading.) + */ +sync protocol PBackgroundLSDatabase +{ + manager PBackground; + manages PBackgroundLSSnapshot; + +parent: + // The DeleteMe message is used to avoid a race condition between the parent + // actor and the child actor. The PBackgroundLSDatabase protocol could be + // simply destroyed by sending the __delete__ message from the child side. + // However, that would destroy the child actor immediatelly and the parent + // could be sending a message to the child at the same time resulting in a + // routing error since the child actor wouldn't exist anymore. A routing + // error typically causes a crash. The race can be prevented by doing the + // teardown in two steps. First, we send the DeleteMe message to the parent + // and the parent then sends the __delete__ message to the child. + async DeleteMe(); + + /** + * Sent in response to a `RequestAllowToClose` message once the snapshot + * cleanup has happened OR from LSDatabase's destructor if AllowToClose has + * not already been reported. + */ + async AllowToClose(); + + /** + * Invoked to create an LSSnapshot backed by a Snapshot in PBackground that + * presents an atomic and consistent view of the state of the authoritative + * Datastore state in the parent. + * + * This needs to be synchronous because LocalStorage's semantics are + * synchronous. Note that the Datastore in the PBackground parent already + * has the answers to this request immediately available without needing to + * consult any other threads or perform any I/O. Additionally, the response + * is explicitly bounded in size by the tunable snapshot prefill byte limit. + * + * @param increasePeakUsage + * Whether the parent should attempt to pre-allocate some amount of quota + * usage to the Snapshot. + */ + sync PBackgroundLSSnapshot(nsString documentURI, + bool increasePeakUsage, + int64_t requestedSize, + int64_t minSize) + returns (LSSnapshotInitInfo initInfo); + +child: + /** + * Only sent by the parent in response to the child's DeleteMe request. + */ + async __delete__(); + + /** + * Request to close the LSDatabase, checkpointing and finishing any + * outstanding snapshots so no state is lost. This request is issued when + * QuotaManager is shutting down or is aborting operations for an origin or + * process. Once the snapshot has cleaned up, AllowToClose will be sent to + * the parent. + * + * Note that the QuotaManager shutdown process is more likely to happen in + * unit tests where we explicitly reset the QuotaManager. At runtime, we + * expect windows to be closed and content processes terminated well before + * QuotaManager shutdown would actually occur. + * + * Also, Operations are usually aborted for an origin due to privacy API's + * clearing data for an origin. Operations are aborted for a process by + * ContentParent::ShutDownProcess. + */ + async RequestAllowToClose(); +}; + +} // namespace dom +} // namespace mozilla diff --git a/dom/localstorage/PBackgroundLSObserver.ipdl b/dom/localstorage/PBackgroundLSObserver.ipdl new file mode 100644 index 000000000000..56434075427d --- /dev/null +++ b/dom/localstorage/PBackgroundLSObserver.ipdl @@ -0,0 +1,57 @@ +/* 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/. */ + +include protocol PBackground; + +include PBackgroundSharedTypes; + +namespace mozilla { +namespace dom { + +/** + * The observer protocol sends "storage" event notifications for changes to + * LocalStorage that take place in other processes as their Snapshots are + * Checkpointed to the canonical Datastore in the parent process. Same-process + * notifications are generated as mutations happen. + * + * Note that mutations are never generated for redundant mutations. Setting the + * key "foo" to have value "bar" when it already has value "bar" will never + * result in a "storage" event. + */ +async protocol PBackgroundLSObserver +{ + manager PBackground; + +parent: + /** + * Sent by the LSObserver's destructor when it's going away. Any Observe + * messages received after this is sent will be ignored. Which is fine, + * because there should be nothing around left to hear. In the event a new + * page came into existence, its Observer creation will happen (effectively) + * synchronously. + */ + async DeleteMe(); + +child: + /** + * Only sent by the parent in response to a deletion request. + */ + async __delete__(); + + /** + * Sent by the parent process as Snapshots from other processes are + * Checkpointed, applying their mutations. The child actor currently directly + * shunts these to Storage::NotifyChange to generate "storage" events for + * immediate dispatch. + */ + async Observe(PrincipalInfo principalInfo, + uint32_t privateBrowsingId, + nsString documentURI, + nsString key, + nsString oldValue, + nsString newValue); +}; + +} // namespace dom +} // namespace mozilla diff --git a/dom/localstorage/PBackgroundLSRequest.ipdl b/dom/localstorage/PBackgroundLSRequest.ipdl new file mode 100644 index 000000000000..c30270be3b5c --- /dev/null +++ b/dom/localstorage/PBackgroundLSRequest.ipdl @@ -0,0 +1,103 @@ +/* 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/. */ + +include protocol PBackground; + +using struct mozilla::null_t + from "ipc/IPCMessageUtils.h"; + +namespace mozilla { +namespace dom { + +union NullableDatastoreId +{ + null_t; + uint64_t; +}; + +struct LSRequestPrepareDatastoreResponse +{ + NullableDatastoreId datastoreId; +}; + +struct LSRequestPrepareObserverResponse +{ + uint64_t observerId; +}; + +/** + * Discriminated union which can contain an error code (`nsresult`) or + * particular request response. + */ +union LSRequestResponse +{ + nsresult; + LSRequestPrepareDatastoreResponse; + LSRequestPrepareObserverResponse; +}; + +/** + * An asynchronous protocol for issuing requests that are used in a synchronous + * fashion by LocalStorage via LSObject's RequestHelper mechanism. This differs + * from LSSimpleRequest which is implemented and used asynchronously. + * + * See `PBackgroundLSSharedTypes.ipdlh` for more on the request types, the + * response types above for their corresponding responses, and `RequestHelper` + * for more on the usage and lifecycle of this mechanism. + */ +protocol PBackgroundLSRequest +{ + manager PBackground; + +parent: + // The Cancel message is used to avoid a possible dead lock caused by a CPOW + // sending a synchronous message from the main thread in the chrome process + // to the main thread in the content process at the time we are blocking + // the main thread in the content process to handle a request. + // We use the PBackground thread on the parent side to handle requests, but + // sometimes we need to get information from principals and that's currently + // only possible on the main thread. So if the main thread in the chrome + // process is blocked by a CPOW operation, our request must wait for the CPOW + // operation to complete. However the CPOW operation can't complete either + // because we are blocking the main thread in the content process. + // The dead lock is prevented by canceling our nested event loop in the + // content process when we receive a synchronous IPC message from the parent. + // + // Note that cancellation isn't instantaneous. It's just an asynchronous flow + // that definitely doesn't involve the main thread in the parent process, so + // we're guaranteed to unblock the main-thread in the content process and + // allow the sync IPC to make progress. When Cancel() is received by the + // parent, it will Send__delete__. The child will either send Cancel or + // Finish, but not both. + async Cancel(); + + /** + * Sent by the child in response to Ready, requesting that __delete__ be sent + * with the result. The child will either send Finish or Cancel, but not + * both. No further message will be sent from the child after invoking one. + */ + async Finish(); + +child: + /** + * The deletion is sent with the result of the request directly in response to + * either Cancel or Finish. + */ + async __delete__(LSRequestResponse response); + + /** + * Sent by the parent when it has completed whatever async stuff it needs to + * do and is ready to send the results. It then awaits the Finish() call to + * send the results. This may seem redundant, but it's not. If the + * __delete__ was sent directly, it's possible there could be a race where + * Cancel() would be received by the parent after it had already sent + * __delete__. (Which may no longer be fatal thanks to improvements to the + * IPC layer, but it would still lead to warnings, etc. And we don't + * expect PBackground to be highly contended nor the DOM File thread.) + */ + async Ready(); +}; + +} // namespace dom +} // namespace mozilla diff --git a/dom/localstorage/PBackgroundLSSharedTypes.ipdlh b/dom/localstorage/PBackgroundLSSharedTypes.ipdlh new file mode 100644 index 000000000000..3ed292a15d51 --- /dev/null +++ b/dom/localstorage/PBackgroundLSSharedTypes.ipdlh @@ -0,0 +1,38 @@ +/* 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/. */ + +include PBackgroundSharedTypes; + +namespace mozilla { +namespace dom { + +struct LSRequestPrepareDatastoreParams +{ + PrincipalInfo principalInfo; + bool createIfNotExists; +}; + +struct LSRequestPrepareObserverParams +{ + PrincipalInfo principalInfo; +}; + +union LSRequestParams +{ + LSRequestPrepareDatastoreParams; + LSRequestPrepareObserverParams; +}; + +struct LSSimpleRequestPreloadedParams +{ + PrincipalInfo principalInfo; +}; + +union LSSimpleRequestParams +{ + LSSimpleRequestPreloadedParams; +}; + +} // namespace dom +} // namespace mozilla diff --git a/dom/localstorage/PBackgroundLSSimpleRequest.ipdl b/dom/localstorage/PBackgroundLSSimpleRequest.ipdl new file mode 100644 index 000000000000..c4bdd8c2771b --- /dev/null +++ b/dom/localstorage/PBackgroundLSSimpleRequest.ipdl @@ -0,0 +1,50 @@ +/* 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/. */ + +include protocol PBackground; + +namespace mozilla { +namespace dom { + +/** + * Response to a `LSSimpleRequestPreloadedParams` request indicating whether the + * origin was preloaded. + */ +struct LSSimpleRequestPreloadedResponse +{ + bool preloaded; +}; + +/** + * Discriminated union which can contain an error code (`nsresult`) or + * particular simple request response. + */ +union LSSimpleRequestResponse +{ + nsresult; + LSSimpleRequestPreloadedResponse; +}; + +/** + * Simple requests are async-only from both a protocol perspective and the + * manner in which they're used. In comparison, PBackgroundLSRequests are + * async only from a protocol perspective; they are used synchronously from the + * main thread via LSObject's RequestHelper mechanism. (With the caveat that + * nsILocalStorageManager does expose LSRequests asynchronously.) + * + * These requests use the common idiom where the arguments to the request are + * sent in the constructor and the result is sent in the __delete__ response. + * Request types are indicated by the Params variant used and those live in + * `PBackgroundLSSharedTypes.ipdlh`. + */ +protocol PBackgroundLSSimpleRequest +{ + manager PBackground; + +child: + async __delete__(LSSimpleRequestResponse response); +}; + +} // namespace dom +} // namespace mozilla diff --git a/dom/localstorage/PBackgroundLSSnapshot.ipdl b/dom/localstorage/PBackgroundLSSnapshot.ipdl new file mode 100644 index 000000000000..5333fa4ea9ce --- /dev/null +++ b/dom/localstorage/PBackgroundLSSnapshot.ipdl @@ -0,0 +1,114 @@ +/* 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/. */ + +include protocol PBackground; +include protocol PBackgroundLSDatabase; + +namespace mozilla { +namespace dom { + +struct LSSetItemInfo +{ + nsString key; + nsString oldValue; + nsString value; +}; + +struct LSRemoveItemInfo +{ + nsString key; + nsString oldValue; +}; + +struct LSClearInfo +{ +}; + +/** + * Union of LocalStorage mutation types. + */ +union LSWriteInfo +{ + LSSetItemInfo; + LSRemoveItemInfo; + LSClearInfo; +}; + +sync protocol PBackgroundLSSnapshot +{ + manager PBackgroundLSDatabase; + +parent: + async DeleteMe(); + + async Checkpoint(LSWriteInfo[] writeInfos); + + async Finish(); + + async Loaded(); + + /** + * Invoked on demand to load an item that didn't fit into the initial + * snapshot prefill. + * + * This needs to be synchronous because LocalStorage's semantics are + * synchronous. Note that the Snapshot in the PBackground parent already + * has the answers to this request immediately available without needing to + * consult any other threads or perform any I/O. + */ + sync LoadItem(nsString key) + returns (nsString value); + + /** + * Invoked on demand to load all keys in in their canonical order if they + * didn't fit into the initial snapshot prefill. + * + * This needs to be synchronous because LocalStorage's semantics are + * synchronous. Note that the Snapshot in the PBackground parent already + * has the answers to this request immediately available without needing to + * consult any other threads or perform any I/O. + */ + sync LoadKeys() + returns (nsString[] keys); + + /** + * This needs to be synchronous because LocalStorage's semantics are + * synchronous. Note that the Snapshot in the PBackground parent typically + * doesn't need to consult any other threads or perform any I/O to handle + * this request. However, it has to call a quota manager method that can + * potentially do I/O directly on the PBackground thread. It can only happen + * rarely in a storage pressure (low storage space) situation. Specifically, + * after we get a list of origin directories for eviction, we will delete + * them directly on the PBackground thread. This doesn't cause any + * performance problems, but avoiding I/O completely might need to be done as + * a futher optimization. + */ + sync IncreasePeakUsage(int64_t requestedSize, int64_t minSize) + returns (int64_t size); + + // A synchronous ping to the parent actor to confirm that the parent actor + // has received previous async message. This should only be used by the + // snapshotting code to end an explicit snapshot. + sync Ping(); + +child: + /** + * Compels the child LSSnapshot to Checkpoint() and Finish(), effectively + * compelling the snapshot to flush any issued mutations and close itself. + * The child LSSnapshot does that either immediately if it's just waiting + * to be reused or when it gets into a stable state. + * + * This message is expected to be sent in the following two cases only: + * 1. The state of the underlying Datastore starts to differ from the state + * captured at the time of snapshot creation. + * 2. The last private browsing context exits. And in that case we expect + * all private browsing globals to already have been destroyed. + */ + async MarkDirty(); + + async __delete__(); +}; + +} // namespace dom +} // namespace mozilla diff --git a/dom/localstorage/ReportInternalError.cpp b/dom/localstorage/ReportInternalError.cpp new file mode 100644 index 000000000000..a15209687f9d --- /dev/null +++ b/dom/localstorage/ReportInternalError.cpp @@ -0,0 +1,36 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/. */ + +#include "ReportInternalError.h" + +#include "mozilla/IntegerPrintfMacros.h" + +#include "nsContentUtils.h" +#include "nsPrintfCString.h" + +namespace mozilla { +namespace dom { +namespace localstorage { + +void +ReportInternalError(const char* aFile, uint32_t aLine, const char* aStr) +{ + // Get leaf of file path + for (const char* p = aFile; *p; ++p) { + if (*p == '/' && *(p + 1)) { + aFile = p + 1; + } + } + + nsContentUtils::LogSimpleConsoleError( + NS_ConvertUTF8toUTF16(nsPrintfCString( + "LocalStorage %s: %s:%" PRIu32, aStr, aFile, aLine)), + "localstorage", false); +} + +} // namespace localstorage +} // namespace dom +} // namespace mozilla diff --git a/dom/localstorage/ReportInternalError.h b/dom/localstorage/ReportInternalError.h new file mode 100644 index 000000000000..1988a880d5f8 --- /dev/null +++ b/dom/localstorage/ReportInternalError.h @@ -0,0 +1,32 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/. */ + +#ifndef mozilla_dom_localstorage_ReportInternalError_h +#define mozilla_dom_localstorage_ReportInternalError_h + +#include "nsDebug.h" + +#define LS_WARNING(...) \ + do { \ + nsPrintfCString s(__VA_ARGS__); \ + mozilla::dom::localstorage::ReportInternalError(__FILE__, \ + __LINE__, \ + s.get()); \ + NS_WARNING(s.get()); \ + } while (0) + +namespace mozilla { +namespace dom { +namespace localstorage { + +void +ReportInternalError(const char* aFile, uint32_t aLine, const char* aStr); + +} // namespace localstorage +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_localstorage_ReportInternalError_h diff --git a/dom/localstorage/SerializationHelpers.h b/dom/localstorage/SerializationHelpers.h new file mode 100644 index 000000000000..d13586a17d5c --- /dev/null +++ b/dom/localstorage/SerializationHelpers.h @@ -0,0 +1,25 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/. */ + +#ifndef mozilla_dom_localstorage_SerializationHelpers_h +#define mozilla_dom_localstorage_SerializationHelpers_h + +#include "ipc/IPCMessageUtils.h" + +#include "mozilla/dom/LSSnapshot.h" + +namespace IPC { + +template <> +struct ParamTraits : + public ContiguousEnumSerializer +{ }; + +} // namespace IPC + +#endif // mozilla_dom_localstorage_SerializationHelpers_h diff --git a/dom/localstorage/moz.build b/dom/localstorage/moz.build new file mode 100644 index 000000000000..048e52e247c5 --- /dev/null +++ b/dom/localstorage/moz.build @@ -0,0 +1,64 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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/. + +XPCSHELL_TESTS_MANIFESTS += [ + 'test/unit/xpcshell.ini' +] + +TEST_HARNESS_FILES.xpcshell.dom.localstorage.test.unit += [ + 'test/unit/databaseShadowing-shared.js', +] + +XPIDL_SOURCES += [ + 'nsILocalStorageManager.idl', +] + +XPIDL_MODULE = 'dom_localstorage' + +EXPORTS.mozilla.dom.localstorage += [ + 'ActorsParent.h', + 'SerializationHelpers.h', +] + +EXPORTS.mozilla.dom += [ + 'LocalStorageCommon.h', + 'LocalStorageManager2.h', + 'LSObject.h', + 'LSObserver.h', + 'LSSnapshot.h', +] + +UNIFIED_SOURCES += [ + 'ActorsChild.cpp', + 'ActorsParent.cpp', + 'LocalStorageCommon.cpp', + 'LocalStorageManager2.cpp', + 'LSDatabase.cpp', + 'LSObject.cpp', + 'LSObserver.cpp', + 'LSSnapshot.cpp', + 'ReportInternalError.cpp', +] + +IPDL_SOURCES += [ + 'PBackgroundLSDatabase.ipdl', + 'PBackgroundLSObserver.ipdl', + 'PBackgroundLSRequest.ipdl', + 'PBackgroundLSSharedTypes.ipdlh', + 'PBackgroundLSSimpleRequest.ipdl', + 'PBackgroundLSSnapshot.ipdl', +] + +include('/ipc/chromium/chromium-config.mozbuild') + +FINAL_LIBRARY = 'xul' + +if CONFIG['GNU_CXX']: + CXXFLAGS += ['-Wno-error=shadow'] + +LOCAL_INCLUDES += [ + '/dom/file/ipc', +] diff --git a/dom/localstorage/nsILocalStorageManager.idl b/dom/localstorage/nsILocalStorageManager.idl new file mode 100644 index 000000000000..eb712fd75c3b --- /dev/null +++ b/dom/localstorage/nsILocalStorageManager.idl @@ -0,0 +1,35 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=2 et sw=2 tw=80: */ +/* 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/. */ + +#include "nsISupports.idl" + +interface nsIPrincipal; + +/** + * Methods specific to LocalStorage, see nsIDOMStorageManager for methods shared + * with SessionStorage. Methods may migrate there as SessionStorage is + * overhauled. + */ +[scriptable, builtinclass, uuid(d4f534da-2744-4db3-8774-8b187c64ade9)] +interface nsILocalStorageManager : nsISupports +{ + readonly attribute boolean nextGenLocalStorageEnabled; + + /** + * Trigger preload of LocalStorage for the given principal. For use by + * ContentParent::AboutToLoadHttpFtpWyciwygDocumentForChild to maximize the + * amount of time we have to load the data off disk before the page might + * attempt to touch LocalStorage. + * + * This method will not create a QuotaManager-managed directory on disk if + * one does not already exist for the principal. + */ + [implicit_jscontext] nsISupports + preload(in nsIPrincipal aPrincipal); + + [implicit_jscontext] nsISupports + isPreloaded(in nsIPrincipal aPrincipal); +}; diff --git a/dom/localstorage/test/unit/archive_profile.zip b/dom/localstorage/test/unit/archive_profile.zip new file mode 100644 index 000000000000..71b2d1e5f90d Binary files /dev/null and b/dom/localstorage/test/unit/archive_profile.zip differ diff --git a/dom/localstorage/test/unit/databaseShadowing-shared.js b/dom/localstorage/test/unit/databaseShadowing-shared.js new file mode 100644 index 000000000000..82b2cbb43dc8 --- /dev/null +++ b/dom/localstorage/test/unit/databaseShadowing-shared.js @@ -0,0 +1,114 @@ +const principalInfos = [ + { url: "http://example.com", attrs: {} }, + + { url: "http://origin.test", attrs: {} }, + + { url: "http://prefix.test", attrs: {} }, + { url: "http://prefix.test", attrs: { userContextId: 10 } }, + + { url: "http://pattern.test", attrs: { userContextId: 15 } }, + { url: "http://pattern.test:8080", attrs: { userContextId: 15 } }, + { url: "https://pattern.test", attrs: { userContextId: 15 } }, +]; + +function enableNextGenLocalStorage() +{ + info("Setting pref"); + + Services.prefs.setBoolPref("dom.storage.next_gen", true); +} + +function disableNextGenLocalStorage() +{ + info("Setting pref"); + + Services.prefs.setBoolPref("dom.storage.next_gen", false); +} + +function storeData() +{ + for (let i = 0; i < principalInfos.length; i++) { + let principalInfo = principalInfos[i]; + let principal = getPrincipal(principalInfo.url, principalInfo.attrs); + + info("Getting storage"); + + let storage = getLocalStorage(principal); + + info("Adding data"); + + storage.setItem("key0", "value0"); + storage.clear(); + storage.setItem("key1", "value1"); + storage.removeItem("key1"); + storage.setItem("key2", "value2"); + + info("Closing storage"); + + storage.close(); + } +} + +function exportShadowDatabase(name) +{ + info("Verifying shadow database"); + + let profileDir = Services.dirsvc.get("ProfD", Ci.nsIFile); + let shadowDatabase = profileDir.clone(); + shadowDatabase.append("webappsstore.sqlite"); + + let exists = shadowDatabase.exists(); + ok(exists, "Shadow database does exist"); + + info("Copying shadow database"); + + let currentDir = Services.dirsvc.get("CurWorkD", Ci.nsIFile); + shadowDatabase.copyTo(currentDir, name); +} + +function importShadowDatabase(name) +{ + info("Verifying shadow database"); + + let currentDir = Services.dirsvc.get("CurWorkD", Ci.nsIFile); + let shadowDatabase = currentDir.clone(); + shadowDatabase.append(name); + + let exists = shadowDatabase.exists(); + if (!exists) { + return false; + } + + info("Copying shadow database"); + + let profileDir = Services.dirsvc.get("ProfD", Ci.nsIFile); + shadowDatabase.copyTo(profileDir, "webappsstore.sqlite"); + + return true; +} + +function verifyData(clearedOrigins) +{ + for (let i = 0; i < principalInfos.length; i++) { + let principalInfo = principalInfos[i]; + let principal = getPrincipal(principalInfo.url, principalInfo.attrs); + + info("Getting storage"); + + let storage = getLocalStorage(principal); + + info("Verifying data"); + + if (clearedOrigins.includes(i)) { + ok(storage.getItem("key2") == null, "Correct value"); + } else { + ok(storage.getItem("key0") == null, "Correct value"); + ok(storage.getItem("key1") == null, "Correct value"); + ok(storage.getItem("key2") == "value2", "Correct value"); + } + + info("Closing storage"); + + storage.close(); + } +} diff --git a/dom/localstorage/test/unit/head.js b/dom/localstorage/test/unit/head.js new file mode 100644 index 000000000000..61f69ab2daa6 --- /dev/null +++ b/dom/localstorage/test/unit/head.js @@ -0,0 +1,271 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +const NS_ERROR_DOM_QUOTA_EXCEEDED_ERR = 22; + +ChromeUtils.import("resource://gre/modules/Services.jsm"); + +function is(a, b, msg) +{ + Assert.equal(a, b, msg); +} + +function ok(cond, msg) +{ + Assert.ok(!!cond, msg); +} + +function run_test() +{ + runTest(); +}; + +if (!this.runTest) { + this.runTest = function() + { + do_get_profile(); + + enableTesting(); + + Assert.ok(typeof testSteps === "function", + "There should be a testSteps function"); + Assert.ok(testSteps.constructor.name === "AsyncFunction", + "testSteps should be an async function"); + + registerCleanupFunction(resetTesting); + + add_task(testSteps); + + // Since we defined run_test, we must invoke run_next_test() to start the + // async test. + run_next_test(); + } +} + +function returnToEventLoop() +{ + return new Promise(function(resolve) { + executeSoon(resolve); + }); +} + +function enableTesting() +{ + Services.prefs.setBoolPref("dom.storage.testing", true); + Services.prefs.setBoolPref("dom.quotaManager.testing", true); +} + +function resetTesting() +{ + Services.prefs.clearUserPref("dom.quotaManager.testing"); + Services.prefs.clearUserPref("dom.storage.testing"); +} + +function setGlobalLimit(globalLimit) +{ + Services.prefs.setIntPref("dom.quotaManager.temporaryStorage.fixedLimit", + globalLimit); +} + +function resetGlobalLimit() +{ + Services.prefs.clearUserPref("dom.quotaManager.temporaryStorage.fixedLimit"); +} + +function setOriginLimit(originLimit) +{ + Services.prefs.setIntPref("dom.storage.default_quota", originLimit); +} + +function resetOriginLimit() +{ + Services.prefs.clearUserPref("dom.storage.default_quota"); +} + +function getOriginUsage(principal) +{ + let request = Services.qms.getUsageForPrincipal(principal, function() { }); + + return request; +} + +function clear() +{ + let request = Services.qms.clear(); + + return request; +} + +function clearOriginsByPattern(pattern) +{ + let request = Services.qms.clearStoragesForOriginAttributesPattern(pattern); + + return request; +} + +function clearOriginsByPrefix(principal, persistence) +{ + let request = + Services.qms.clearStoragesForPrincipal(principal, persistence, null, true); + + return request; +} + +function clearOrigin(principal, persistence) +{ + let request = Services.qms.clearStoragesForPrincipal(principal, persistence); + + return request; +} + +function reset(callback) +{ + let request = Services.qms.reset(); + request.callback = callback; + + return request; +} + +function resetOrigin(principal) +{ + let request = + Services.qms.resetStoragesForPrincipal(principal, "default", "ls"); + + return request; +} + +function installPackage(packageName) +{ + let directoryService = Cc["@mozilla.org/file/directory_service;1"] + .getService(Ci.nsIProperties); + + let currentDir = directoryService.get("CurWorkD", Ci.nsIFile); + + let packageFile = currentDir.clone(); + packageFile.append(packageName + ".zip"); + + let zipReader = Cc["@mozilla.org/libjar/zip-reader;1"] + .createInstance(Ci.nsIZipReader); + zipReader.open(packageFile); + + let entryNames = []; + let entries = zipReader.findEntries(null); + while (entries.hasMore()) { + let entry = entries.getNext(); + entryNames.push(entry); + } + entryNames.sort(); + + for (let entryName of entryNames) { + let zipentry = zipReader.getEntry(entryName); + + let file = getRelativeFile(entryName); + + if (zipentry.isDirectory) { + file.create(Ci.nsIFile.DIRECTORY_TYPE, parseInt("0755", 8)); + } else { + let istream = zipReader.getInputStream(entryName); + + var ostream = Cc["@mozilla.org/network/file-output-stream;1"] + .createInstance(Ci.nsIFileOutputStream); + ostream.init(file, -1, parseInt("0644", 8), 0); + + let bostream = Cc['@mozilla.org/network/buffered-output-stream;1'] + .createInstance(Ci.nsIBufferedOutputStream); + bostream.init(ostream, 32768); + + bostream.writeFrom(istream, istream.available()); + + istream.close(); + bostream.close(); + } + } + + zipReader.close(); +} + +function getProfileDir() +{ + let directoryService = + Cc["@mozilla.org/file/directory_service;1"].getService(Ci.nsIProperties); + + return directoryService.get("ProfD", Ci.nsIFile); +} + +// Given a "/"-delimited path relative to the profile directory, +// return an nsIFile representing the path. This does not test +// for the existence of the file or parent directories. +// It is safe even on Windows where the directory separator is not "/", +// but make sure you're not passing in a "\"-delimited path. +function getRelativeFile(relativePath) +{ + let profileDir = getProfileDir(); + + let file = profileDir.clone(); + relativePath.split('/').forEach(function(component) { + file.append(component); + }); + + return file; +} + +function repeatChar(count, ch) { + if (count == 0) { + return ""; + } + + let result = ch; + let count2 = count / 2; + + // Double the input until it is long enough. + while (result.length <= count2) { + result += result; + } + + // Use substring to hit the precise length target without using extra memory. + return result + result.substring(0, count - result.length); +} + +function getPrincipal(url, attrs) +{ + let uri = Services.io.newURI(url); + if (!attrs) { + attrs = {}; + } + return Services.scriptSecurityManager.createCodebasePrincipal(uri, attrs); +} + +function getCurrentPrincipal() +{ + return Cc["@mozilla.org/systemprincipal;1"].createInstance(Ci.nsIPrincipal); +} + +function getLocalStorage(principal) +{ + if (!principal) { + principal = getCurrentPrincipal(); + } + + return Services.domStorageManager.createStorage(null, principal, ""); +} + +function requestFinished(request) { + return new Promise(function(resolve, reject) { + request.callback = function(request) { + if (request.resultCode == Cr.NS_OK) { + resolve(request.result); + } else { + reject(request.resultCode); + } + } + }); +} + +function loadSubscript(path) +{ + let file = do_get_file(path, false); + let uri = Services.io.newFileURI(file); + Services.scriptloader.loadSubScript(uri.spec); +} diff --git a/dom/localstorage/test/unit/migration_profile.zip b/dom/localstorage/test/unit/migration_profile.zip new file mode 100644 index 000000000000..19dc3d480537 Binary files /dev/null and b/dom/localstorage/test/unit/migration_profile.zip differ diff --git a/dom/localstorage/test/unit/test_archive.js b/dom/localstorage/test/unit/test_archive.js new file mode 100644 index 000000000000..272d677b1064 --- /dev/null +++ b/dom/localstorage/test/unit/test_archive.js @@ -0,0 +1,76 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +async function testSteps() +{ + const lsArchiveFile = "storage/ls-archive.sqlite"; + + const principalInfo = { + url: "http://example.com", + attrs: {} + }; + + function checkStorage() + { + let principal = getPrincipal(principalInfo.url, principalInfo.attrs); + let storage = getLocalStorage(principal); + try { + storage.open(); + ok(true, "Did not throw"); + } catch(ex) { + ok(false, "Should not have thrown"); + } + } + + info("Setting pref"); + + Services.prefs.setBoolPref("dom.storage.next_gen", true); + + // Profile 1 - Archive file is a directory. + info("Clearing"); + + let request = clear(); + await requestFinished(request); + + let archiveFile = getRelativeFile(lsArchiveFile); + + archiveFile.create(Ci.nsIFile.DIRECTORY_TYPE, parseInt("0755", 8)); + + checkStorage(); + + // Profile 2 - Corrupted archive file. + info("Clearing"); + + request = clear(); + await requestFinished(request); + + let ostream = Cc["@mozilla.org/network/file-output-stream;1"] + .createInstance(Ci.nsIFileOutputStream); + ostream.init(archiveFile, -1, parseInt("0644", 8), 0); + ostream.write("foobar", 6); + ostream.close(); + + checkStorage(); + + // Profile 3 - Nonupdateable archive file. + info("Clearing"); + + request = clear(); + await requestFinished(request); + + info("Installing package"); + + // The profile contains storage.sqlite and storage/ls-archive.sqlite + // storage/ls-archive.sqlite was taken from FF 54 to force an upgrade. + // There's just one record in the webappsstore2 table. The record was + // modified by renaming the origin attribute userContextId to userContextKey. + // This triggers an error during the upgrade. + installPackage("archive_profile"); + + let fileSize = archiveFile.fileSize; + ok(fileSize > 0, "archive file size is greater than zero"); + + checkStorage(); +} diff --git a/dom/localstorage/test/unit/test_databaseShadowing1.js b/dom/localstorage/test/unit/test_databaseShadowing1.js new file mode 100644 index 000000000000..5533941a7489 --- /dev/null +++ b/dom/localstorage/test/unit/test_databaseShadowing1.js @@ -0,0 +1,23 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +loadSubscript("databaseShadowing-shared.js"); + +async function testSteps() +{ + enableNextGenLocalStorage(); + + storeData(); + + verifyData([]); + + // Wait for all database connections to close. + let request = reset(); + await requestFinished(request); + + exportShadowDatabase("shadowdb.sqlite"); + + // The shadow database is now prepared for test_databaseShadowing2.js +} diff --git a/dom/localstorage/test/unit/test_databaseShadowing2.js b/dom/localstorage/test/unit/test_databaseShadowing2.js new file mode 100644 index 000000000000..f0f5f7e7ae38 --- /dev/null +++ b/dom/localstorage/test/unit/test_databaseShadowing2.js @@ -0,0 +1,19 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +loadSubscript("databaseShadowing-shared.js"); + +async function testSteps() +{ + // The shadow database was prepared in test_databaseShadowing1.js + + disableNextGenLocalStorage(); + + if (!importShadowDatabase("shadowdb.sqlite")) { + return; + } + + verifyData([]); +} diff --git a/dom/localstorage/test/unit/test_databaseShadowing_clearOrigin1.js b/dom/localstorage/test/unit/test_databaseShadowing_clearOrigin1.js new file mode 100644 index 000000000000..536fe0332d6f --- /dev/null +++ b/dom/localstorage/test/unit/test_databaseShadowing_clearOrigin1.js @@ -0,0 +1,30 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +loadSubscript("databaseShadowing-shared.js"); + +async function testSteps() +{ + enableNextGenLocalStorage(); + + storeData(); + + verifyData([]); + + let principal = getPrincipal("http://origin.test", {}); + let request = clearOrigin(principal, "default"); + await requestFinished(request); + + verifyData([1]); + + // Wait for all database connections to close. + request = reset(); + await requestFinished(request); + + exportShadowDatabase("shadowdb_clearedOrigin.sqlite"); + + // The shadow database is now prepared for + // test_databaseShadowing_clearOrigin2.js +} diff --git a/dom/localstorage/test/unit/test_databaseShadowing_clearOrigin2.js b/dom/localstorage/test/unit/test_databaseShadowing_clearOrigin2.js new file mode 100644 index 000000000000..43f1d8b9c3ad --- /dev/null +++ b/dom/localstorage/test/unit/test_databaseShadowing_clearOrigin2.js @@ -0,0 +1,19 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +loadSubscript("databaseShadowing-shared.js"); + +async function testSteps() +{ + // The shadow database was prepared in test_databaseShadowing_clearOrigin1.js + + disableNextGenLocalStorage(); + + if (!importShadowDatabase("shadowdb-clearedOrigin.sqlite")) { + return; + } + + verifyData([1]); +} diff --git a/dom/localstorage/test/unit/test_databaseShadowing_clearOriginsByPattern1.js b/dom/localstorage/test/unit/test_databaseShadowing_clearOriginsByPattern1.js new file mode 100644 index 000000000000..12ca2b770ffb --- /dev/null +++ b/dom/localstorage/test/unit/test_databaseShadowing_clearOriginsByPattern1.js @@ -0,0 +1,29 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +loadSubscript("databaseShadowing-shared.js"); + +async function testSteps() +{ + enableNextGenLocalStorage(); + + storeData(); + + verifyData([]); + + let request = clearOriginsByPattern(JSON.stringify({ userContextId: 15 })); + await requestFinished(request); + + verifyData([4,5,6]); + + // Wait for all database connections to close. + request = reset(); + await requestFinished(request); + + exportShadowDatabase("shadowdb-clearedOriginsByPattern.sqlite"); + + // The shadow database is now prepared for + // test_databaseShadowing_clearOriginsByPattern2.js +} diff --git a/dom/localstorage/test/unit/test_databaseShadowing_clearOriginsByPattern2.js b/dom/localstorage/test/unit/test_databaseShadowing_clearOriginsByPattern2.js new file mode 100644 index 000000000000..337eea04a5aa --- /dev/null +++ b/dom/localstorage/test/unit/test_databaseShadowing_clearOriginsByPattern2.js @@ -0,0 +1,20 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +loadSubscript("databaseShadowing-shared.js"); + +async function testSteps() +{ + // The shadow database was prepared in + // test_databaseShadowing_clearOriginsByPattern1.js + + disableNextGenLocalStorage(); + + if (!importShadowDatabase("shadowdb-clearedOriginsByPattern.sqlite")) { + return; + } + + verifyData([4,5,6]); +} diff --git a/dom/localstorage/test/unit/test_databaseShadowing_clearOriginsByPrefix1.js b/dom/localstorage/test/unit/test_databaseShadowing_clearOriginsByPrefix1.js new file mode 100644 index 000000000000..d2a84a245c19 --- /dev/null +++ b/dom/localstorage/test/unit/test_databaseShadowing_clearOriginsByPrefix1.js @@ -0,0 +1,28 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +loadSubscript("databaseShadowing-shared.js"); + +async function testSteps() +{ + enableNextGenLocalStorage(); + + storeData(); + + verifyData([]); + + let principal = getPrincipal("http://prefix.test", {}); + let request = clearOriginsByPrefix(principal, "default"); + await requestFinished(request); + + // Wait for all database connections to close. + request = reset(); + await requestFinished(request); + + exportShadowDatabase("shadowdb-clearedOriginsByPrefix.sqlite"); + + // The shadow database is now prepared for + // test_databaseShadowing_clearOriginsByPrefix2.js +} diff --git a/dom/localstorage/test/unit/test_databaseShadowing_clearOriginsByPrefix2.js b/dom/localstorage/test/unit/test_databaseShadowing_clearOriginsByPrefix2.js new file mode 100644 index 000000000000..8d334c7fa0fb --- /dev/null +++ b/dom/localstorage/test/unit/test_databaseShadowing_clearOriginsByPrefix2.js @@ -0,0 +1,20 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +loadSubscript("databaseShadowing-shared.js"); + +async function testSteps() +{ + // The shadow database was prepared in + // test_databaseShadowing_clearOriginsByPrefix1.js + + disableNextGenLocalStorage(); + + if (!importShadowDatabase("shadowdb-clearedOriginsByPrefix.sqlite")) { + return; + } + + verifyData([2,3]); +} diff --git a/dom/localstorage/test/unit/test_eviction.js b/dom/localstorage/test/unit/test_eviction.js new file mode 100644 index 000000000000..5eec9c130d1d --- /dev/null +++ b/dom/localstorage/test/unit/test_eviction.js @@ -0,0 +1,88 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +async function testSteps() +{ + const globalLimitKB = 5 * 1024; + + const data = {}; + data.sizeKB = 1 * 1024; + data.key = "A"; + data.value = repeatChar(data.sizeKB * 1024 - data.key.length, "."); + data.urlCount = globalLimitKB / data.sizeKB; + + function getSpec(index) { + return "http://example" + index + ".com"; + } + + info("Setting prefs"); + + Services.prefs.setBoolPref("dom.storage.next_gen", true); + Services.prefs.setBoolPref("dom.storage.snapshot_reusing", false); + + info("Setting limits"); + + setGlobalLimit(globalLimitKB); + + let request = clear(); + await requestFinished(request); + + info("Getting storages"); + + let storages = []; + for (let i = 0; i < data.urlCount; i++) { + let storage = getLocalStorage(getPrincipal(getSpec(i))); + storages.push(storage); + } + + info("Filling up entire default storage"); + + for (let i = 0; i < data.urlCount; i++) { + storages[i].setItem(data.key, data.value); + } + + info("Verifying no more data can be written"); + + for (let i = 0; i < data.urlCount; i++) { + try { + storages[i].setItem("B", ""); + ok(false, "Should have thrown"); + } catch(ex) { + ok(true, "Did throw"); + ok(ex instanceof DOMException, "Threw DOMException"); + is(ex.name, "QuotaExceededError", "Threw right DOMException"); + is(ex.code, NS_ERROR_DOM_QUOTA_EXCEEDED_ERR, "Threw with right code"); + } + } + + info("Closing first origin"); + + storages[0].close(); + + let principal = getPrincipal("http://example0.com"); + + request = resetOrigin(principal); + await requestFinished(request); + + info("Getting usage for first origin"); + + request = getOriginUsage(principal); + await requestFinished(request); + + is(request.result.usage, data.sizeKB * 1024, "Correct usage"); + + info("Verifying more data data can be written"); + + for (let i = 1; i < data.urlCount; i++) { + storages[i].setItem("B", ""); + } + + info("Getting usage for first origin"); + + request = getOriginUsage(principal); + await requestFinished(request); + + is(request.result.usage, 0, "Zero usage"); +} diff --git a/dom/localstorage/test/unit/test_groupLimit.js b/dom/localstorage/test/unit/test_groupLimit.js new file mode 100644 index 000000000000..14b8aa5103b6 --- /dev/null +++ b/dom/localstorage/test/unit/test_groupLimit.js @@ -0,0 +1,81 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +async function testSteps() +{ + const groupLimitKB = 10 * 1024; + + const globalLimitKB = groupLimitKB * 5; + + const originLimit = 10 * 1024; + + const urls = [ + "http://example.com", + "http://test1.example.com", + "https://test2.example.com", + "http://test3.example.com:8080" + ]; + + const data = {}; + data.sizeKB = 5 * 1024; + data.key = "A"; + data.value = repeatChar(data.sizeKB * 1024 - data.key.length, "."); + data.urlCount = groupLimitKB / data.sizeKB; + + info("Setting pref"); + + Services.prefs.setBoolPref("dom.storage.snapshot_reusing", false); + + info("Setting limits"); + + setGlobalLimit(globalLimitKB); + + let request = clear(); + await requestFinished(request); + + setOriginLimit(originLimit); + + info("Getting storages"); + + let storages = []; + for (let i = 0; i < urls.length; i++) { + let storage = getLocalStorage(getPrincipal(urls[i])); + storages.push(storage); + } + + info("Filling up the whole group"); + + for (let i = 0; i < data.urlCount; i++) { + storages[i].setItem(data.key, data.value); + } + + info("Verifying no more data can be written"); + + for (let i = 0; i < urls.length; i++) { + try { + storages[i].setItem("B", ""); + ok(false, "Should have thrown"); + } catch(ex) { + ok(true, "Did throw"); + ok(ex instanceof DOMException, "Threw DOMException"); + is(ex.name, "QuotaExceededError", "Threw right DOMException"); + is(ex.code, NS_ERROR_DOM_QUOTA_EXCEEDED_ERR, "Threw with right code"); + } + } + + info("Clearing first origin"); + + storages[0].clear(); + + // Let the internal snapshot finish (usage is not descreased until all + // snapshots finish).. + await returnToEventLoop(); + + info("Verifying more data can be written"); + + for (let i = 0; i < urls.length; i++) { + storages[i].setItem("B", ""); + } +} diff --git a/dom/localstorage/test/unit/test_migration.js b/dom/localstorage/test/unit/test_migration.js new file mode 100644 index 000000000000..03ffaf91554c --- /dev/null +++ b/dom/localstorage/test/unit/test_migration.js @@ -0,0 +1,125 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +async function testSteps() +{ + const principalInfos = [ + { url: "http://localhost", attrs: {} }, + { url: "http://www.mozilla.org", attrs: {} }, + { url: "http://example.com", attrs: {} }, + { url: "http://example.org", attrs: { userContextId: 5 } }, + + { url: "http://origin.test", attrs: {} }, + + { url: "http://prefix.test", attrs: {} }, + { url: "http://prefix.test", attrs: { userContextId: 10 } }, + + { url: "http://pattern.test", attrs: { userContextId: 15 } }, + { url: "http://pattern.test:8080", attrs: { userContextId: 15 } }, + { url: "https://pattern.test", attrs: { userContextId: 15 } }, + ]; + + const data = { + key: "foo", + value: "bar" + }; + + function verifyData(clearedOrigins) { + info("Getting storages"); + + let storages = []; + for (let i = 0; i < principalInfos.length; i++) { + let principalInfo = principalInfos[i]; + let principal = getPrincipal(principalInfo.url, principalInfo.attrs); + let storage = getLocalStorage(principal); + storages.push(storage); + } + + info("Verifying data"); + + for (let i = 0; i < storages.length; i++) { + let value = storages[i].getItem(data.key + i); + if (clearedOrigins.includes(i)) { + is(value, null, "Correct value"); + } else { + is(value, data.value + i, "Correct value"); + } + } + } + + info("Setting pref"); + + Services.prefs.setBoolPref("dom.storage.next_gen", true); + + info("Stage 1 - Testing archived data migration"); + + info("Clearing"); + + let request = clear(); + await requestFinished(request); + + info("Installing package"); + + // The profile contains storage.sqlite and webappsstore.sqlite. The file + // create_db.js in the package was run locally, specifically it was + // temporarily added to xpcshell.ini and then executed: + // mach xpcshell-test --interactive dom/localstorage/test/unit/create_db.js + installPackage("migration_profile"); + + verifyData([]); + + info("Stage 2 - Testing archived data clearing"); + + for (let type of ["origin", "prefix", "pattern"]) { + info("Clearing"); + + request = clear(); + await requestFinished(request); + + info("Installing package"); + + // See the comment for the first installPackage() call. + installPackage("migration_profile"); + + let clearedOrigins = []; + + switch (type) { + case "origin": { + let principal = getPrincipal("http://origin.test", {}); + request = clearOrigin(principal, "default"); + await requestFinished(request); + + clearedOrigins.push(4); + + break; + } + + case "prefix": { + let principal = getPrincipal("http://prefix.test", {}); + request = clearOriginsByPrefix(principal, "default"); + await requestFinished(request); + + clearedOrigins.push(5, 6); + + break; + } + + case "pattern": { + request = clearOriginsByPattern(JSON.stringify({ userContextId: 15 })); + await requestFinished(request); + + clearedOrigins.push(7, 8, 9); + + break; + } + + default: { + throw("Unknown type: " + type); + } + } + + verifyData(clearedOrigins); + } +} diff --git a/dom/localstorage/test/unit/test_snapshotting.js b/dom/localstorage/test/unit/test_snapshotting.js new file mode 100644 index 000000000000..b9c735f35ce7 --- /dev/null +++ b/dom/localstorage/test/unit/test_snapshotting.js @@ -0,0 +1,234 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +async function testSteps() +{ + const url = "http://example.com"; + + const items = [ + { key: "key1", value: "value1" }, + { key: "key2", value: "value2" }, + { key: "key3", value: "value3" }, + { key: "key4", value: "value4" }, + { key: "key5", value: "value5" }, + { key: "key6", value: "value6" }, + { key: "key7", value: "value7" }, + { key: "key8", value: "value8" }, + { key: "key9", value: "value9" }, + { key: "key10", value: "value10" } + ]; + + function getPartialPrefill() + { + let size = 0; + for (let i = 0; i < items.length / 2; i++) { + let item = items[i]; + size += item.key.length + item.value.length; + } + return size; + } + + const prefillValues = [ + 0, // no prefill + getPartialPrefill(), // partial prefill + -1, // full prefill + ]; + + info("Setting pref"); + + Services.prefs.setBoolPref("dom.storage.snapshot_reusing", false); + + for (let prefillValue of prefillValues) { + info("Setting prefill value"); + + Services.prefs.setIntPref("dom.storage.snapshot_prefill", prefillValue); + + info("Getting storage"); + + let storage = getLocalStorage(getPrincipal(url)); + + // 1st snapshot + + info("Adding data"); + + for (let item of items) { + storage.setItem(item.key, item.value); + } + + info("Saving key order"); + + // This forces GetKeys to be called internally. + let savedKeys = Object.keys(storage); + + // GetKey should match GetKeys + for (let i = 0; i < savedKeys.length; i++) { + is(storage.key(i), savedKeys[i], "Correct key"); + } + + info("Returning to event loop"); + + // Returning to event loop forces the internal snapshot to finish. + await returnToEventLoop(); + + // 2nd snapshot + + info("Verifying length"); + + is(storage.length, items.length, "Correct length"); + + info("Verifying key order"); + + let keys = Object.keys(storage); + + is(keys.length, savedKeys.length); + + for (let i = 0; i < keys.length; i++) { + is(keys[i], savedKeys[i], "Correct key"); + } + + info("Verifying values"); + + for (let item of items) { + is(storage.getItem(item.key), item.value, "Correct value"); + } + + info("Returning to event loop"); + + await returnToEventLoop(); + + // 3rd snapshot + + // Force key2 to load. + storage.getItem("key2"); + + // Fill out write infos a bit. + storage.removeItem("key5"); + storage.setItem("key5", "value5"); + storage.removeItem("key5"); + storage.setItem("key11", "value11"); + storage.setItem("key5", "value5"); + + items.push({ key: "key11", value: "value11" }); + + info("Verifying length"); + + is(storage.length, items.length, "Correct length"); + + // This forces to get all keys from the parent and then apply write infos + // on already cached values. + savedKeys = Object.keys(storage); + + info("Verifying values"); + + for (let item of items) { + is(storage.getItem(item.key), item.value, "Correct value"); + } + + storage.removeItem("key11"); + + items.pop(); + + info("Returning to event loop"); + + await returnToEventLoop(); + + // 4th snapshot + + // Force loading of all items. + info("Verifying length"); + + is(storage.length, items.length, "Correct length"); + + info("Verifying values"); + + for (let item of items) { + is(storage.getItem(item.key), item.value, "Correct value"); + } + + is(storage.getItem("key11"), null, "Correct value"); + + info("Returning to event loop"); + + await returnToEventLoop(); + + // 5th snapshot + + // Force loading of all keys. + info("Saving key order"); + + savedKeys = Object.keys(storage); + + // Force loading of all items. + info("Verifying length"); + + is(storage.length, items.length, "Correct length"); + + info("Verifying values"); + + for (let item of items) { + is(storage.getItem(item.key), item.value, "Correct value"); + } + + is(storage.getItem("key11"), null, "Correct value"); + + info("Returning to event loop"); + + await returnToEventLoop(); + + // 6th snapshot + info("Verifying unknown item"); + + is(storage.getItem("key11"), null, "Correct value"); + + info("Verifying unknown item again"); + + is(storage.getItem("key11"), null, "Correct value"); + + info("Returning to event loop"); + + await returnToEventLoop(); + + // 7th snapshot + + // Save actual key order. + info("Saving key order"); + + savedKeys = Object.keys(storage); + + await returnToEventLoop(); + + // 8th snapshot + + // Force loading of all items, but in reverse order. + info("Getting values"); + + for (let i = items.length - 1; i >= 0; i--) { + let item = items[i]; + storage.getItem(item.key); + } + + info("Verifying key order"); + + keys = Object.keys(storage); + + is(keys.length, savedKeys.length); + + for (let i = 0; i < keys.length; i++) { + is(keys[i], savedKeys[i], "Correct key"); + } + + await returnToEventLoop(); + + // 9th snapshot + + info("Clearing"); + + storage.clear(); + + info("Returning to event loop"); + + await returnToEventLoop(); + } +} diff --git a/dom/localstorage/test/unit/xpcshell.ini b/dom/localstorage/test/unit/xpcshell.ini new file mode 100644 index 000000000000..0ba82e9019cd --- /dev/null +++ b/dom/localstorage/test/unit/xpcshell.ini @@ -0,0 +1,31 @@ +# 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/. + +[DEFAULT] +head = head.js +support-files = + archive_profile.zip + migration_profile.zip + +[test_archive.js] +[test_databaseShadowing1.js] +run-sequentially = test_databaseShadowing2.js depends on a file produced by this test +[test_databaseShadowing2.js] +run-sequentially = this test depends on a file produced by test_databaseShadowing1.js +[test_databaseShadowing_clearOrigin1.js] +run-sequentially = test_databaseShadowing_clearOrigin2.js depends on a file produced by this test +[test_databaseShadowing_clearOrigin2.js] +run-sequentially = this test depends on a file produced by test_databaseShadowing_clearOrigin1.js +[test_databaseShadowing_clearOriginsByPattern1.js] +run-sequentially = test_databaseShadowing_clearOriginsByPattern2.js depends on a file produced by this test +[test_databaseShadowing_clearOriginsByPattern2.js] +run-sequentially = this test depends on a file produced by test_databaseShadowing_clearOriginsByPattern1.js +[test_databaseShadowing_clearOriginsByPrefix1.js] +run-sequentially = test_databaseShadowing_clearOriginsByPrefix2.js depends on a file produced by this test +[test_databaseShadowing_clearOriginsByPrefix2.js] +run-sequentially = this test depends on a file produced by test_databaseShadowing_clearOriginsByPrefix1.js +[test_eviction.js] +[test_groupLimit.js] +[test_migration.js] +[test_snapshotting.js] diff --git a/dom/moz.build b/dom/moz.build index 28b85538deb7..7c2240efcfe2 100644 --- a/dom/moz.build +++ b/dom/moz.build @@ -103,6 +103,7 @@ DIRS += [ 'serviceworkers', 'simpledb', 'reporting', + 'localstorage', ] if CONFIG['MOZ_LIBPRIO']: diff --git a/dom/quota/ActorsChild.cpp b/dom/quota/ActorsChild.cpp index 3f7911635685..ca09da17d7b2 100644 --- a/dom/quota/ActorsChild.cpp +++ b/dom/quota/ActorsChild.cpp @@ -318,6 +318,7 @@ QuotaRequestChild::Recv__delete__(const RequestResponse& aResponse) case RequestResponse::TInitResponse: case RequestResponse::TInitTemporaryStorageResponse: case RequestResponse::TClearOriginResponse: + case RequestResponse::TResetOriginResponse: case RequestResponse::TClearDataResponse: case RequestResponse::TClearAllResponse: case RequestResponse::TResetAllResponse: diff --git a/dom/quota/ActorsParent.cpp b/dom/quota/ActorsParent.cpp index 7a9df9c283f9..782f10c1b7a8 100644 --- a/dom/quota/ActorsParent.cpp +++ b/dom/quota/ActorsParent.cpp @@ -34,11 +34,13 @@ #include "mozilla/dom/asmjscache/AsmJSCache.h" #include "mozilla/dom/cache/QuotaClient.h" #include "mozilla/dom/indexedDB/ActorsParent.h" +#include "mozilla/dom/localstorage/ActorsParent.h" #include "mozilla/dom/quota/PQuotaParent.h" #include "mozilla/dom/quota/PQuotaRequestParent.h" #include "mozilla/dom/quota/PQuotaUsageRequestParent.h" #include "mozilla/dom/simpledb/ActorsParent.h" #include "mozilla/dom/StorageActivityService.h" +#include "mozilla/dom/StorageDBUpdater.h" #include "mozilla/ipc/BackgroundParent.h" #include "mozilla/ipc/BackgroundUtils.h" #include "mozilla/IntegerRange.h" @@ -210,6 +212,7 @@ enum AppId { #define METADATA_V2_FILE_NAME ".metadata-v2" #define METADATA_V2_TMP_FILE_NAME ".metadata-v2-tmp" +#define WEB_APPS_STORE_FILE_NAME "webappsstore.sqlite" #define LS_ARCHIVE_FILE_NAME "ls-archive.sqlite" #define LS_ARCHIVE_TMP_FILE_NAME "ls-archive-tmp.sqlite" @@ -263,6 +266,65 @@ CreateTables(mozIStorageConnection* aConnection) return NS_OK; } +nsresult +CreateWebAppsStoreConnection(nsIFile* aWebAppsStoreFile, + mozIStorageService* aStorageService, + mozIStorageConnection** aConnection) +{ + AssertIsOnIOThread(); + MOZ_ASSERT(aWebAppsStoreFile); + MOZ_ASSERT(aStorageService); + MOZ_ASSERT(aConnection); + + // Check if the old database exists at all. + bool exists; + nsresult rv = aWebAppsStoreFile->Exists(&exists); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (!exists) { + // webappsstore.sqlite doesn't exist, return a null connection. + *aConnection = nullptr; + return NS_OK; + } + + bool isDirectory; + rv = aWebAppsStoreFile->IsDirectory(&isDirectory); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (isDirectory) { + QM_WARNING("webappsstore.sqlite is not a file!"); + *aConnection = nullptr; + return NS_OK; + } + + nsCOMPtr connection; + rv = aStorageService->OpenUnsharedDatabase(aWebAppsStoreFile, + getter_AddRefs(connection)); + if (rv == NS_ERROR_FILE_CORRUPTED) { + // Don't throw an error, leave a corrupted webappsstore database as it is. + *aConnection = nullptr; + return NS_OK; + } + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = StorageDBUpdater::Update(connection); + if (NS_FAILED(rv)) { + // Don't throw an error, leave a non-updateable webappsstore database as + // it is. + *aConnection = nullptr; + return NS_OK; + } + + connection.forget(aConnection); + return NS_OK; +} + /****************************************************************************** * Quota manager class declarations ******************************************************************************/ @@ -404,6 +466,7 @@ class QuotaManager::CreateRunnable final : public BackgroundThreadObject , public Runnable { + nsCOMPtr mMainEventTarget; nsTArray> mCallbacks; nsString mBaseDirPath; RefPtr mManager; @@ -421,8 +484,9 @@ class QuotaManager::CreateRunnable final State mState; public: - CreateRunnable() + explicit CreateRunnable(nsIEventTarget* aMainEventTarget) : Runnable("dom::quota::QuotaManager::CreateRunnable") + , mMainEventTarget(aMainEventTarget) , mResultCode(NS_OK) , mState(State::Initial) { @@ -950,6 +1014,7 @@ class NormalOriginOperationBase protected: Nullable mPersistenceType; OriginScope mOriginScope; + Nullable mClientType; mozilla::Atomic mCanceled; const bool mExclusive; @@ -1310,8 +1375,13 @@ class ClearRequestBase : public QuotaRequestBase { protected: - explicit ClearRequestBase(bool aExclusive) + const bool mClear; + +protected: + ClearRequestBase(bool aExclusive, + bool aClear) : QuotaRequestBase(aExclusive) + , mClear(aClear) { AssertIsOnOwningThread(); } @@ -1327,7 +1397,7 @@ protected: class ClearOriginOp final : public ClearRequestBase { - const ClearOriginParams mParams; + const ClearResetOriginParams mParams; public: explicit ClearOriginOp(const RequestParams& aParams); @@ -1738,7 +1808,7 @@ private: eExpectingScheme, eExpectingEmptyToken1, eExpectingEmptyToken2, - eExpectingEmptyToken3, + eExpectingEmptyTokenOrUniversalFileOrigin, eExpectingHost, eExpectingPort, eExpectingEmptyTokenOrDriveLetterOrPathnameComponent, @@ -1761,6 +1831,7 @@ private: SchemeType mSchemeType; State mState; bool mInIsolatedMozBrowser; + bool mUniversalFileOrigin; bool mMaybeDriveLetter; bool mError; @@ -1775,6 +1846,7 @@ public: , mSchemeType(eNone) , mState(eExpectingAppIdOrScheme) , mInIsolatedMozBrowser(false) + , mUniversalFileOrigin(false) , mMaybeDriveLetter(false) , mError(false) { } @@ -2561,7 +2633,7 @@ DirectoryLockImpl::DirectoryLockImpl(QuotaManager* aQuotaManager, MOZ_ASSERT_IF(!aInternal, !aGroup.IsEmpty()); MOZ_ASSERT_IF(!aInternal, aOriginScope.IsOrigin()); MOZ_ASSERT_IF(!aInternal, !aClientType.IsNull()); - MOZ_ASSERT_IF(!aInternal, aClientType.Value() != Client::TYPE_MAX); + MOZ_ASSERT_IF(!aInternal, aClientType.Value() < Client::TypeMax()); MOZ_ASSERT_IF(!aInternal, aOpenListener); } @@ -2666,6 +2738,8 @@ CreateRunnable::Init() return rv; } + Unused << NextGenLocalStorageEnabled(); + return NS_OK; } @@ -2777,7 +2851,11 @@ CreateRunnable::GetNextState(nsCOMPtr& aThread) -> State aThread = mOwningThread; return State::CreatingManager; case State::CreatingManager: - aThread = GetMainThreadEventTarget(); + if (mMainEventTarget) { + aThread = mMainEventTarget; + } else { + aThread = GetMainThreadEventTarget(); + } return State::RegisteringObserver; case State::RegisteringObserver: aThread = mOwningThread; @@ -3099,9 +3177,16 @@ QuotaObject::LockedMaybeUpdateSize(int64_t aSize, bool aTruncate) // This will block the thread without holding the lock while waitting. AutoTArray, 10> locks; + uint64_t sizeToBeFreed; - uint64_t sizeToBeFreed = - quotaManager->LockedCollectOriginsForEviction(delta, locks); + if (IsOnBackgroundThread()) { + MutexAutoUnlock autoUnlock(quotaManager->mQuotaMutex); + + sizeToBeFreed = quotaManager->CollectOriginsForEviction(delta, locks); + } else { + sizeToBeFreed = quotaManager->LockedCollectOriginsForEviction(delta, + locks); + } if (!sizeToBeFreed) { uint64_t usage = quotaManager->mTemporaryStorageUsage; @@ -3242,7 +3327,8 @@ QuotaManager::~QuotaManager() } void -QuotaManager::GetOrCreate(nsIRunnable* aCallback) +QuotaManager::GetOrCreate(nsIRunnable* aCallback, + nsIEventTarget* aMainEventTarget) { AssertIsOnBackgroundThread(); @@ -3258,8 +3344,14 @@ QuotaManager::GetOrCreate(nsIRunnable* aCallback) MOZ_ALWAYS_SUCCEEDS(NS_DispatchToCurrentThread(aCallback)); } else { if (!gCreateRunnable) { - gCreateRunnable = new CreateRunnable(); - MOZ_ALWAYS_SUCCEEDS(NS_DispatchToMainThread(gCreateRunnable)); + gCreateRunnable = new CreateRunnable(aMainEventTarget); + if (aMainEventTarget) { + MOZ_ALWAYS_SUCCEEDS(aMainEventTarget->Dispatch(gCreateRunnable, + NS_DISPATCH_NORMAL)); + } else { + MOZ_ALWAYS_SUCCEEDS(NS_DispatchToMainThread(gCreateRunnable)); + } + } gCreateRunnable->AddCallback(aCallback); @@ -3299,7 +3391,7 @@ QuotaManager::CreateDirectoryLock(const Nullable& aPersistenceT MOZ_ASSERT_IF(!aInternal, !aGroup.IsEmpty()); MOZ_ASSERT_IF(!aInternal, aOriginScope.IsOrigin()); MOZ_ASSERT_IF(!aInternal, !aClientType.IsNull()); - MOZ_ASSERT_IF(!aInternal, aClientType.Value() != Client::TYPE_MAX); + MOZ_ASSERT_IF(!aInternal, aClientType.Value() < Client::TypeMax()); MOZ_ASSERT_IF(!aInternal, aOpenListener); RefPtr lock = new DirectoryLockImpl(this, @@ -3647,7 +3739,8 @@ QuotaManager::Init(const nsAString& aBasePath) Client::ASMJS == 1 && Client::DOMCACHE == 2 && Client::SDB == 3 && - Client::TYPE_MAX == 4, + Client::LS == 4 && + Client::TYPE_MAX == 5, "Fix the registration!"); MOZ_ASSERT(mClients.Capacity() == Client::TYPE_MAX, @@ -3658,6 +3751,11 @@ QuotaManager::Init(const nsAString& aBasePath) mClients.AppendElement(asmjscache::CreateClient()); mClients.AppendElement(cache::CreateQuotaClient()); mClients.AppendElement(simpledb::CreateQuotaClient()); + if (CachedNextGenLocalStorageEnabled()) { + mClients.AppendElement(localstorage::CreateQuotaClient()); + } else { + mClients.SetLength(Client::TypeMax()); + } return NS_OK; } @@ -3685,7 +3783,7 @@ QuotaManager::Shutdown() // Each client will spin the event loop while we wait on all the threads // to close. Our timer may fire during that loop. - for (uint32_t index = 0; index < Client::TYPE_MAX; index++) { + for (uint32_t index = 0; index < uint32_t(Client::TypeMax()); index++) { mClients[index]->ShutdownWorkThreads(); } @@ -3846,6 +3944,7 @@ QuotaManager::GetQuotaObject(PersistenceType aPersistenceType, const nsACString& aGroup, const nsACString& aOrigin, nsIFile* aFile, + int64_t aFileSize, int64_t* aFileSizeOut /* = nullptr */) { NS_ASSERTION(!NS_IsMainThread(), "Wrong thread!"); @@ -3864,16 +3963,20 @@ QuotaManager::GetQuotaObject(PersistenceType aPersistenceType, int64_t fileSize; - bool exists; - rv = aFile->Exists(&exists); - NS_ENSURE_SUCCESS(rv, nullptr); - - if (exists) { - rv = aFile->GetFileSize(&fileSize); + if (aFileSize == -1) { + bool exists; + rv = aFile->Exists(&exists); NS_ENSURE_SUCCESS(rv, nullptr); - } - else { - fileSize = 0; + + if (exists) { + rv = aFile->GetFileSize(&fileSize); + NS_ENSURE_SUCCESS(rv, nullptr); + } + else { + fileSize = 0; + } + } else { + fileSize = aFileSize; } // Re-escape our parameters above to make sure we get the right quota group. @@ -3939,6 +4042,7 @@ QuotaManager::GetQuotaObject(PersistenceType aPersistenceType, const nsACString& aGroup, const nsACString& aOrigin, const nsAString& aPath, + int64_t aFileSize, int64_t* aFileSizeOut /* = nullptr */) { NS_ASSERTION(!NS_IsMainThread(), "Wrong thread!"); @@ -3951,7 +4055,12 @@ QuotaManager::GetQuotaObject(PersistenceType aPersistenceType, nsresult rv = NS_NewLocalFile(aPath, false, getter_AddRefs(file)); NS_ENSURE_SUCCESS(rv, nullptr); - return GetQuotaObject(aPersistenceType, aGroup, aOrigin, file, aFileSizeOut); + return GetQuotaObject(aPersistenceType, + aGroup, + aOrigin, + file, + aFileSize, + aFileSizeOut); } Nullable @@ -4789,6 +4898,7 @@ nsresult QuotaManager::MaybeRemoveLocalStorageData() { AssertIsOnIOThread(); + MOZ_ASSERT(!CachedNextGenLocalStorageEnabled()); // Cleanup the tmp file first, if there's any. nsCOMPtr lsArchiveTmpFile; @@ -4985,6 +5095,202 @@ QuotaManager::MaybeRemoveLocalStorageDirectories() return NS_OK; } +nsresult +QuotaManager::MaybeCreateLocalStorageArchive() +{ + AssertIsOnIOThread(); + MOZ_ASSERT(CachedNextGenLocalStorageEnabled()); + + // Check if the archive was already successfully created. + nsCOMPtr lsArchiveFile; + nsresult rv = NS_NewLocalFile(mStoragePath, + false, + getter_AddRefs(lsArchiveFile)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = lsArchiveFile->Append(NS_LITERAL_STRING(LS_ARCHIVE_FILE_NAME)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + bool exists; + rv = lsArchiveFile->Exists(&exists); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (exists) { + // ls-archive.sqlite already exists, nothing to create. + return NS_OK; + } + + // Get the storage service first, we will need it at multiple places. + nsCOMPtr ss = + do_GetService(MOZ_STORAGE_SERVICE_CONTRACTID, &rv); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + // Get the web apps store file. + nsCOMPtr webAppsStoreFile; + rv = NS_NewLocalFile(mBasePath, false, getter_AddRefs(webAppsStoreFile)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = webAppsStoreFile->Append(NS_LITERAL_STRING(WEB_APPS_STORE_FILE_NAME)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + // Now check if the web apps store is useable. + nsCOMPtr connection; + rv = CreateWebAppsStoreConnection(webAppsStoreFile, + ss, + getter_AddRefs(connection)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (connection) { + // Find out the journal mode. + nsCOMPtr stmt; + rv = connection->CreateStatement(NS_LITERAL_CSTRING( + "PRAGMA journal_mode;" + ), getter_AddRefs(stmt)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + bool hasResult; + rv = stmt->ExecuteStep(&hasResult); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + MOZ_ASSERT(hasResult); + + nsCString journalMode; + rv = stmt->GetUTF8String(0, journalMode); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = stmt->Finalize(); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (journalMode.EqualsLiteral("wal")) { + // We don't copy the WAL file, so make sure the old database is fully + // checkpointed. + rv = connection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "PRAGMA wal_checkpoint(TRUNCATE);" + )); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } + + // Explicitely close the connection before the old database is copied. + rv = connection->Close(); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + // Copy the old database. The database is copied from + // /webappsstore.sqlite to + // /storage/ls-archive-tmp.sqlite + // We use a "-tmp" postfix since we are not done yet. + nsCOMPtr storageDir; + rv = NS_NewLocalFile(mStoragePath, false, getter_AddRefs(storageDir)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = webAppsStoreFile->CopyTo(storageDir, + NS_LITERAL_STRING(LS_ARCHIVE_TMP_FILE_NAME)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + nsCOMPtr lsArchiveTmpFile; + rv = NS_NewLocalFile(mStoragePath, false, getter_AddRefs(lsArchiveTmpFile)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = lsArchiveTmpFile->Append(NS_LITERAL_STRING(LS_ARCHIVE_TMP_FILE_NAME)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (journalMode.EqualsLiteral("wal")) { + nsCOMPtr lsArchiveTmpConnection; + rv = ss->OpenUnsharedDatabase(lsArchiveTmpFile, + getter_AddRefs(lsArchiveTmpConnection)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + // The archive will only be used for lazy data migration. There won't be + // any concurrent readers and writers that could benefit from Write-Ahead + // Logging. So switch to a standard rollback journal. The standard + // rollback journal also provides atomicity across multiple attached + // databases which is import for the lazy data migration to work safely. + rv = lsArchiveTmpConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "PRAGMA journal_mode = DELETE;" + )); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + // The connection will be now implicitely closed (it's always safer to + // close database connection before we manipulate underlying file) + } + + // Finally, rename ls-archive-tmp.sqlite to ls-archive.sqlite + rv = lsArchiveTmpFile->MoveTo(nullptr, + NS_LITERAL_STRING(LS_ARCHIVE_FILE_NAME)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } else { + // If webappsstore database is not useable, just create an empty archive. + + // Ensure the storage directory actually exists. + nsCOMPtr storageDirectory; + rv = NS_NewLocalFile(GetStoragePath(), + false, + getter_AddRefs(storageDirectory)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + bool dummy; + rv = EnsureDirectory(storageDirectory, &dummy); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + nsCOMPtr lsArchiveConnection; + rv = ss->OpenUnsharedDatabase(lsArchiveFile, + getter_AddRefs(lsArchiveConnection)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = StorageDBUpdater::Update(lsArchiveConnection); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } + + return NS_OK; +} + #ifdef DEBUG void @@ -5164,7 +5470,11 @@ QuotaManager::EnsureStorageIsInitialized() } } - rv = MaybeRemoveLocalStorageData(); + if (CachedNextGenLocalStorageEnabled()) { + rv = MaybeCreateLocalStorageArchive(); + } else { + rv = MaybeRemoveLocalStorageData(); + } if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } @@ -5222,18 +5532,18 @@ QuotaManager::OpenDirectoryInternal(const Nullable& aPersistenc // We also need to notify clients to abort operations for them. AutoTArray>, Client::TYPE_MAX> origins; - origins.SetLength(Client::TYPE_MAX); + origins.SetLength(Client::TypeMax()); const nsTArray& blockedOnLocks = lock->GetBlockedOnLocks(); for (DirectoryLockImpl* blockedOnLock : blockedOnLocks) { - blockedOnLock->Invalidate(); - if (!blockedOnLock->IsInternal()) { + blockedOnLock->Invalidate(); + MOZ_ASSERT(!blockedOnLock->GetClientType().IsNull()); Client::Type clientType = blockedOnLock->GetClientType().Value(); - MOZ_ASSERT(clientType < Client::TYPE_MAX); + MOZ_ASSERT(clientType < Client::TypeMax()); const OriginScope& originScope = blockedOnLock->GetOriginScope(); MOZ_ASSERT(originScope.IsOrigin()); @@ -5247,7 +5557,7 @@ QuotaManager::OpenDirectoryInternal(const Nullable& aPersistenc } } - for (uint32_t index : IntegerRange(uint32_t(Client::TYPE_MAX))) { + for (uint32_t index : IntegerRange(uint32_t(Client::TypeMax()))) { if (origins[index]) { for (auto iter = origins[index]->Iter(); !iter.Done(); iter.Next()) { MOZ_ASSERT(mClients[index]); @@ -5263,6 +5573,7 @@ QuotaManager::EnsureOriginIsInitialized(PersistenceType aPersistenceType, const nsACString& aSuffix, const nsACString& aGroup, const nsACString& aOrigin, + bool aCreateIfNotExists, nsIFile** aDirectory) { AssertIsOnIOThread(); @@ -5274,8 +5585,12 @@ QuotaManager::EnsureOriginIsInitialized(PersistenceType aPersistenceType, aSuffix, aGroup, aOrigin, + aCreateIfNotExists, getter_AddRefs(directory), &created); + if (rv == NS_ERROR_NOT_AVAILABLE) { + return rv; + } if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } @@ -5290,6 +5605,7 @@ QuotaManager::EnsureOriginIsInitializedInternal( const nsACString& aSuffix, const nsACString& aGroup, const nsACString& aOrigin, + bool aCreateIfNotExists, nsIFile** aDirectory, bool* aCreated) { @@ -5320,7 +5636,10 @@ QuotaManager::EnsureOriginIsInitializedInternal( } bool created; - rv = EnsureOriginDirectory(directory, &created); + rv = EnsureOriginDirectory(directory, aCreateIfNotExists, &created); + if (rv == NS_ERROR_NOT_AVAILABLE) { + return rv; + } if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } @@ -5441,6 +5760,7 @@ QuotaManager::EnsureTemporaryStorageIsInitialized() nsresult QuotaManager::EnsureOriginDirectory(nsIFile* aDirectory, + bool aCreateIfNotExists, bool* aCreated) { AssertIsOnIOThread(); @@ -5454,6 +5774,10 @@ QuotaManager::EnsureOriginDirectory(nsIFile* aDirectory, } if (!exists) { + if (!aCreateIfNotExists) { + return NS_ERROR_NOT_AVAILABLE; + } + nsString leafName; rv = aDirectory->GetLeafName(leafName); if (NS_WARN_IF(NS_FAILED(rv))) { @@ -5476,18 +5800,53 @@ QuotaManager::EnsureOriginDirectory(nsIFile* aDirectory, return NS_OK; } -void -QuotaManager::OriginClearCompleted(PersistenceType aPersistenceType, - const nsACString& aOrigin) +nsresult +QuotaManager::AboutToClearOrigins( + const Nullable& aPersistenceType, + const OriginScope& aOriginScope, + const Nullable& aClientType) { AssertIsOnIOThread(); - if (aPersistenceType == PERSISTENCE_TYPE_PERSISTENT) { - mInitializedOrigins.RemoveElement(aOrigin); + nsresult rv; + + if (aClientType.IsNull()) { + for (uint32_t index = 0; index < uint32_t(Client::TypeMax()); index++) { + rv = mClients[index]->AboutToClearOrigins(aPersistenceType, + aOriginScope); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } + } else { + rv = mClients[aClientType.Value()]->AboutToClearOrigins(aPersistenceType, + aOriginScope); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } } - for (uint32_t index = 0; index < Client::TYPE_MAX; index++) { - mClients[index]->OnOriginClearCompleted(aPersistenceType, aOrigin); + return NS_OK; +} + +void +QuotaManager::OriginClearCompleted(PersistenceType aPersistenceType, + const nsACString& aOrigin, + const Nullable& aClientType) +{ + AssertIsOnIOThread(); + + if (aClientType.IsNull()) { + if (aPersistenceType == PERSISTENCE_TYPE_PERSISTENT) { + mInitializedOrigins.RemoveElement(aOrigin); + } + + for (uint32_t index = 0; index < uint32_t(Client::TypeMax()); index++) { + mClients[index]->OnOriginClearCompleted(aPersistenceType, aOrigin); + } + } else { + mClients[aClientType.Value()]->OnOriginClearCompleted(aPersistenceType, + aOrigin); } } @@ -5507,7 +5866,7 @@ Client* QuotaManager::GetClient(Client::Type aClientType) { MOZ_ASSERT(aClientType >= Client::IDB); - MOZ_ASSERT(aClientType < Client::TYPE_MAX); + MOZ_ASSERT(aClientType < Client::TypeMax()); return mClients.ElementAt(aClientType); } @@ -5743,6 +6102,31 @@ QuotaManager::AreOriginsEqualOnDisk(nsACString& aOrigin1, return origin1Sanitized == origin2Sanitized; } +// static +bool +QuotaManager::ParseOrigin(const nsACString& aOrigin, + nsCString& aSpec, + OriginAttributes* aAttrs) +{ + MOZ_ASSERT(aAttrs); + + if (aOrigin.Equals(kChromeOrigin)) { + aSpec = kChromeOrigin; + return true; + } + + nsCString sanitizedOrigin(aOrigin); + SanitizeOriginString(sanitizedOrigin); + + OriginParser::ResultType result = + OriginParser::ParseOrigin(sanitizedOrigin, aSpec, aAttrs); + if (NS_WARN_IF(result != OriginParser::ValidOrigin)) { + return false; + } + + return true; +} + uint64_t QuotaManager::LockedCollectOriginsForEviction( uint64_t aMinSizeToBeFreed, @@ -5959,7 +6343,8 @@ QuotaManager::CheckTemporaryStorageLimits() for (const OriginParams& doomedOrigin : doomedOrigins) { OriginClearCompleted(doomedOrigin.mPersistenceType, - doomedOrigin.mOrigin); + doomedOrigin.mOrigin, + Nullable()); } if (mTemporaryStorageUsage > mTemporaryStorageLimit) { @@ -6499,7 +6884,8 @@ FinalizeOriginEvictionOp::DoDirectoryWork(QuotaManager* aQuotaManager) for (RefPtr& lock : mLocks) { aQuotaManager->OriginClearCompleted(lock->GetPersistenceType().Value(), - lock->GetOriginScope().GetOrigin()); + lock->GetOriginScope().GetOrigin(), + Nullable()); } return NS_OK; @@ -6533,7 +6919,7 @@ NormalOriginOperationBase::Open() QuotaManager::Get()->OpenDirectoryInternal(mPersistenceType, mOriginScope, - Nullable(), + mClientType, mExclusive, this); } @@ -6778,6 +7164,7 @@ Quota::AllocPQuotaRequestParent(const RequestParams& aParams) break; case RequestParams::TClearOriginParams: + case RequestParams::TResetOriginParams: actor = new ClearOriginOp(aParams); break; @@ -7508,12 +7895,14 @@ InitOriginOp::DoDirectoryWork(QuotaManager* aQuotaManager) nsCOMPtr directory; bool created; nsresult rv = - aQuotaManager->EnsureOriginIsInitializedInternal(mPersistenceType.Value(), - mSuffix, - mGroup, - mOriginScope.GetOrigin(), - getter_AddRefs(directory), - &created); + aQuotaManager->EnsureOriginIsInitializedInternal( + mPersistenceType.Value(), + mSuffix, + mGroup, + mOriginScope.GetOrigin(), + /* aCreateIfNotExists */ true, + getter_AddRefs(directory), + &created); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } @@ -7541,10 +7930,18 @@ ResetOrClearOp::DeleteFiles(QuotaManager* aQuotaManager) AssertIsOnIOThread(); MOZ_ASSERT(aQuotaManager); + nsresult rv = + aQuotaManager->AboutToClearOrigins(Nullable(), + OriginScope::FromNull(), + Nullable()); + if (NS_WARN_IF(NS_FAILED(rv))) { + return; + } nsCOMPtr directory; - nsresult rv = NS_NewLocalFile(aQuotaManager->GetStoragePath(), false, - getter_AddRefs(directory)); + rv = NS_NewLocalFile(aQuotaManager->GetStoragePath(), + false, + getter_AddRefs(directory)); if (NS_WARN_IF(NS_FAILED(rv))) { return; } @@ -7614,10 +8011,19 @@ ClearRequestBase::DeleteFiles(QuotaManager* aQuotaManager, AssertIsOnIOThread(); MOZ_ASSERT(aQuotaManager); + nsresult rv = + aQuotaManager->AboutToClearOrigins( + Nullable(aPersistenceType), + mOriginScope, + mClientType); + if (NS_WARN_IF(NS_FAILED(rv))) { + return; + } nsCOMPtr directory; - nsresult rv = NS_NewLocalFile(aQuotaManager->GetStoragePath(aPersistenceType), - false, getter_AddRefs(directory)); + rv = NS_NewLocalFile(aQuotaManager->GetStoragePath(aPersistenceType), + false, + getter_AddRefs(directory)); if (NS_WARN_IF(NS_FAILED(rv))) { return; } @@ -7685,6 +8091,62 @@ ClearRequestBase::DeleteFiles(QuotaManager* aQuotaManager, return; } + UsageInfo usageInfo; + + if (!mClientType.IsNull()) { + Client::Type clientType = mClientType.Value(); + + nsAutoString clientDirectoryName; + rv = Client::TypeToText(clientType, clientDirectoryName); + if (NS_WARN_IF(NS_FAILED(rv))) { + return; + } + + rv = file->Append(clientDirectoryName); + if (NS_WARN_IF(NS_FAILED(rv))) { + return; + } + + bool exists; + rv = file->Exists(&exists); + if (NS_WARN_IF(NS_FAILED(rv))) { + return; + } + + if (!exists) { + continue; + } + + bool initialized; + if (aPersistenceType == PERSISTENCE_TYPE_PERSISTENT) { + initialized = aQuotaManager->IsOriginInitialized(origin); + } else { + initialized = aQuotaManager->IsTemporaryStorageInitialized(); + } + + Client* client = aQuotaManager->GetClient(clientType); + MOZ_ASSERT(client); + + Atomic dummy(false); + if (initialized) { + rv = client->GetUsageForOrigin(aPersistenceType, + group, + origin, + dummy, + &usageInfo); + } else { + rv = client->InitOrigin(aPersistenceType, + group, + origin, + dummy, + &usageInfo); + + } + if (NS_WARN_IF(NS_FAILED(rv))) { + return; + } + } + for (uint32_t index = 0; index < 10; index++) { // We can't guarantee that this will always succeed on Windows... if (NS_SUCCEEDED((rv = file->Remove(true)))) { @@ -7701,12 +8163,18 @@ ClearRequestBase::DeleteFiles(QuotaManager* aQuotaManager, } if (aPersistenceType != PERSISTENCE_TYPE_PERSISTENT) { - aQuotaManager->RemoveQuotaForOrigin(aPersistenceType, group, origin); + if (mClientType.IsNull()) { + aQuotaManager->RemoveQuotaForOrigin(aPersistenceType, group, origin); + } else { + aQuotaManager->DecreaseUsageForOrigin(aPersistenceType, + group, + origin, + usageInfo.TotalUsage()); + } } - aQuotaManager->OriginClearCompleted(aPersistenceType, origin); + aQuotaManager->OriginClearCompleted(aPersistenceType, origin, mClientType); } - } nsresult @@ -7716,22 +8184,29 @@ ClearRequestBase::DoDirectoryWork(QuotaManager* aQuotaManager) AUTO_PROFILER_LABEL("ClearRequestBase::DoDirectoryWork", OTHER); - if (mPersistenceType.IsNull()) { - for (const PersistenceType type : kAllPersistenceTypes) { - DeleteFiles(aQuotaManager, type); + if (mClear) { + if (mPersistenceType.IsNull()) { + for (const PersistenceType type : kAllPersistenceTypes) { + DeleteFiles(aQuotaManager, type); + } + } else { + DeleteFiles(aQuotaManager, mPersistenceType.Value()); } - } else { - DeleteFiles(aQuotaManager, mPersistenceType.Value()); } return NS_OK; } ClearOriginOp::ClearOriginOp(const RequestParams& aParams) - : ClearRequestBase(/* aExclusive */ true) - , mParams(aParams) + : ClearRequestBase(/* aExclusive */ true, + aParams.type() == RequestParams::TClearOriginParams) + , mParams(aParams.type() == RequestParams::TClearOriginParams ? + aParams.get_ClearOriginParams().commonParams() : + aParams.get_ResetOriginParams().commonParams()) + { - MOZ_ASSERT(aParams.type() == RequestParams::TClearOriginParams); + MOZ_ASSERT(aParams.type() == RequestParams::TClearOriginParams || + aParams.type() == RequestParams::TResetOriginParams); } bool @@ -7750,6 +8225,12 @@ ClearOriginOp::Init(Quota* aQuota) mPersistenceType.SetValue(mParams.persistenceType()); } + if (mParams.clientTypeIsExplicit()) { + MOZ_ASSERT(mParams.clientType() != Client::TYPE_MAX); + + mClientType.SetValue(mParams.clientType()); + } + mNeedsMainThreadInit = true; return true; @@ -7779,7 +8260,7 @@ ClearOriginOp::DoInitOnMainThread() return rv; } - if (mParams.clearAll()) { + if (mParams.matchAll()) { mOriginScope.SetFromPrefix(origin); } else { mOriginScope.SetFromOrigin(origin); @@ -7793,11 +8274,16 @@ ClearOriginOp::GetResponse(RequestResponse& aResponse) { AssertIsOnOwningThread(); - aResponse = ClearOriginResponse(); + if (mClear) { + aResponse = ClearOriginResponse(); + } else { + aResponse = ResetOriginResponse(); + } } ClearDataOp::ClearDataOp(const RequestParams& aParams) - : ClearRequestBase(/* aExclusive */ true) + : ClearRequestBase(/* aExclusive */ true, + /* aClear */ true) , mParams(aParams) { MOZ_ASSERT(aParams.type() == RequestParams::TClearDataParams); @@ -7990,7 +8476,9 @@ PersistOp::DoDirectoryWork(QuotaManager* aQuotaManager) } bool created; - rv = aQuotaManager->EnsureOriginDirectory(directory, &created); + rv = aQuotaManager->EnsureOriginDirectory(directory, + /* aCreateIfNotExists */ true, + &created); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } @@ -8465,11 +8953,17 @@ OriginParser::Parse(nsACString& aSpec, OriginAttributes* aAttrs) -> ResultType if (mSchemeType == eFile) { spec.AppendLiteral("://"); - for (uint32_t count = mPathnameComponents.Length(), index = 0; - index < count; - index++) { - spec.Append('/'); - spec.Append(mPathnameComponents[index]); + if (mUniversalFileOrigin) { + MOZ_ASSERT(mPathnameComponents.Length() == 1); + + spec.Append(mPathnameComponents[0]); + } else { + for (uint32_t count = mPathnameComponents.Length(), index = 0; + index < count; + index++) { + spec.Append('/'); + spec.Append(mPathnameComponents[index]); + } } aSpec = spec; @@ -8637,7 +9131,7 @@ OriginParser::HandleToken(const nsDependentCSubstring& aToken) } if (mSchemeType == eFile) { - mState = eExpectingEmptyToken3; + mState = eExpectingEmptyTokenOrUniversalFileOrigin; } else { mState = eExpectingHost; } @@ -8645,20 +9139,31 @@ OriginParser::HandleToken(const nsDependentCSubstring& aToken) return; } - case eExpectingEmptyToken3: { + case eExpectingEmptyTokenOrUniversalFileOrigin: { MOZ_ASSERT(mSchemeType == eFile); - if (!aToken.IsEmpty()) { - QM_WARNING("Expected the third empty token!"); + if (aToken.IsEmpty()) { + mState = mTokenizer.hasMoreTokens() + ? eExpectingEmptyTokenOrDriveLetterOrPathnameComponent + : eComplete; - mError = true; return; } - mState = mTokenizer.hasMoreTokens() - ? eExpectingEmptyTokenOrDriveLetterOrPathnameComponent - : eComplete; + if (aToken.EqualsLiteral("UNIVERSAL_FILE_URI_ORIGIN")) { + mUniversalFileOrigin = true; + mPathnameComponents.AppendElement(aToken); + + mState = eComplete; + + return; + } + + QM_WARNING("Expected the third empty token or " + "UNIVERSAL_FILE_URI_ORIGIN!"); + + mError = true; return; } diff --git a/dom/quota/Client.h b/dom/quota/Client.h index dce4762b1c30..cfa32095f795 100644 --- a/dom/quota/Client.h +++ b/dom/quota/Client.h @@ -9,6 +9,7 @@ #include "mozilla/dom/quota/QuotaCommon.h" +#include "mozilla/dom/LocalStorageCommon.h" #include "mozilla/dom/ipc/IdType.h" #include "PersistenceType.h" @@ -24,6 +25,7 @@ class nsIRunnable; BEGIN_QUOTA_NAMESPACE +class OriginScope; class QuotaManager; class UsageInfo; @@ -39,14 +41,23 @@ public: enum Type { IDB = 0, - //LS, //APPCACHE, ASMJS, DOMCACHE, SDB, + LS, TYPE_MAX }; + static Type + TypeMax() + { + if (CachedNextGenLocalStorageEnabled()) { + return TYPE_MAX; + } + return LS; + } + virtual Type GetType() = 0; @@ -70,6 +81,13 @@ public: aText.AssignLiteral(SDB_DIRECTORY_NAME); break; + case LS: + if (CachedNextGenLocalStorageEnabled()) { + aText.AssignLiteral(LS_DIRECTORY_NAME); + break; + } + MOZ_FALLTHROUGH; + case TYPE_MAX: default: MOZ_ASSERT_UNREACHABLE("Bad id value!"); @@ -94,6 +112,10 @@ public: else if (aText.EqualsLiteral(SDB_DIRECTORY_NAME)) { aType = SDB; } + else if (CachedNextGenLocalStorageEnabled() && + aText.EqualsLiteral(LS_DIRECTORY_NAME)) { + aType = LS; + } else { return NS_ERROR_FAILURE; } @@ -101,6 +123,24 @@ public: return NS_OK; } + static nsresult + NullableTypeFromText(const nsAString& aText, Nullable* aType) + { + if (aText.IsVoid()) { + *aType = Nullable(); + return NS_OK; + } + + Type type; + nsresult rv = TypeFromText(aText, type); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + *aType = Nullable(type); + return NS_OK; + } + // Methods which are called on the IO thread. virtual nsresult UpgradeStorageFrom1_0To2_0(nsIFile* aDirectory) @@ -128,6 +168,15 @@ public: const AtomicBool& aCanceled, UsageInfo* aUsageInfo) = 0; + // This method is called when origins are about to be cleared + // (except the case when clearing is triggered by the origin eviction). + virtual nsresult + AboutToClearOrigins(const Nullable& aPersistenceType, + const OriginScope& aOriginScope) + { + return NS_OK; + } + virtual void OnOriginClearCompleted(PersistenceType aPersistenceType, const nsACString& aOrigin) = 0; diff --git a/dom/quota/PQuota.ipdl b/dom/quota/PQuota.ipdl index 81e2b4660eb8..61d5e3da6826 100644 --- a/dom/quota/PQuota.ipdl +++ b/dom/quota/PQuota.ipdl @@ -13,6 +13,9 @@ include "mozilla/dom/quota/SerializationHelpers.h"; using mozilla::dom::quota::PersistenceType from "mozilla/dom/quota/PersistenceType.h"; +using mozilla::dom::quota::Client::Type + from "mozilla/dom/quota/Client.h"; + namespace mozilla { namespace dom { namespace quota { @@ -48,12 +51,24 @@ union UsageRequestParams OriginUsageParams; }; -struct ClearOriginParams +struct ClearResetOriginParams { PrincipalInfo principalInfo; PersistenceType persistenceType; bool persistenceTypeIsExplicit; - bool clearAll; + Type clientType; + bool clientTypeIsExplicit; + bool matchAll; +}; + +struct ClearOriginParams +{ + ClearResetOriginParams commonParams; +}; + +struct ResetOriginParams +{ + ClearResetOriginParams commonParams; }; struct ClearDataParams @@ -85,6 +100,7 @@ union RequestParams InitTemporaryStorageParams; InitOriginParams; ClearOriginParams; + ResetOriginParams; ClearDataParams; ClearAllParams; ResetAllParams; diff --git a/dom/quota/PQuotaRequest.ipdl b/dom/quota/PQuotaRequest.ipdl index d9a0074bd8e7..9eae731fd9fc 100644 --- a/dom/quota/PQuotaRequest.ipdl +++ b/dom/quota/PQuotaRequest.ipdl @@ -25,6 +25,10 @@ struct ClearOriginResponse { }; +struct ResetOriginResponse +{ +}; + struct ClearDataResponse { }; @@ -53,6 +57,7 @@ union RequestResponse InitTemporaryStorageResponse; InitOriginResponse; ClearOriginResponse; + ResetOriginResponse; ClearDataResponse; ClearAllResponse; ResetAllResponse; diff --git a/dom/quota/QuotaManager.h b/dom/quota/QuotaManager.h index 7085aa9a0a2b..d99f9a908138 100644 --- a/dom/quota/QuotaManager.h +++ b/dom/quota/QuotaManager.h @@ -32,6 +32,12 @@ class nsIURI; class nsPIDOMWindowOuter; class nsIRunnable; +namespace mozilla { + +class OriginAttributes; + +} // namespace mozilla + BEGIN_QUOTA_NAMESPACE class DirectoryLockImpl; @@ -117,7 +123,8 @@ public: static const char kReplaceChars[]; static void - GetOrCreate(nsIRunnable* aCallback); + GetOrCreate(nsIRunnable* aCallback, + nsIEventTarget* aMainEventTarget = nullptr); // Returns a non-owning reference. static QuotaManager* @@ -178,6 +185,7 @@ public: const nsACString& aGroup, const nsACString& aOrigin, nsIFile* aFile, + int64_t aFileSize = -1, int64_t* aFileSizeOut = nullptr); already_AddRefed @@ -185,6 +193,7 @@ public: const nsACString& aGroup, const nsACString& aOrigin, const nsAString& aPath, + int64_t aFileSize = -1, int64_t* aFileSizeOut = nullptr); Nullable @@ -288,6 +297,7 @@ public: const nsACString& aSuffix, const nsACString& aGroup, const nsACString& aOrigin, + bool aCreateIfNotExists, nsIFile** aDirectory); nsresult @@ -295,6 +305,7 @@ public: const nsACString& aSuffix, const nsACString& aGroup, const nsACString& aOrigin, + bool aCreateIfNotExists, nsIFile** aDirectory, bool* aCreated); @@ -303,11 +314,18 @@ public: nsresult EnsureOriginDirectory(nsIFile* aDirectory, + bool aCreateIfNotExists, bool* aCreated); + nsresult + AboutToClearOrigins(const Nullable& aPersistenceType, + const OriginScope& aOriginScope, + const Nullable& aClientType); + void OriginClearCompleted(PersistenceType aPersistenceType, - const nsACString& aOrigin); + const nsACString& aOrigin, + const Nullable& aClientType); void ResetOrClearCompleted(); @@ -419,6 +437,11 @@ public: AreOriginsEqualOnDisk(nsACString& aOrigin1, nsACString& aOrigin2); + static bool + ParseOrigin(const nsACString& aOrigin, + nsCString& aSpec, + OriginAttributes* aAttrs); + private: QuotaManager(); @@ -498,6 +521,9 @@ private: nsresult MaybeRemoveLocalStorageDirectories(); + nsresult + MaybeCreateLocalStorageArchive(); + nsresult InitializeRepository(PersistenceType aPersistenceType); @@ -524,7 +550,7 @@ private: { AssertIsOnIOThread(); - for (uint32_t index = 0; index < Client::TYPE_MAX; index++) { + for (uint32_t index = 0; index < uint32_t(Client::TypeMax()); index++) { mClients[index]->ReleaseIOThreadObjects(); } } diff --git a/dom/quota/QuotaManagerService.cpp b/dom/quota/QuotaManagerService.cpp index 8b39cea75db8..663176641ee4 100644 --- a/dom/quota/QuotaManagerService.cpp +++ b/dom/quota/QuotaManagerService.cpp @@ -78,6 +78,53 @@ CheckedPrincipalToPrincipalInfo(nsIPrincipal* aPrincipal, return NS_OK; } +nsresult +GetClearResetOriginParams(nsIPrincipal* aPrincipal, + const nsACString& aPersistenceType, + const nsAString& aClientType, + bool aMatchAll, + ClearResetOriginParams& aParams) +{ + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(aPrincipal); + + nsresult rv = CheckedPrincipalToPrincipalInfo(aPrincipal, + aParams.principalInfo()); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + Nullable persistenceType; + rv = NullablePersistenceTypeFromText(aPersistenceType, &persistenceType); + if (NS_WARN_IF(NS_FAILED(rv))) { + return NS_ERROR_INVALID_ARG; + } + + if (persistenceType.IsNull()) { + aParams.persistenceTypeIsExplicit() = false; + } else { + aParams.persistenceType() = persistenceType.Value(); + aParams.persistenceTypeIsExplicit() = true; + } + + Nullable clientType; + rv = Client::NullableTypeFromText(aClientType, &clientType); + if (NS_WARN_IF(NS_FAILED(rv))) { + return NS_ERROR_INVALID_ARG; + } + + if (clientType.IsNull()) { + aParams.clientTypeIsExplicit() = false; + } else { + aParams.clientType() = clientType.Value(); + aParams.clientTypeIsExplicit() = true; + } + + aParams.matchAll() = aMatchAll; + + return NS_OK; +} + class AbortOperationsRunnable final : public Runnable { @@ -607,9 +654,34 @@ QuotaManagerService::Clear(nsIQuotaRequest** _retval) return NS_OK; } +NS_IMETHODIMP +QuotaManagerService::ClearStoragesForOriginAttributesPattern( + const nsAString& aPattern, + nsIQuotaRequest** _retval) +{ + MOZ_ASSERT(NS_IsMainThread()); + + RefPtr request = new Request(); + + ClearDataParams params; + + params.pattern() = aPattern; + + nsAutoPtr info(new RequestInfo(request, params)); + + nsresult rv = InitiateRequest(info); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + request.forget(_retval); + return NS_OK; +} + NS_IMETHODIMP QuotaManagerService::ClearStoragesForPrincipal(nsIPrincipal* aPrincipal, const nsACString& aPersistenceType, + const nsAString& aClientType, bool aClearAll, nsIQuotaRequest** _retval) { @@ -627,28 +699,19 @@ QuotaManagerService::ClearStoragesForPrincipal(nsIPrincipal* aPrincipal, RefPtr request = new Request(aPrincipal); - ClearOriginParams params; + ClearResetOriginParams commonParams; - nsresult rv = CheckedPrincipalToPrincipalInfo(aPrincipal, - params.principalInfo()); + nsresult rv = GetClearResetOriginParams(aPrincipal, + aPersistenceType, + aClientType, + aClearAll, + commonParams); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } - Nullable persistenceType; - rv = NullablePersistenceTypeFromText(aPersistenceType, &persistenceType); - if (NS_WARN_IF(NS_FAILED(rv))) { - return NS_ERROR_INVALID_ARG; - } - - if (persistenceType.IsNull()) { - params.persistenceTypeIsExplicit() = false; - } else { - params.persistenceType() = persistenceType.Value(); - params.persistenceTypeIsExplicit() = true; - } - - params.clearAll() = aClearAll; + RequestParams params; + params = ClearOriginParams(commonParams); nsAutoPtr info(new RequestInfo(request, params)); @@ -685,6 +748,56 @@ QuotaManagerService::Reset(nsIQuotaRequest** _retval) return NS_OK; } +NS_IMETHODIMP +QuotaManagerService::ResetStoragesForPrincipal(nsIPrincipal* aPrincipal, + const nsACString& aPersistenceType, + const nsAString& aClientType, + bool aResetAll, + nsIQuotaRequest** _retval) +{ + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(aPrincipal); + + if (NS_WARN_IF(!gTestingMode)) { + return NS_ERROR_UNEXPECTED; + } + + nsCString suffix; + aPrincipal->OriginAttributesRef().CreateSuffix(suffix); + + if (NS_WARN_IF(aResetAll && !suffix.IsEmpty())) { + // The originAttributes should be default originAttributes when the + // aClearAll flag is set. + return NS_ERROR_INVALID_ARG; + } + + RefPtr request = new Request(aPrincipal); + + ClearResetOriginParams commonParams; + + nsresult rv = GetClearResetOriginParams(aPrincipal, + aPersistenceType, + aClientType, + aResetAll, + commonParams); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + RequestParams params; + params = ResetOriginParams(commonParams); + + nsAutoPtr info(new RequestInfo(request, params)); + + rv = InitiateRequest(info); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + request.forget(_retval); + return NS_OK; +} + NS_IMETHODIMP QuotaManagerService::Persisted(nsIPrincipal* aPrincipal, nsIQuotaRequest** _retval) @@ -757,14 +870,10 @@ QuotaManagerService::Observe(nsISupports* aSubject, } if (!strcmp(aTopic, "clear-origin-attributes-data")) { - RefPtr request = new Request(); - - ClearDataParams params; - params.pattern() = nsDependentString(aData); - - nsAutoPtr info(new RequestInfo(request, params)); - - nsresult rv = InitiateRequest(info); + nsCOMPtr request; + nsresult rv = + ClearStoragesForOriginAttributesPattern(nsDependentString(aData), + getter_AddRefs(request)); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } diff --git a/dom/quota/SerializationHelpers.h b/dom/quota/SerializationHelpers.h index 2f32b195e578..29b75e176f00 100644 --- a/dom/quota/SerializationHelpers.h +++ b/dom/quota/SerializationHelpers.h @@ -9,6 +9,7 @@ #include "ipc/IPCMessageUtils.h" +#include "mozilla/dom/quota/Client.h" #include "mozilla/dom/quota/PersistenceType.h" #include "mozilla/OriginAttributes.h" @@ -22,6 +23,14 @@ struct ParamTraits : mozilla::dom::quota::PERSISTENCE_TYPE_INVALID> { }; +template <> +struct ParamTraits : + public ContiguousEnumSerializer< + mozilla::dom::quota::Client::Type, + mozilla::dom::quota::Client::IDB, + mozilla::dom::quota::Client::TYPE_MAX> +{ }; + template <> struct ParamTraits { diff --git a/dom/quota/nsIQuotaManagerService.idl b/dom/quota/nsIQuotaManagerService.idl index 196ea1316b5e..d972297ba998 100644 --- a/dom/quota/nsIQuotaManagerService.idl +++ b/dom/quota/nsIQuotaManagerService.idl @@ -96,6 +96,23 @@ interface nsIQuotaManagerService : nsISupports [must_use] nsIQuotaRequest clear(); + /** + * Removes all storages stored for the given pattern. The files may not be + * deleted immediately depending on prohibitive concurrent operations. + * In terms of locks, it will get an exclusive multi directory lock for given + * pattern. For example, given pattern {"appId":1007} and set of 3 origins + * ["http://www.mozilla.org^appId=1007", "http://www.example.org^appId=1007", + * "http://www.example.org^appId=1008"], the method will only lock 2 origins + * ["http://www.mozilla.org^appId=1007", "http://www.example.org^appId=1007"]. + * + * @param aPattern + * A pattern for the origins whose storages are to be cleared. + * Currently this is expected to be a JSON representation of the + * OriginAttributesPatternDictionary defined in ChromeUtils.webidl. + */ + [must_use] nsIQuotaRequest + clearStoragesForOriginAttributesPattern(in AString aPattern); + /** * Removes all storages stored for the given principal. The files may not be * deleted immediately depending on prohibitive concurrent operations. @@ -104,7 +121,20 @@ interface nsIQuotaManagerService : nsISupports * A principal for the origin whose storages are to be cleared. * @param aPersistenceType * An optional string that tells what persistence type of storages - * will be cleared. + * will be cleared. If omitted (or void), all persistence types will + * be cleared for the principal. If a single persistence type + * ("persistent", "temporary", or "default") is provided, then only + * that persistence directory will be considered. Note that + * "persistent" is different than being "persisted" via persist() and + * is only for chrome principals. See bug 1354500 for more info. + * In general, null is the right thing to pass here. + * @param aClientType + * An optional string that tells what client type of storages + * will be cleared. If omitted (or void), all client types will be + * cleared for the principal. If a single client type is provided + * from Client.h, then only that client's storage will be cleared. + * If you want to clear multiple client types (but not all), then you + * must call this method multiple times. * @param aClearAll * An optional boolean to indicate clearing all storages under the * given origin. @@ -112,6 +142,7 @@ interface nsIQuotaManagerService : nsISupports [must_use] nsIQuotaRequest clearStoragesForPrincipal(in nsIPrincipal aPrincipal, [optional] in ACString aPersistenceType, + [optional] in AString aClientType, [optional] in boolean aClearAll); /** @@ -126,6 +157,40 @@ interface nsIQuotaManagerService : nsISupports [must_use] nsIQuotaRequest reset(); + /** + * Resets all storages stored for the given principal. + * + * If the dom.quotaManager.testing preference is not true the call will be + * a no-op. + * + * @param aPrincipal + * A principal for the origin whose storages are to be reset. + * @param aPersistenceType + * An optional string that tells what persistence type of storages + * will be reset. If omitted (or void), all persistence types will + * be cleared for the principal. If a single persistence type + * ("persistent", "temporary", or "default") is provided, then only + * that persistence directory will be considered. Note that + * "persistent" is different than being "persisted" via persist() and + * is only for chrome principals. See bug 1354500 for more info. + * In general, null is the right thing to pass here. + * @param aClientType + * An optional string that tells what client type of storages + * will be reset. If omitted (or void), all client types will be + * cleared for the principal. If a single client type is provided + * from Client.h, then only that client's storage will be cleared. + * If you want to clear multiple client types (but not all), then you + * must call this method multiple times. + * @param aResetAll + * An optional boolean to indicate resetting all storages under the + * given origin. + */ + [must_use] nsIQuotaRequest + resetStoragesForPrincipal(in nsIPrincipal aPrincipal, + [optional] in ACString aPersistenceType, + [optional] in AString aClientType, + [optional] in boolean aResetAll); + /** * Check if given origin is persisted. * diff --git a/dom/quota/test/unit/createLocalStorage_profile.zip b/dom/quota/test/unit/createLocalStorage_profile.zip new file mode 100644 index 000000000000..d5958dbd5946 Binary files /dev/null and b/dom/quota/test/unit/createLocalStorage_profile.zip differ diff --git a/dom/quota/test/unit/head.js b/dom/quota/test/unit/head.js index 55c810d92e26..7c0848bb1d43 100644 --- a/dom/quota/test/unit/head.js +++ b/dom/quota/test/unit/head.js @@ -223,7 +223,9 @@ function installPackage(packageName) let file = getRelativeFile(entryName); if (zipentry.isDirectory) { - file.create(Ci.nsIFile.DIRECTORY_TYPE, parseInt("0755", 8)); + if (!file.exists()) { + file.create(Ci.nsIFile.DIRECTORY_TYPE, parseInt("0755", 8)); + } } else { let istream = zipReader.getInputStream(entryName); diff --git a/dom/quota/test/unit/test_createLocalStorage.js b/dom/quota/test/unit/test_createLocalStorage.js new file mode 100644 index 000000000000..c25df7100656 --- /dev/null +++ b/dom/quota/test/unit/test_createLocalStorage.js @@ -0,0 +1,161 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +async function testSteps() +{ + const webAppsStoreFile = "webappsstore.sqlite"; + const lsArchiveFile = "storage/ls-archive.sqlite"; + const lsArchiveTmpFile = "storage/ls-archive-tmp.sqlite"; + + function checkArchiveFileNotExists() + { + info("Checking archive tmp file"); + + let archiveTmpFile = getRelativeFile(lsArchiveTmpFile); + + let exists = archiveTmpFile.exists(); + ok(!exists, "archive tmp file doesn't exist"); + + info("Checking archive file"); + + let archiveFile = getRelativeFile(lsArchiveFile); + + exists = archiveFile.exists(); + ok(!exists, "archive file doesn't exist"); + } + + function checkArchiveFileExists() + { + info("Checking archive tmp file"); + + let archiveTmpFile = getRelativeFile(lsArchiveTmpFile); + + let exists = archiveTmpFile.exists(); + ok(!exists, "archive tmp file doesn't exist"); + + info("Checking archive file"); + + let archiveFile = getRelativeFile(lsArchiveFile); + + exists = archiveFile.exists(); + ok(exists, "archive file does exist"); + + info("Checking archive file size"); + + let fileSize = archiveFile.fileSize; + ok(fileSize > 0, "archive file size is greater than zero"); + } + + info("Setting pref"); + + SpecialPowers.setBoolPref("dom.storage.next_gen", true); + + // Profile 1 - Nonexistent apps store file. + info("Clearing"); + + let request = clear(); + await requestFinished(request); + + let appsStoreFile = getRelativeFile(webAppsStoreFile); + + let exists = appsStoreFile.exists(); + ok(!exists, "apps store file doesn't exist"); + + checkArchiveFileNotExists(); + + try { + request = init(); + await requestFinished(request); + + ok(true, "Should not have thrown"); + } catch(ex) { + ok(false, "Should not have thrown"); + } + + checkArchiveFileExists(); + + // Profile 2 - apps store file is a directory. + info("Clearing"); + + request = clear(); + await requestFinished(request); + + appsStoreFile.create(Ci.nsIFile.DIRECTORY_TYPE, parseInt("0755", 8)); + + checkArchiveFileNotExists(); + + try { + request = init(); + await requestFinished(request); + + ok(true, "Should not have thrown"); + } catch(ex) { + ok(false, "Should not have thrown"); + } + + checkArchiveFileExists(); + + appsStoreFile.remove(true); + + // Profile 3 - Corrupted apps store file. + info("Clearing"); + + request = clear(); + await requestFinished(request); + + let ostream = Cc["@mozilla.org/network/file-output-stream;1"] + .createInstance(Ci.nsIFileOutputStream); + ostream.init(appsStoreFile, -1, parseInt("0644", 8), 0); + ostream.write("foobar", 6); + ostream.close(); + + checkArchiveFileNotExists(); + + try { + request = init(); + await requestFinished(request); + + ok(true, "Should not have thrown"); + } catch(ex) { + ok(false, "Should not have thrown"); + } + + checkArchiveFileExists(); + + appsStoreFile.remove(false); + + // Profile 4 - Nonupdateable apps store file. + info("Clearing"); + + request = clear(); + await requestFinished(request); + + info("Installing package"); + + // The profile contains storage.sqlite and webappsstore.sqlite + // webappstore.sqlite was taken from FF 54 to force an upgrade. + // There's just one record in the webappsstore2 table. The record was + // modified by renaming the origin attribute userContextId to userContextKey. + // This triggers an error during the upgrade. + installPackage("createLocalStorage_profile"); + + let fileSize = appsStoreFile.fileSize; + ok(fileSize > 0, "apps store file size is greater than zero"); + + checkArchiveFileNotExists(); + + try { + request = init(); + await requestFinished(request); + + ok(true, "Should not have thrown"); + } catch(ex) { + ok(false, "Should not have thrown"); + } + + checkArchiveFileExists(); + + appsStoreFile.remove(false); +} diff --git a/dom/quota/test/unit/test_removeLocalStorage.js b/dom/quota/test/unit/test_removeLocalStorage.js index 9d4a3b80050a..162c10145412 100644 --- a/dom/quota/test/unit/test_removeLocalStorage.js +++ b/dom/quota/test/unit/test_removeLocalStorage.js @@ -11,6 +11,10 @@ function* testSteps() const lsArchiveTmpFile = "storage/ls-archive-tmp.sqlite"; const lsDir = "storage/default/http+++localhost/ls"; + info("Setting pref"); + + SpecialPowers.setBoolPref("dom.storage.next_gen", false); + // Profile 1 info("Clearing"); diff --git a/dom/quota/test/unit/test_specialOrigins.js b/dom/quota/test/unit/test_specialOrigins.js new file mode 100644 index 000000000000..d464e6edcbcc --- /dev/null +++ b/dom/quota/test/unit/test_specialOrigins.js @@ -0,0 +1,44 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +async function testSteps() +{ + const origins = [ + { + path: "storage/default/file+++UNIVERSAL_FILE_URI_ORIGIN", + url: "file:///Test/test.html", + persistence: "default" + }, + ]; + + info("Setting pref"); + + SpecialPowers.setBoolPref("security.fileuri.strict_origin_policy", false); + + info("Creating origin directories"); + + for (let origin of origins) { + let originDir = getRelativeFile(origin.path); + originDir.create(Ci.nsIFile.DIRECTORY_TYPE, parseInt("0755", 8)); + } + + info("Initializing origin directories"); + + for (let origin of origins) { + let result; + + try { + let request = initOrigin(getPrincipal(origin.url), + origin.persistence); + result = await requestFinished(request); + + ok(true, "Should not have thrown"); + } catch(ex) { + ok(false, "Should not have thrown"); + } + + ok(!result, "Origin directory wasn't created"); + } +} diff --git a/dom/quota/test/unit/xpcshell.ini b/dom/quota/test/unit/xpcshell.ini index dfeb62e1b2d6..6499a01a3428 100644 --- a/dom/quota/test/unit/xpcshell.ini +++ b/dom/quota/test/unit/xpcshell.ini @@ -6,6 +6,7 @@ head = head.js support-files = basics_profile.zip + createLocalStorage_profile.zip defaultStorageUpgrade_profile.zip getUsage_profile.zip idbSubdirUpgrade1_profile.zip @@ -22,6 +23,7 @@ support-files = [test_basics.js] [test_bad_origin_directory.js] +[test_createLocalStorage.js] [test_defaultStorageUpgrade.js] [test_getUsage.js] [test_idbSubdirUpgrade.js] @@ -36,6 +38,7 @@ support-files = [test_removeAppsUpgrade.js] [test_removeLocalStorage.js] [test_simpledb.js] +[test_specialOrigins.js] [test_storagePersistentUpgrade.js] [test_storagePressure.js] [test_tempMetadataCleanup.js] diff --git a/dom/simpledb/ActorsParent.cpp b/dom/simpledb/ActorsParent.cpp index c87c05d3ee52..22c9250c170c 100644 --- a/dom/simpledb/ActorsParent.cpp +++ b/dom/simpledb/ActorsParent.cpp @@ -1352,6 +1352,7 @@ OpenOp::DatabaseWork() mSuffix, mGroup, mOrigin, + /* aCreateIfNotExists */ true, getter_AddRefs(dbDirectory)); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; diff --git a/dom/storage/LocalStorage.cpp b/dom/storage/LocalStorage.cpp index 5255ce013b6d..f396de886661 100644 --- a/dom/storage/LocalStorage.cpp +++ b/dom/storage/LocalStorage.cpp @@ -33,7 +33,16 @@ using namespace ipc; namespace dom { -NS_IMPL_CYCLE_COLLECTION_INHERITED(LocalStorage, Storage, mManager); +NS_IMPL_CYCLE_COLLECTION_CLASS(LocalStorage) +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(LocalStorage, Storage) +NS_IMPL_CYCLE_COLLECTION_UNLINK(mManager) +NS_IMPL_CYCLE_COLLECTION_UNLINK_END +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(LocalStorage, Storage) + CycleCollectionNoteChild(cb, + NS_ISUPPORTS_CAST(nsIDOMStorageManager*, + tmp->mManager.get()), + "mManager"); +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(LocalStorage) NS_INTERFACE_MAP_ENTRY(nsISupportsWeakReference) diff --git a/dom/storage/LocalStorageManager.cpp b/dom/storage/LocalStorageManager.cpp index 7a37097ef53d..5a57a650fb44 100644 --- a/dom/storage/LocalStorageManager.cpp +++ b/dom/storage/LocalStorageManager.cpp @@ -21,6 +21,7 @@ #include "nsIObserverService.h" #include "mozilla/Services.h" #include "mozilla/Preferences.h" +#include "mozilla/dom/LocalStorageCommon.h" // Only allow relatively small amounts of data since performance of // the synchronous IO is very bad. @@ -56,11 +57,14 @@ LocalStorageManager::GetQuota() } NS_IMPL_ISUPPORTS(LocalStorageManager, - nsIDOMStorageManager) + nsIDOMStorageManager, + nsILocalStorageManager) LocalStorageManager::LocalStorageManager() : mCaches(8) { + MOZ_ASSERT(!NextGenLocalStorageEnabled()); + StorageObserver* observer = StorageObserver::Self(); NS_ASSERTION(observer, "No StorageObserver, cannot observe private data delete notifications!"); @@ -367,6 +371,40 @@ LocalStorageManager::CheckStorage(nsIPrincipal* aPrincipal, return NS_OK; } +NS_IMETHODIMP +LocalStorageManager::GetNextGenLocalStorageEnabled(bool* aResult) +{ + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(aResult); + + *aResult = NextGenLocalStorageEnabled(); + return NS_OK; +} + +NS_IMETHODIMP +LocalStorageManager::Preload(nsIPrincipal* aPrincipal, + JSContext* aContext, + nsISupports** _retval) +{ + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(aPrincipal); + MOZ_ASSERT(_retval); + + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +LocalStorageManager::IsPreloaded(nsIPrincipal* aPrincipal, + JSContext* aContext, + nsISupports** _retval) +{ + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(aPrincipal); + MOZ_ASSERT(_retval); + + return NS_ERROR_NOT_IMPLEMENTED; +} + void LocalStorageManager::ClearCaches(uint32_t aUnloadFlags, const OriginAttributesPattern& aPattern, @@ -473,9 +511,20 @@ LocalStorageManager::Observe(const char* aTopic, return NS_ERROR_UNEXPECTED; } +// static +LocalStorageManager* +LocalStorageManager::Self() +{ + MOZ_ASSERT(!NextGenLocalStorageEnabled()); + + return sSelf; +} + LocalStorageManager* LocalStorageManager::Ensure() { + MOZ_ASSERT(!NextGenLocalStorageEnabled()); + if (sSelf) { return sSelf; } diff --git a/dom/storage/LocalStorageManager.h b/dom/storage/LocalStorageManager.h index 2a36278ace49..68f1516aa5a1 100644 --- a/dom/storage/LocalStorageManager.h +++ b/dom/storage/LocalStorageManager.h @@ -8,6 +8,7 @@ #define mozilla_dom_StorageManager_h #include "nsIDOMStorageManager.h" +#include "nsILocalStorageManager.h" #include "StorageObserver.h" #include "LocalStorage.h" @@ -26,10 +27,12 @@ class OriginAttributesPattern; namespace dom { class LocalStorageManager final : public nsIDOMStorageManager + , public nsILocalStorageManager , public StorageObserverSink { NS_DECL_ISUPPORTS NS_DECL_NSIDOMSTORAGEMANAGER + NS_DECL_NSILOCALSTORAGEMANAGER public: LocalStorageManager(); @@ -117,7 +120,7 @@ private: const nsACString& aKeyPrefix); // Global getter of localStorage manager service - static LocalStorageManager* Self() { return sSelf; } + static LocalStorageManager* Self(); // Like Self, but creates an instance if we're not yet initialized. static LocalStorageManager* Ensure(); diff --git a/dom/storage/Storage.cpp b/dom/storage/Storage.cpp index 79aa0a77ef36..f54c06050b13 100644 --- a/dom/storage/Storage.cpp +++ b/dom/storage/Storage.cpp @@ -19,7 +19,7 @@ static const char kStorageEnabled[] = "dom.storage.enabled"; NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(Storage, mWindow, mPrincipal) NS_IMPL_CYCLE_COLLECTING_ADDREF(Storage) -NS_IMPL_CYCLE_COLLECTING_RELEASE(Storage) +NS_IMPL_CYCLE_COLLECTING_RELEASE_WITH_LAST_RELEASE(Storage, LastRelease()) NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(Storage) NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY diff --git a/dom/storage/Storage.h b/dom/storage/Storage.h index 550f83ac85e3..75180573b840 100644 --- a/dom/storage/Storage.h +++ b/dom/storage/Storage.h @@ -110,11 +110,41 @@ public: bool IsSessionOnly() const { return mIsSessionOnly; } + ////////////////////////////////////////////////////////////////////////////// + // Testing Methods: + // + // These methods are exposed on the `Storage` WebIDL interface behind a + // preference for the benefit of automated-tests. They are not exposed to + // content. See `Storage.webidl` for more details. + + virtual void + Open(nsIPrincipal& aSubjectPrincipal, ErrorResult& aRv) + { } + + virtual void + Close(nsIPrincipal& aSubjectPrincipal, ErrorResult& aRv) + { } + + virtual void + BeginExplicitSnapshot(nsIPrincipal& aSubjectPrincipal, ErrorResult& aRv) + { } + + virtual void + EndExplicitSnapshot(nsIPrincipal& aSubjectPrincipal, ErrorResult& aRv) + { } + + ////////////////////////////////////////////////////////////////////////////// + + // Dispatch storage notification events on all impacted pages in the current + // process as well as for consumption by devtools. Pages receive the + // notification via StorageNotifierService (not observers like in the past), + // while devtools does receive the notification via the observer service. + // // aStorage can be null if this method is called by LocalStorageCacheChild. // // aImmediateDispatch is for use by child IPC code (LocalStorageCacheChild) // so that PBackground ordering can be maintained. Without this, the event - // would be/ enqueued and run in a future turn of the event loop, potentially + // would be enqueued and run in a future turn of the event loop, potentially // allowing other PBackground Recv* methods to trigger script that wants to // assume our localstorage changes have already been applied. This is the // case for message manager messages which are used by ContentTask testing @@ -138,6 +168,10 @@ protected: // hand together). bool CanUseStorage(nsIPrincipal& aSubjectPrincipal); + virtual void + LastRelease() + { } + private: nsCOMPtr mWindow; nsCOMPtr mPrincipal; diff --git a/dom/storage/StorageDBThread.cpp b/dom/storage/StorageDBThread.cpp index f956bd0ba36e..2e38a7c7116c 100644 --- a/dom/storage/StorageDBThread.cpp +++ b/dom/storage/StorageDBThread.cpp @@ -58,63 +58,6 @@ StorageDBThread* sStorageThread = nullptr; // False until we shut the storage thread down. bool sStorageThreadDown = false; -// This is only a compatibility code for schema version 0. Returns the 'scope' -// key in the schema version 0 format for the scope column. -nsCString -Scheme0Scope(LocalStorageCacheBridge* aCache) -{ - nsCString result; - - nsCString suffix = aCache->OriginSuffix(); - - OriginAttributes oa; - if (!suffix.IsEmpty()) { - DebugOnly success = oa.PopulateFromSuffix(suffix); - MOZ_ASSERT(success); - } - - if (oa.mAppId != nsIScriptSecurityManager::NO_APP_ID || - oa.mInIsolatedMozBrowser) { - result.AppendInt(oa.mAppId); - result.Append(':'); - result.Append(oa.mInIsolatedMozBrowser ? 't' : 'f'); - result.Append(':'); - } - - // If there is more than just appid and/or inbrowser stored in origin - // attributes, put it to the schema 0 scope as well. We must do that - // to keep the scope column unique (same resolution as schema 1 has - // with originAttributes and originKey columns) so that switch between - // schema 1 and 0 always works in both ways. - nsAutoCString remaining; - oa.mAppId = 0; - oa.mInIsolatedMozBrowser = false; - oa.CreateSuffix(remaining); - if (!remaining.IsEmpty()) { - MOZ_ASSERT(!suffix.IsEmpty()); - - if (result.IsEmpty()) { - // Must contain the old prefix, otherwise we won't search for the whole - // origin attributes suffix. - result.AppendLiteral("0:f:"); - } - - // Append the whole origin attributes suffix despite we have already stored - // appid and inbrowser. We are only looking for it when the scope string - // starts with "$appid:$inbrowser:" (with whatever valid values). - // - // The OriginAttributes suffix is a string in a form like: - // "^addonId=101&userContextId=5" and it's ensured it always starts with '^' - // and never contains ':'. See OriginAttributes::CreateSuffix. - result.Append(suffix); - result.Append(':'); - } - - result.Append(aCache->OriginNoSuffix()); - - return result; -} - } // anon // XXX Fix me! @@ -1156,7 +1099,8 @@ StorageDBThread::DBOperation::Perform(StorageDBThread* aThread) NS_ENSURE_SUCCESS(rv, rv); // Filling the 'scope' column just for downgrade compatibility reasons rv = stmt->BindUTF8StringByName(NS_LITERAL_CSTRING("scope"), - Scheme0Scope(mCache)); + Scheme0Scope(mCache->OriginSuffix(), + mCache->OriginNoSuffix())); NS_ENSURE_SUCCESS(rv, rv); rv = stmt->BindStringByName(NS_LITERAL_CSTRING("key"), mKey); diff --git a/dom/storage/StorageIPC.cpp b/dom/storage/StorageIPC.cpp index 54bcaab64269..760bcfca7b1f 100644 --- a/dom/storage/StorageIPC.cpp +++ b/dom/storage/StorageIPC.cpp @@ -147,6 +147,7 @@ StorageDBChild::StorageDBChild(LocalStorageManager* aManager) , mStatus(NS_OK) , mIPCOpen(false) { + MOZ_ASSERT(!NextGenLocalStorageEnabled()); } StorageDBChild::~StorageDBChild() @@ -158,6 +159,7 @@ StorageDBChild* StorageDBChild::Get() { MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(!NextGenLocalStorageEnabled()); return sStorageChild; } @@ -167,6 +169,7 @@ StorageDBChild* StorageDBChild::GetOrCreate() { MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(!NextGenLocalStorageEnabled()); if (sStorageChild || sStorageChildDown) { // When sStorageChildDown is at true, sStorageChild is null. diff --git a/dom/storage/StorageNotifierService.h b/dom/storage/StorageNotifierService.h index 8762785f7014..d7c8034389ff 100644 --- a/dom/storage/StorageNotifierService.h +++ b/dom/storage/StorageNotifierService.h @@ -14,6 +14,13 @@ namespace dom { class StorageEvent; +/** + * Enables the StorageNotifierService to check whether an observer is interested + * in receiving events for the given principal before calling the method, an + * optimization refactoring. + * + * Expected to only be implemented by nsGlobalWindowObserver or its succesor. + */ class StorageNotificationObserver { public: @@ -34,6 +41,15 @@ public: GetEventTarget() const = 0; }; +/** + * A specialized version of the observer service that uses the custom + * StorageNotificationObserver so that principal checks can happen in this class + * rather than in the nsIObserver::observe method where they used to happen. + * + * The only expected consumers are nsGlobalWindowInner instances via their + * nsGlobalWindowObserver helper that avoids being able to use the window as an + * nsIObserver. + */ class StorageNotifierService final { public: diff --git a/dom/storage/StorageObserver.cpp b/dom/storage/StorageObserver.cpp index 56fd031a010f..79f3845d3f9b 100644 --- a/dom/storage/StorageObserver.cpp +++ b/dom/storage/StorageObserver.cpp @@ -181,13 +181,15 @@ StorageObserver::ClearMatchingOrigin(const char16_t* aData, return rv; } - if (XRE_IsParentProcess()) { - StorageDBChild* storageChild = StorageDBChild::GetOrCreate(); - if (NS_WARN_IF(!storageChild)) { - return NS_ERROR_FAILURE; - } + if (!NextGenLocalStorageEnabled()) { + if (XRE_IsParentProcess()) { + StorageDBChild* storageChild = StorageDBChild::GetOrCreate(); + if (NS_WARN_IF(!storageChild)) { + return NS_ERROR_FAILURE; + } - storageChild->SendClearMatchingOrigin(originScope); + storageChild->SendClearMatchingOrigin(originScope); + } } aOriginScope = originScope; @@ -205,6 +207,10 @@ StorageObserver::Observe(nsISupports* aSubject, if (!strcmp(aTopic, kStartupTopic)) { MOZ_ASSERT(XRE_IsParentProcess()); + if (NextGenLocalStorageEnabled()) { + return NS_OK; + } + nsCOMPtr obs = mozilla::services::GetObserverService(); obs->RemoveObserver(this, kStartupTopic); @@ -215,6 +221,7 @@ StorageObserver::Observe(nsISupports* aSubject, // Timer callback used to start the database a short timer after startup if (!strcmp(aTopic, NS_TIMER_CALLBACK_TOPIC)) { MOZ_ASSERT(XRE_IsParentProcess()); + MOZ_ASSERT(!NextGenLocalStorageEnabled()); nsCOMPtr timer = do_QueryInterface(aSubject); if (!timer) { @@ -241,15 +248,17 @@ StorageObserver::Observe(nsISupports* aSubject, return NS_OK; } - StorageDBChild* storageChild = StorageDBChild::GetOrCreate(); - if (NS_WARN_IF(!storageChild)) { - return NS_ERROR_FAILURE; - } + if (!NextGenLocalStorageEnabled()) { + StorageDBChild* storageChild = StorageDBChild::GetOrCreate(); + if (NS_WARN_IF(!storageChild)) { + return NS_ERROR_FAILURE; + } - storageChild->AsyncClearAll(); + storageChild->AsyncClearAll(); - if (XRE_IsParentProcess()) { - storageChild->SendClearAll(); + if (XRE_IsParentProcess()) { + storageChild->SendClearAll(); + } } Notify("cookie-cleared"); @@ -311,6 +320,10 @@ StorageObserver::Observe(nsISupports* aSubject, } if (!strcmp(aTopic, "extension:purge-localStorage")) { + if (NextGenLocalStorageEnabled()) { + return NS_OK; + } + const char topic[] = "extension:purge-localStorage-caches"; if (aData) { @@ -355,6 +368,10 @@ StorageObserver::Observe(nsISupports* aSubject, // Clear all private-browsing caches if (!strcmp(aTopic, "last-pb-context-exited")) { + if (NextGenLocalStorageEnabled()) { + return NS_OK; + } + Notify("private-browsing-data-cleared"); return NS_OK; @@ -364,6 +381,10 @@ StorageObserver::Observe(nsISupports* aSubject, if (!strcmp(aTopic, "clear-origin-attributes-data")) { MOZ_ASSERT(XRE_IsParentProcess()); + if (NextGenLocalStorageEnabled()) { + return NS_OK; + } + OriginAttributesPattern pattern; if (!pattern.Init(nsDependentString(aData))) { NS_ERROR("Cannot parse origin attributes pattern"); @@ -391,6 +412,10 @@ StorageObserver::Observe(nsISupports* aSubject, if (!strcmp(aTopic, "profile-before-change")) { MOZ_ASSERT(XRE_IsParentProcess()); + if (NextGenLocalStorageEnabled()) { + return NS_OK; + } + if (mBackgroundThread) { bool done = false; @@ -409,6 +434,10 @@ StorageObserver::Observe(nsISupports* aSubject, #ifdef DOM_STORAGE_TESTS if (!strcmp(aTopic, "domstorage-test-flush-force")) { + if (NextGenLocalStorageEnabled()) { + return NS_OK; + } + StorageDBChild* storageChild = StorageDBChild::GetOrCreate(); if (NS_WARN_IF(!storageChild)) { return NS_ERROR_FAILURE; @@ -420,6 +449,10 @@ StorageObserver::Observe(nsISupports* aSubject, } if (!strcmp(aTopic, "domstorage-test-flushed")) { + if (NextGenLocalStorageEnabled()) { + return NS_OK; + } + // Only used to propagate to IPC children Notify("test-flushed"); @@ -427,6 +460,10 @@ StorageObserver::Observe(nsISupports* aSubject, } if (!strcmp(aTopic, "domstorage-test-reload")) { + if (NextGenLocalStorageEnabled()) { + return NS_OK; + } + Notify("test-reload"); return NS_OK; diff --git a/dom/storage/StorageUtils.cpp b/dom/storage/StorageUtils.cpp index 076f49e650b5..a239de4357d1 100644 --- a/dom/storage/StorageUtils.cpp +++ b/dom/storage/StorageUtils.cpp @@ -113,6 +113,62 @@ CreateReversedDomain(const nsACString& aAsciiDomain, return NS_OK; } +// This is only a compatibility code for schema version 0. Returns the 'scope' +// key in the schema version 0 format for the scope column. +nsCString +Scheme0Scope(const nsACString& aOriginSuffix, + const nsACString& aOriginNoSuffix) +{ + nsCString result; + + OriginAttributes oa; + if (!aOriginSuffix.IsEmpty()) { + DebugOnly success = oa.PopulateFromSuffix(aOriginSuffix); + MOZ_ASSERT(success); + } + + if (oa.mAppId != nsIScriptSecurityManager::NO_APP_ID || + oa.mInIsolatedMozBrowser) { + result.AppendInt(oa.mAppId); + result.Append(':'); + result.Append(oa.mInIsolatedMozBrowser ? 't' : 'f'); + result.Append(':'); + } + + // If there is more than just appid and/or inbrowser stored in origin + // attributes, put it to the schema 0 scope as well. We must do that + // to keep the scope column unique (same resolution as schema 1 has + // with originAttributes and originKey columns) so that switch between + // schema 1 and 0 always works in both ways. + nsAutoCString remaining; + oa.mAppId = 0; + oa.mInIsolatedMozBrowser = false; + oa.CreateSuffix(remaining); + if (!remaining.IsEmpty()) { + MOZ_ASSERT(!aOriginSuffix.IsEmpty()); + + if (result.IsEmpty()) { + // Must contain the old prefix, otherwise we won't search for the whole + // origin attributes suffix. + result.AppendLiteral("0:f:"); + } + + // Append the whole origin attributes suffix despite we have already stored + // appid and inbrowser. We are only looking for it when the scope string + // starts with "$appid:$inbrowser:" (with whatever valid values). + // + // The OriginAttributes suffix is a string in a form like: + // "^addonId=101&userContextId=5" and it's ensured it always starts with '^' + // and never contains ':'. See OriginAttributes::CreateSuffix. + result.Append(aOriginSuffix); + result.Append(':'); + } + + result.Append(aOriginNoSuffix); + + return result; +} + } // StorageUtils namespace } // dom namespace } // mozilla namespace diff --git a/dom/storage/StorageUtils.h b/dom/storage/StorageUtils.h index a48eed332855..75b85d17cb4f 100644 --- a/dom/storage/StorageUtils.h +++ b/dom/storage/StorageUtils.h @@ -28,6 +28,10 @@ nsresult CreateReversedDomain(const nsACString& aAsciiDomain, nsACString& aKey); +nsCString +Scheme0Scope(const nsACString& aOriginSuffix, + const nsACString& aOriginNoSuffix); + } // StorageUtils namespace } // dom namespace } // mozilla namespace diff --git a/dom/storage/moz.build b/dom/storage/moz.build index c1173af7d02c..345a018c96df 100644 --- a/dom/storage/moz.build +++ b/dom/storage/moz.build @@ -14,6 +14,7 @@ EXPORTS.mozilla.dom += [ 'SessionStorageManager.h', 'Storage.h', 'StorageActivityService.h', + 'StorageDBUpdater.h', 'StorageIPC.h', 'StorageNotifierService.h', 'StorageObserver.h', diff --git a/dom/tests/browser/browser.ini b/dom/tests/browser/browser.ini index 26ed1132a8a0..0384564616ca 100644 --- a/dom/tests/browser/browser.ini +++ b/dom/tests/browser/browser.ini @@ -4,6 +4,7 @@ support-files = browser_frame_elements.html page_privatestorageevent.html page_localstorage_e10s.html + page_localstorage_snapshotting_e10s.html position.html test-console-api.html test_bug1004814.html @@ -16,6 +17,7 @@ support-files = test_largeAllocation2.html^headers^ test_largeAllocationFormSubmit.sjs helper_largeAllocation.js + helper_localStorage_e10s.js !/dom/tests/mochitest/geolocation/network_geolocation.sjs [browser_allocateGigabyte.js] @@ -56,6 +58,8 @@ skip-if = !e10s || (os == "win" && processor == "x86") || (verify && debug && (o [browser_localStorage_e10s.js] skip-if = !e10s || verify # This is a test of e10s functionality. [browser_localStorage_privatestorageevent.js] +[browser_localStorage_snapshotting_e10s.js] +skip-if = !e10s # This is a test of e10s functionality. [browser_persist_cookies.js] support-files = set-samesite-cookies-and-redirect.sjs diff --git a/dom/tests/browser/browser_localStorage_e10s.js b/dom/tests/browser/browser_localStorage_e10s.js index 67d19784928d..44b6f1a80fd2 100644 --- a/dom/tests/browser/browser_localStorage_e10s.js +++ b/dom/tests/browser/browser_localStorage_e10s.js @@ -2,74 +2,9 @@ const HELPER_PAGE_URL = "http://example.com/browser/dom/tests/browser/page_localstorage_e10s.html"; const HELPER_PAGE_ORIGIN = "http://example.com/"; -// Simple tab wrapper abstracting our messaging mechanism; -class KnownTab { - constructor(name, tab) { - this.name = name; - this.tab = tab; - } - - cleanup() { - this.tab = null; - } -} - -// Simple data structure class to help us track opened tabs and their pids. -class KnownTabs { - constructor() { - this.byPid = new Map(); - this.byName = new Map(); - } - - cleanup() { - this.byPid = null; - this.byName = null; - } -} - -/** - * Open our helper page in a tab in its own content process, asserting that it - * really is in its own process. We initially load and wait for about:blank to - * load, and only then loadURI to our actual page. This is to ensure that - * LocalStorageManager has had an opportunity to be created and populate - * mOriginsHavingData. - * - * (nsGlobalWindow will reliably create LocalStorageManager as a side-effect of - * the unconditional call to nsGlobalWindow::PreloadLocalStorage. This will - * reliably create the StorageDBChild instance, and its corresponding - * StorageDBParent will send the set of origins when it is constructed.) - */ -async function openTestTabInOwnProcess(name, knownTabs) { - let realUrl = HELPER_PAGE_URL + '?' + encodeURIComponent(name); - // Load and wait for about:blank. - let tab = await BrowserTestUtils.openNewForegroundTab({ - gBrowser, opening: 'about:blank', forceNewProcess: true - }); - let pid = tab.linkedBrowser.frameLoader.tabParent.osPid; - ok(!knownTabs.byName.has(name), "tab needs its own name: " + name); - ok(!knownTabs.byPid.has(pid), "tab needs to be in its own process: " + pid); - - let knownTab = new KnownTab(name, tab); - knownTabs.byPid.set(pid, knownTab); - knownTabs.byName.set(name, knownTab); - - // Now trigger the actual load of our page. - BrowserTestUtils.loadURI(tab.linkedBrowser, realUrl); - await BrowserTestUtils.browserLoaded(tab.linkedBrowser); - is(tab.linkedBrowser.frameLoader.tabParent.osPid, pid, "still same pid"); - return knownTab; -} - -/** - * Close all the tabs we opened. - */ -async function cleanupTabs(knownTabs) { - for (let knownTab of knownTabs.byName.values()) { - BrowserTestUtils.removeTab(knownTab.tab); - knownTab.cleanup(); - } - knownTabs.cleanup(); -} +let testDir = gTestPath.substr(0, gTestPath.lastIndexOf("/")); +Services.scriptloader.loadSubScript(testDir + "/helper_localStorage_e10s.js", + this); /** * Wait for a LocalStorage flush to occur. This notification can occur as a @@ -79,6 +14,10 @@ async function cleanupTabs(knownTabs) { * - Us generating a "domstorage-test-flush-force" observer notification. */ function waitForLocalStorageFlush() { + if (Services.lsm.nextGenLocalStorageEnabled) { + return new Promise(resolve => executeSoon(resolve)); + } + return new Promise(function(resolve) { let observer = { observe: function() { @@ -101,6 +40,10 @@ function waitForLocalStorageFlush() { * notifications, but correctness is guaranteed after the second notification. */ function triggerAndWaitForLocalStorageFlush() { + if (Services.lsm.nextGenLocalStorageEnabled) { + return new Promise(resolve => executeSoon(resolve)); + } + SpecialPowers.notifyObservers(null, "domstorage-test-flush-force"); // This first wait is ambiguous... return waitForLocalStorageFlush().then(function() { @@ -127,6 +70,18 @@ function clearOriginStorageEnsuringNoPreload() { let principal = Services.scriptSecurityManager.createCodebasePrincipalFromOrigin( HELPER_PAGE_ORIGIN); + + if (Services.lsm.nextGenLocalStorageEnabled) { + let request = + Services.qms.clearStoragesForPrincipal(principal, "default", "ls"); + let promise = new Promise(resolve => { + request.callback = () => { + resolve(); + }; + }); + return promise; + } + // We want to use createStorage to force the cache to be created so we can // issue the clear. It's possible for getStorage to return false but for the // origin preload hash to still have our origin in it. @@ -146,6 +101,9 @@ async function verifyTabPreload(knownTab, expectStorageExists) { let principal = Services.scriptSecurityManager.createCodebasePrincipalFromOrigin( origin); + if (Services.lsm.nextGenLocalStorageEnabled) { + return Services.lsm.isPreloaded(principal); + } return !!Services.domStorageManager.getStorage(null, principal); }); is(storageExists, expectStorageExists, "Storage existence === preload"); @@ -317,10 +275,13 @@ add_task(async function() { // - Open tabs. Don't configure any of them yet. const knownTabs = new KnownTabs(); - const writerTab = await openTestTabInOwnProcess("writer", knownTabs); - const listenerTab = await openTestTabInOwnProcess("listener", knownTabs); - const readerTab = await openTestTabInOwnProcess("reader", knownTabs); - const lateWriteThenListenTab = await openTestTabInOwnProcess( + const writerTab = await openTestTabInOwnProcess(HELPER_PAGE_URL, "writer", + knownTabs); + const listenerTab = await openTestTabInOwnProcess(HELPER_PAGE_URL, "listener", + knownTabs); + const readerTab = await openTestTabInOwnProcess(HELPER_PAGE_URL, "reader", + knownTabs); + const lateWriteThenListenTab = await openTestTabInOwnProcess(HELPER_PAGE_URL, "lateWriteThenListen", knownTabs); // Sanity check that preloading did not occur in the tabs. @@ -461,8 +422,8 @@ add_task(async function() { // - Open a fresh tab and make sure it sees the precache/preload info("late open preload check"); - const lateOpenSeesPreload = - await openTestTabInOwnProcess("lateOpenSeesPreload", knownTabs); + const lateOpenSeesPreload = await openTestTabInOwnProcess(HELPER_PAGE_URL, + "lateOpenSeesPreload", knownTabs); await verifyTabPreload(lateOpenSeesPreload, true); // - Clean up. diff --git a/dom/tests/browser/browser_localStorage_snapshotting_e10s.js b/dom/tests/browser/browser_localStorage_snapshotting_e10s.js new file mode 100644 index 000000000000..c1a6ac28ffa4 --- /dev/null +++ b/dom/tests/browser/browser_localStorage_snapshotting_e10s.js @@ -0,0 +1,371 @@ +const HELPER_PAGE_URL = + "http://example.com/browser/dom/tests/browser/page_localstorage_snapshotting_e10s.html"; +const HELPER_PAGE_ORIGIN = "http://example.com/"; + +let testDir = gTestPath.substr(0, gTestPath.lastIndexOf("/")); +Services.scriptloader.loadSubScript(testDir + "/helper_localStorage_e10s.js", + this); + +function clearOrigin() { + let principal = + Services.scriptSecurityManager.createCodebasePrincipalFromOrigin( + HELPER_PAGE_ORIGIN); + let request = + Services.qms.clearStoragesForPrincipal(principal, "default", "ls"); + let promise = new Promise(resolve => { + request.callback = () => { + resolve(); + }; + }); + return promise; +} + +async function applyMutations(knownTab, mutations) { + await ContentTask.spawn( + knownTab.tab.linkedBrowser, + mutations, + function(mutations) { + return content.wrappedJSObject.applyMutations(Cu.cloneInto(mutations, + content)); + }); +} + +async function verifyState(knownTab, expectedState) { + let actualState = await ContentTask.spawn( + knownTab.tab.linkedBrowser, + {}, + function() { + return content.wrappedJSObject.getState(); + }); + + for (let [expectedKey, expectedValue] of Object.entries(expectedState)) { + ok(actualState.hasOwnProperty(expectedKey), "key present: " + expectedKey); + is(actualState[expectedKey], expectedValue, "value correct"); + } + for (let actualKey of Object.keys(actualState)) { + if (!expectedState.hasOwnProperty(actualKey)) { + ok(false, "actual state has key it shouldn't have: " + actualKey); + } + } +} + +async function getKeys(knownTab) { + let keys = await ContentTask.spawn( + knownTab.tab.linkedBrowser, + null, + function() { + return content.wrappedJSObject.getKeys(); + }); + return keys; +} + +async function beginExplicitSnapshot(knownTab) { + await ContentTask.spawn( + knownTab.tab.linkedBrowser, + null, + function() { + return content.wrappedJSObject.beginExplicitSnapshot(); + }); +} + +async function endExplicitSnapshot(knownTab) { + await ContentTask.spawn( + knownTab.tab.linkedBrowser, + null, + function() { + return content.wrappedJSObject.endExplicitSnapshot(); + }); +} + +// We spin up a ton of child processes. +requestLongerTimeout(4); + +/** + * Verify snapshotting of our localStorage implementation in multi-e10s setup. + */ +add_task(async function() { + if (!Services.lsm.nextGenLocalStorageEnabled) { + ok(true, "Test ignored when the next gen local storage is not enabled."); + return; + } + + await SpecialPowers.pushPrefEnv({ + set: [ + // Enable LocalStorage's testing API so we can explicitly create + // snapshots when needed. + ["dom.storage.testing", true], + ] + }); + + // Ensure that there is no localstorage data by forcing the origin to be + // cleared prior to the start of our test.. + await clearOrigin(); + + // - Open tabs. Don't configure any of them yet. + const knownTabs = new KnownTabs(); + const writerTab1 = await openTestTabInOwnProcess(HELPER_PAGE_URL, "writer1", + knownTabs); + const writerTab2 = await openTestTabInOwnProcess(HELPER_PAGE_URL, "writer2", + knownTabs); + const readerTab1 = await openTestTabInOwnProcess(HELPER_PAGE_URL, "reader1", + knownTabs); + const readerTab2 = await openTestTabInOwnProcess(HELPER_PAGE_URL, "reader2", + knownTabs); + + const initialMutations = [ + [null, null], + ["key1", "initial1"], + ["key2", "initial2"], + ["key3", "initial3"], + ["key5", "initial5"], + ["key6", "initial6"], + ["key7", "initial7"], + ["key8", "initial8"] + ]; + + const initialState = { + key1: "initial1", + key2: "initial2", + key3: "initial3", + key5: "initial5", + key6: "initial6", + key7: "initial7", + key8: "initial8" + }; + + function getPartialPrefill() + { + let size = 0; + let entries = Object.entries(initialState); + for (let i = 0; i < entries.length / 2; i++) { + let entry = entries[i]; + size += entry[0].length + entry[1].length; + } + return size; + } + + const prefillValues = [ + 0, // no prefill + getPartialPrefill(), // partial prefill + -1 // full prefill + ]; + + for (let prefillValue of prefillValues) { + info("Setting prefill value"); + + await SpecialPowers.pushPrefEnv({ + set: [ + ["dom.storage.snapshot_prefill", prefillValue] + ] + }); + + info("Stage 1"); + + const setRemoveMutations1 = [ + ["key0", "setRemove10"], + ["key1", "setRemove11"], + ["key2", null], + ["key3", "setRemove13"], + ["key4", "setRemove14"], + ["key5", "setRemove15"], + ["key6", "setRemove16"], + ["key7", "setRemove17"], + ["key8", null], + ["key9", "setRemove19"] + ]; + + const setRemoveState1 = { + key0: "setRemove10", + key1: "setRemove11", + key3: "setRemove13", + key4: "setRemove14", + key5: "setRemove15", + key6: "setRemove16", + key7: "setRemove17", + key9: "setRemove19" + }; + + const setRemoveMutations2 = [ + ["key0", "setRemove20"], + ["key1", null], + ["key2", "setRemove22"], + ["key3", "setRemove23"], + ["key4", "setRemove24"], + ["key5", "setRemove25"], + ["key6", "setRemove26"], + ["key7", null], + ["key8", "setRemove28"], + ["key9", "setRemove29"] + ]; + + const setRemoveState2 = { + key0: "setRemove20", + key2: "setRemove22", + key3: "setRemove23", + key4: "setRemove24", + key5: "setRemove25", + key6: "setRemove26", + key8: "setRemove28", + key9: "setRemove29" + }; + + // Apply initial mutations using an explicit snapshot. The explicit + // snapshot here ensures that the parent process have received the changes. + await beginExplicitSnapshot(writerTab1); + await applyMutations(writerTab1, initialMutations); + await endExplicitSnapshot(writerTab1); + + // Begin explicit snapshots in all tabs except readerTab2. All these tabs + // should see the initial state regardless what other tabs are doing. + await beginExplicitSnapshot(writerTab1); + await beginExplicitSnapshot(writerTab2); + await beginExplicitSnapshot(readerTab1); + + // Apply first array of set/remove mutations in writerTab1 and end the + // explicit snapshot. This will trigger saving of values in other active + // snapshots. + await applyMutations(writerTab1, setRemoveMutations1); + await endExplicitSnapshot(writerTab1); + + // Begin an explicit snapshot in readerTab2. writerTab1 already ended its + // explicit snapshot, so readerTab2 should see mutations done by + // writerTab1. + await beginExplicitSnapshot(readerTab2); + + // Apply second array of set/remove mutations in writerTab2 and end the + // explicit snapshot. This will trigger saving of values in other active + // snapshots, but only if they haven't been saved already. + await applyMutations(writerTab2, setRemoveMutations2); + await endExplicitSnapshot(writerTab2); + + // Verify state in readerTab1, it should match the initial state. + await verifyState(readerTab1, initialState); + await endExplicitSnapshot(readerTab1); + + // Verify state in readerTab2, it should match the state after the first + // array of set/remove mutatations have been applied and "commited". + await verifyState(readerTab2, setRemoveState1); + await endExplicitSnapshot(readerTab2); + + // Verify final state, it should match the state after the second array of + // set/remove mutation have been applied and "commited". An explicit + // snapshot is used. + await beginExplicitSnapshot(readerTab1); + await verifyState(readerTab1, setRemoveState2); + await endExplicitSnapshot(readerTab1); + + info("Stage 2"); + + const setRemoveClearMutations1 = [ + ["key0", "setRemoveClear10"], + ["key1", null], + [null, null] + ]; + + const setRemoveClearState1 = { + }; + + const setRemoveClearMutations2 = [ + ["key8", null], + ["key9", "setRemoveClear29"], + [null, null] + ]; + + const setRemoveClearState2 = { + }; + + // This is very similar to previous stage except that in addition to + // set/remove, the clear operation is involved too. + await beginExplicitSnapshot(writerTab1); + await applyMutations(writerTab1, initialMutations); + await endExplicitSnapshot(writerTab1); + + await beginExplicitSnapshot(writerTab1); + await beginExplicitSnapshot(writerTab2); + await beginExplicitSnapshot(readerTab1); + + await applyMutations(writerTab1, setRemoveClearMutations1); + await endExplicitSnapshot(writerTab1); + + await beginExplicitSnapshot(readerTab2); + + await applyMutations(writerTab2, setRemoveClearMutations2); + await endExplicitSnapshot(writerTab2); + + await verifyState(readerTab1, initialState); + await endExplicitSnapshot(readerTab1); + + await verifyState(readerTab2, setRemoveClearState1); + await endExplicitSnapshot(readerTab2); + + await beginExplicitSnapshot(readerTab1); + await verifyState(readerTab1, setRemoveClearState2); + await endExplicitSnapshot(readerTab1); + + info("Stage 3"); + + const changeOrderMutations = [ + ["key1", null], + ["key2", null], + ["key3", null], + ["key5", null], + ["key6", null], + ["key7", null], + ["key8", null], + ["key8", "initial8"], + ["key7", "initial7"], + ["key6", "initial6"], + ["key5", "initial5"], + ["key3", "initial3"], + ["key2", "initial2"], + ["key1", "initial1"] + ]; + + // Apply initial mutations using an explicit snapshot. The explicit + // snapshot here ensures that the parent process have received the changes. + await beginExplicitSnapshot(writerTab1); + await applyMutations(writerTab1, initialMutations); + await endExplicitSnapshot(writerTab1); + + // Begin explicit snapshots in all tabs except writerTab2 which is not used + // in this stage. All these tabs should see the initial order regardless + // what other tabs are doing. + await beginExplicitSnapshot(readerTab1); + await beginExplicitSnapshot(writerTab1); + await beginExplicitSnapshot(readerTab2); + + // Get all keys in readerTab1 and end the explicit snapshot. No mutations + // have been applied yet. + let tab1Keys = await getKeys(readerTab1); + await endExplicitSnapshot(readerTab1); + + // Apply mutations that change the order of keys and end the explicit + // snapshot. The state is unchanged. This will trigger saving of key order + // in other active snapshots, but only if the order hasn't been saved + // already. + await applyMutations(writerTab1, changeOrderMutations); + await endExplicitSnapshot(writerTab1); + + // Get all keys in readerTab2 and end the explicit snapshot. Change order + // mutations have been applied, but the order should stay unchanged. + let tab2Keys = await getKeys(readerTab2); + await endExplicitSnapshot(readerTab2); + + // Verify the key order is the same. + is(tab2Keys.length, tab1Keys.length, "Correct keys length"); + for (let i = 0; i < tab2Keys.length; i++) { + is(tab2Keys[i], tab1Keys[i], "Correct key"); + } + + // Verify final state, it should match the initial state since applied + // mutations only changed the key order. An explicit snapshot is used. + await beginExplicitSnapshot(readerTab1); + await verifyState(readerTab1, initialState); + await endExplicitSnapshot(readerTab1); + } + + // - Clean up. + await cleanupTabs(knownTabs); + + clearOrigin(); +}); diff --git a/dom/tests/browser/helper_localStorage_e10s.js b/dom/tests/browser/helper_localStorage_e10s.js new file mode 100644 index 000000000000..307d0e84c9c9 --- /dev/null +++ b/dom/tests/browser/helper_localStorage_e10s.js @@ -0,0 +1,72 @@ +/* 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/. */ + +// Simple tab wrapper abstracting our messaging mechanism; +class KnownTab { + constructor(name, tab) { + this.name = name; + this.tab = tab; + } + + cleanup() { + this.tab = null; + } +} + +// Simple data structure class to help us track opened tabs and their pids. +class KnownTabs { + constructor() { + this.byPid = new Map(); + this.byName = new Map(); + } + + cleanup() { + this.byPid = null; + this.byName = null; + } +} + +/** + * Open our helper page in a tab in its own content process, asserting that it + * really is in its own process. We initially load and wait for about:blank to + * load, and only then loadURI to our actual page. This is to ensure that + * LocalStorageManager has had an opportunity to be created and populate + * mOriginsHavingData. + * + * (nsGlobalWindow will reliably create LocalStorageManager as a side-effect of + * the unconditional call to nsGlobalWindow::PreloadLocalStorage. This will + * reliably create the StorageDBChild instance, and its corresponding + * StorageDBParent will send the set of origins when it is constructed.) + */ +async function openTestTabInOwnProcess(helperPageUrl, name, knownTabs) { + let realUrl = helperPageUrl + '?' + encodeURIComponent(name); + // Load and wait for about:blank. + let tab = await BrowserTestUtils.openNewForegroundTab({ + gBrowser, opening: 'about:blank', forceNewProcess: true + }); + let pid = tab.linkedBrowser.frameLoader.tabParent.osPid; + ok(!knownTabs.byName.has(name), "tab needs its own name: " + name); + ok(!knownTabs.byPid.has(pid), "tab needs to be in its own process: " + pid); + + let knownTab = new KnownTab(name, tab); + knownTabs.byPid.set(pid, knownTab); + knownTabs.byName.set(name, knownTab); + + // Now trigger the actual load of our page. + BrowserTestUtils.loadURI(tab.linkedBrowser, realUrl); + await BrowserTestUtils.browserLoaded(tab.linkedBrowser); + is(tab.linkedBrowser.frameLoader.tabParent.osPid, pid, "still same pid"); + return knownTab; +} + +/** + * Close all the tabs we opened. + */ +async function cleanupTabs(knownTabs) { + for (let knownTab of knownTabs.byName.values()) { + BrowserTestUtils.removeTab(knownTab.tab); + knownTab.cleanup(); + } + knownTabs.cleanup(); +} diff --git a/dom/tests/browser/page_localstorage_snapshotting_e10s.html b/dom/tests/browser/page_localstorage_snapshotting_e10s.html new file mode 100644 index 000000000000..ae8b685d02ed --- /dev/null +++ b/dom/tests/browser/page_localstorage_snapshotting_e10s.html @@ -0,0 +1,55 @@ + + + + + + +

+ diff --git a/dom/tests/mochitest/localstorage/frameQuota.html b/dom/tests/mochitest/localstorage/frameQuota.html index 2b6c8f9ddecd..5ff9855c99ad 100644 --- a/dom/tests/mochitest/localstorage/frameQuota.html +++ b/dom/tests/mochitest/localstorage/frameQuota.html @@ -44,14 +44,6 @@ function doStep() localStorage.removeItem(keyName); is(localStorage.getItem(keyName), null, "Key "+keyName+" removed"); break; - - case "checkclean": - is(localStorage.getItem(keyName), null, "Key "+keyName+" not present"); - break; - - case "checknotclean": - is(localStorage.getItem(keyName), "12345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890", "Key "+keyName+" is present"); - break; } break; diff --git a/dom/tests/mochitest/localstorage/frameQuotaSessionOnly.html b/dom/tests/mochitest/localstorage/frameQuotaSessionOnly.html index 68efde9b8b09..e687991faed4 100644 --- a/dom/tests/mochitest/localstorage/frameQuotaSessionOnly.html +++ b/dom/tests/mochitest/localstorage/frameQuotaSessionOnly.html @@ -44,14 +44,6 @@ function doStep() localStorage.removeItem(keyName); is(localStorage.getItem(keyName), null, "Key "+keyName+" removed"); break; - - case "checkclean": - is(localStorage.getItem(keyName), null, "Key "+keyName+" not present"); - break; - - case "checknotclean": - is(localStorage.getItem(keyName), "12345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890", "Key "+keyName+" is present"); - break; } break; diff --git a/dom/tests/mochitest/localstorage/localStorageCommon.js b/dom/tests/mochitest/localstorage/localStorageCommon.js index ecbc80f309a5..4bb00c197844 100644 --- a/dom/tests/mochitest/localstorage/localStorageCommon.js +++ b/dom/tests/mochitest/localstorage/localStorageCommon.js @@ -1,5 +1,12 @@ function localStorageFlush(cb) { + if (SpecialPowers.Services.lsm.nextGenLocalStorageEnabled) { + SimpleTest.executeSoon(function () { + cb(); + }); + return; + } + var ob = { observe : function(sub, top, dat) { @@ -11,27 +18,94 @@ function localStorageFlush(cb) notify("domstorage-test-flush-force"); } -function localStorageReload() +function localStorageReload(callback) { - notify("domstorage-test-reload"); -} + if (SpecialPowers.Services.lsm.nextGenLocalStorageEnabled) { + localStorage.close(); + let qms = SpecialPowers.Services.qms; + let principal = SpecialPowers.wrap(document).nodePrincipal; + let request = qms.resetStoragesForPrincipal(principal, "default", "ls"); + request.callback = SpecialPowers.wrapCallback(function() { + localStorage.open(); + callback(); + }); + return; + } -function localStorageFlushAndReload(cb) -{ - localStorageFlush(function() { - localStorageReload(); - cb(); + notify("domstorage-test-reload"); + SimpleTest.executeSoon(function () { + callback(); }); } -function localStorageClearAll() +function localStorageFlushAndReload(callback) { - os().notifyObservers(null, "cookie-changed", "cleared"); + if (SpecialPowers.Services.lsm.nextGenLocalStorageEnabled) { + localStorage.close(); + let qms = SpecialPowers.Services.qms; + let principal = SpecialPowers.wrap(document).nodePrincipal; + let request = qms.resetStoragesForPrincipal(principal, "default", "ls"); + request.callback = SpecialPowers.wrapCallback(function() { + localStorage.open(); + callback(); + }); + return; + } + + localStorageFlush(function() { + localStorageReload(callback); + }); } -function localStorageClearDomain(domain) +function localStorageClearAll(callback) { + if (SpecialPowers.Services.lsm.nextGenLocalStorageEnabled) { + let qms = SpecialPowers.Services.qms; + let ssm = SpecialPowers.Services.scriptSecurityManager; + + qms.getUsage(SpecialPowers.wrapCallback(function(request) { + if (request.resultCode != SpecialPowers.Cr.NS_OK) { + callback(); + return; + } + + let clearRequestCount = 0; + for (let item of request.result) { + let principal = ssm.createCodebasePrincipalFromOrigin(item.origin); + let clearRequest = + qms.clearStoragesForPrincipal(principal, "default", "ls"); + clearRequestCount++; + clearRequest.callback = SpecialPowers.wrapCallback(function() { + if (--clearRequestCount == 0) { + callback(); + } + }); + } + })); + return; + } + + os().notifyObservers(null, "cookie-changed", "cleared"); + SimpleTest.executeSoon(function () { + callback(); + }); +} + +function localStorageClearDomain(domain, callback) +{ + if (SpecialPowers.Services.lsm.nextGenLocalStorageEnabled) { + let qms = SpecialPowers.Services.qms; + let principal = SpecialPowers.wrap(document).nodePrincipal; + let request = qms.clearStoragesForPrincipal(principal, "default", "ls"); + let cb = SpecialPowers.wrapCallback(callback); + request.callback = cb; + return; + } + os().notifyObservers(null, "browser:purge-domain-data", domain); + SimpleTest.executeSoon(function () { + callback(); + }); } function os() @@ -49,5 +123,7 @@ function notify(top) */ function localStorageEnableTestingMode(cb) { - SpecialPowers.pushPrefEnv({ "set": [["dom.storage.testing", true]] }, cb); + SpecialPowers.pushPrefEnv({ set: [["dom.storage.testing", true], + ["dom.quotaManager.testing", true]] }, + cb); } diff --git a/dom/tests/mochitest/localstorage/mochitest.ini b/dom/tests/mochitest/localstorage/mochitest.ini index c62ab9f6d84f..48173aac3b72 100644 --- a/dom/tests/mochitest/localstorage/mochitest.ini +++ b/dom/tests/mochitest/localstorage/mochitest.ini @@ -43,7 +43,7 @@ skip-if = toolkit == 'android' #TIMED_OUT [test_localStorageQuota.html] skip-if = toolkit == 'android' #TIMED_OUT [test_localStorageQuotaSessionOnly.html] -skip-if = toolkit == 'android' || (verify && (os == 'linux' || os == 'win')) #TIMED_OUT +skip-if = toolkit == 'android' || (verify && (os == 'linux' || os == 'mac' || os == 'win')) #TIMED_OUT [test_localStorageQuotaSessionOnly2.html] skip-if = true # bug 1347690 [test_localStorageReplace.html] diff --git a/dom/tests/mochitest/localstorage/test_bug600307-DBOps.html b/dom/tests/mochitest/localstorage/test_bug600307-DBOps.html index dd1a7c2b5d07..8c3ae3f79301 100644 --- a/dom/tests/mochitest/localstorage/test_bug600307-DBOps.html +++ b/dom/tests/mochitest/localstorage/test_bug600307-DBOps.html @@ -84,10 +84,11 @@ function startTest() localStorage.setItem("item", "value"); localStorage.setItem("item2", "value2"); localStorage.clear(); - localStorageReload(); + localStorageReload(function() { is(localStorage.length, 0, "localStorage clean in case 4"); - if (SpecialPowers.Cc["@mozilla.org/xre/app-info;1"].getService( + if (!SpecialPowers.Services.lsm.nextGenLocalStorageEnabled && + SpecialPowers.Cc["@mozilla.org/xre/app-info;1"].getService( SpecialPowers.Ci.nsIXULRuntime).processType != SpecialPowers.Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT) { // Following tests cannot be run in a child/plugin process type SimpleTest.finish(); @@ -97,7 +98,7 @@ function startTest() // Cookies clean 1 localStorageFlush(function() { localStorage.setItem("item", "value"); - localStorageClearAll(); + localStorageClearAll(function() { is(localStorage.length, 0, "localStorage clean after cookies deletion"); localStorage.setItem("item2", "value2"); is(localStorage.getItem("item"), null, "Unexpected key 1, cookies delete"); @@ -110,7 +111,7 @@ function startTest() localStorage.clear(); localStorageFlush(function() { localStorage.setItem("item", "value"); - localStorageClearAll(); + localStorageClearAll(function() { is(localStorage.length, 0, "localStorage clean after cookies deletion 2"); localStorageFlushAndReload(function() { is(localStorage.length, 0, "localStorage clean after cookies deletion 2"); @@ -119,7 +120,7 @@ function startTest() // Domain clean 1 localStorageFlush(function() { localStorage.setItem("item", "value"); - localStorageClearDomain("test"); + localStorageClearDomain("test", function() { is(localStorage.length, 0, "localStorage clean after domain deletion"); localStorage.setItem("item2", "value2"); is(localStorage.getItem("item"), null, "Unexpected key 1, domain delete"); @@ -132,7 +133,7 @@ function startTest() localStorage.clear(); localStorageFlush(function() { localStorage.setItem("item", "value"); - localStorageClearDomain("test"); + localStorageClearDomain("test", function() { is(localStorage.length, 0, "localStorage clean after domain deletion 2"); localStorageFlushAndReload(function() { is(localStorage.length, 0, "localStorage clean after domain deletion 2"); @@ -157,6 +158,11 @@ function startTest() }); }); }); + }); + }); + }); + }); + }); } SimpleTest.waitForExplicitFinish(); diff --git a/dom/tests/mochitest/localstorage/test_cookieBlock.html b/dom/tests/mochitest/localstorage/test_cookieBlock.html index c95cbca49784..5e3cae6c8f90 100644 --- a/dom/tests/mochitest/localstorage/test_cookieBlock.html +++ b/dom/tests/mochitest/localstorage/test_cookieBlock.html @@ -30,6 +30,10 @@ function startTest() SimpleTest.waitForExplicitFinish(); +// Initialize storage before setting the cookie, otherwise we won't be testing +// the checks in setItem/getItem methods. +var storage = localStorage; + SpecialPowers.pushPermissions([{'type': 'cookie', 'allow': false, 'context': document}], startTest); diff --git a/dom/tests/mochitest/localstorage/test_cookieSession.html b/dom/tests/mochitest/localstorage/test_cookieSession.html index 1fdd6f4d5656..34989553509a 100644 --- a/dom/tests/mochitest/localstorage/test_cookieSession.html +++ b/dom/tests/mochitest/localstorage/test_cookieSession.html @@ -118,6 +118,13 @@ function startTest() { break; default: SimpleTest.waitForExplicitFinish(); + + if (SpecialPowers.Services.lsm.nextGenLocalStorageEnabled) { + ok(true, "Test ignored when the next gen local storage is enabled."); + SimpleTest.finish(); + return; + } + var iframe = document.createElement('iframe'); iframe.src = 'test_cookieSession.html?1'; document.body.appendChild(iframe); diff --git a/dom/tests/mochitest/localstorage/test_localStorageBaseSessionOnly.html b/dom/tests/mochitest/localstorage/test_localStorageBaseSessionOnly.html index c55a8e48d152..9b7a086e845c 100644 --- a/dom/tests/mochitest/localstorage/test_localStorageBaseSessionOnly.html +++ b/dom/tests/mochitest/localstorage/test_localStorageBaseSessionOnly.html @@ -9,6 +9,12 @@ function startTest() { + if (SpecialPowers.Services.lsm.nextGenLocalStorageEnabled) { + ok(true, "Test ignored when the next gen local storage is enabled."); + SimpleTest.finish(); + return; + } + SpecialPowers.pushPermissions([{'type': 'cookie', 'allow': SpecialPowers.Ci.nsICookiePermission.ACCESS_SESSION, 'context': document}], test1); } diff --git a/dom/tests/mochitest/localstorage/test_localStorageQuota.html b/dom/tests/mochitest/localstorage/test_localStorageQuota.html index 6aeefe6bcb9d..3ed723c4d27f 100644 --- a/dom/tests/mochitest/localstorage/test_localStorageQuota.html +++ b/dom/tests/mochitest/localstorage/test_localStorageQuota.html @@ -18,12 +18,15 @@ function doNextTest() switch (currentTest) { case 1: - slaveOrigin = "http://example.com"; + if (SpecialPowers.Services.lsm.nextGenLocalStorageEnabled) { + slaveOrigin = "http://test1.example.com"; + } else { + slaveOrigin = "http://example.com"; + } slave.location = slaveOrigin + slavePath + "frameQuota.html?add&A&success"; break; - // In subdomain now set another key with length 500 bytes, i.e. - // allocate 501 bytes + // Now set another key with length 500 bytes, i.e. allocate 501 bytes case 2: slaveOrigin = "http://test1.example.com"; slave.location = slaveOrigin + slavePath + "frameQuota.html?add&B&success"; @@ -44,16 +47,19 @@ function doNextTest() slave.location = slaveOrigin + slavePath + "frameQuota.html?add2&B&failure"; break; - // In a different subdomain try to set a new 500 bytes key - // and check we fail because we are over the quota + // Try to set a new 500 bytes key and check we fail because we are over the + // quota. case 5: - slaveOrigin = "https://test2.example.com"; + if (SpecialPowers.Services.lsm.nextGenLocalStorageEnabled) { + slaveOrigin = "http://test1.example.com"; + } else { + slaveOrigin = "https://test2.example.com"; + } slave.location = slaveOrigin + slavePath + "frameQuota.html?add&C&failure"; break; - // Remove from the second subdomain the second key, it must not fail - // This should release the allocated space of the quota assigned to - // example.com. + // Remove the second key, it must not fail. This should release the + // allocated space of the quota assigned to test1.example.com. case 6: slaveOrigin = "http://test1.example.com"; slave.location = slaveOrigin + slavePath + "frameQuota.html?remove&B&success"; @@ -61,14 +67,22 @@ function doNextTest() // Now try again to set 500 bytes key, it must succeed. case 7: - slaveOrigin = "https://test2.example.com"; + if (SpecialPowers.Services.lsm.nextGenLocalStorageEnabled) { + slaveOrigin = "http://test1.example.com"; + } else { + slaveOrigin = "https://test2.example.com"; + } slave.location = slaveOrigin + slavePath + "frameQuota.html?add&C&success"; break; case 8: - // Do a clean up... - slaveOrigin = "http://example.com"; - slave.location = slaveOrigin + slavePath + "frameQuota.html?clear"; + if (SpecialPowers.Services.lsm.nextGenLocalStorageEnabled) { + SimpleTest.executeSoon(doNextTest); + } else { + // Do a clean up... + slaveOrigin = "http://example.com"; + slave.location = slaveOrigin + slavePath + "frameQuota.html?clear"; + } break; case 9: @@ -78,9 +92,13 @@ function doNextTest() break; case 10: - // Do a clean up... - slaveOrigin = "https://test2.example.com"; - slave.location = slaveOrigin + slavePath + "frameQuota.html?clear"; + if (SpecialPowers.Services.lsm.nextGenLocalStorageEnabled) { + SimpleTest.executeSoon(doNextTest); + } else { + // Do a clean up... + slaveOrigin = "https://test2.example.com"; + slave.location = slaveOrigin + slavePath + "frameQuota.html?clear"; + } break; default: // end diff --git a/dom/tests/mochitest/localstorage/test_localStorageQuotaPrivateBrowsing_perwindowpb.html b/dom/tests/mochitest/localstorage/test_localStorageQuotaPrivateBrowsing_perwindowpb.html index 84147fa3cd89..03d5f9cd16ae 100644 --- a/dom/tests/mochitest/localstorage/test_localStorageQuotaPrivateBrowsing_perwindowpb.html +++ b/dom/tests/mochitest/localstorage/test_localStorageQuotaPrivateBrowsing_perwindowpb.html @@ -42,12 +42,15 @@ function doNextTest(aWindow) { // Initialy setup the quota to testing value of 1024B and // set a 500 bytes key with name length 1 (allocate 501 bytes) case 1: - slaveOrigin = "http://example.com"; + if (SpecialPowers.Services.lsm.nextGenLocalStorageEnabled) { + slaveOrigin = "http://test1.example.com"; + } else { + slaveOrigin = "http://example.com"; + } slave.location = slaveOrigin + slavePath + "frameQuota.html?add&A&success"; break; - // In subdomain now set another key with length 500 bytes, i.e. - // allocate 501 bytes + // Now set another key with length 500 bytes, i.e. allocate 501 bytes case 2: slaveOrigin = "http://test1.example.com"; slave.location = slaveOrigin + slavePath + "frameQuota.html?add&B&success"; @@ -68,16 +71,19 @@ function doNextTest(aWindow) { slave.location = slaveOrigin + slavePath + "frameQuota.html?add2&B&failure"; break; - // In a different subdomain try to set a new 500 bytes key - // and check we fail because we are over the quota + // Try to set a new 500 bytes key and check we fail because we are over the + // quota. case 5: - slaveOrigin = "https://test2.example.com"; + if (SpecialPowers.Services.lsm.nextGenLocalStorageEnabled) { + slaveOrigin = "http://test1.example.com"; + } else { + slaveOrigin = "https://test2.example.com"; + } slave.location = slaveOrigin + slavePath + "frameQuota.html?add&C&failure"; break; - // Remove from the second subdomain the second key, it must not fail - // This should release the allocated space of the quota assigned to - // example.com. + // Remove the second key, it must not fail. This should release the + // allocated space of the quota assigned to test1.example.com. case 6: slaveOrigin = "http://test1.example.com"; slave.location = slaveOrigin + slavePath + "frameQuota.html?remove&B&success"; @@ -85,30 +91,38 @@ function doNextTest(aWindow) { // Now try again to set 500 bytes key, it must succeed. case 7: - slaveOrigin = "https://test2.example.com"; + if (SpecialPowers.Services.lsm.nextGenLocalStorageEnabled) { + slaveOrigin = "http://test1.example.com"; + } else { + slaveOrigin = "https://test2.example.com"; + } slave.location = slaveOrigin + slavePath + "frameQuota.html?add&C&success"; break; case 8: - // Do a clean up... - // TODO Bug 455070, use just ?clear what invokes call - // of clear() in the target frame. W/o clear method we must - // call clear implemented as removeItem for each item in - // the localStorage. - slaveOrigin = "http://example.com"; - slave.location = slaveOrigin + slavePath + "frameQuota.html?clear&A&"; + if (SpecialPowers.Services.lsm.nextGenLocalStorageEnabled) { + SimpleTest.executeSoon(() => doNextTest(aWindow)); + } else { + // Do a clean up... + slaveOrigin = "http://example.com"; + slave.location = slaveOrigin + slavePath + "frameQuota.html?clear"; + } break; case 9: // Do a clean up... slaveOrigin = "http://test1.example.com"; - slave.location = slaveOrigin + slavePath + "frameQuota.html?clear&B&"; + slave.location = slaveOrigin + slavePath + "frameQuota.html?clear"; break; case 10: - // Do a clean up... - slaveOrigin = "https://test2.example.com"; - slave.location = slaveOrigin + slavePath + "frameQuota.html?clear&C&"; + if (SpecialPowers.Services.lsm.nextGenLocalStorageEnabled) { + SimpleTest.executeSoon(() => doNextTest(aWindow)); + } else { + // Do a clean up... + slaveOrigin = "https://test2.example.com"; + slave.location = slaveOrigin + slavePath + "frameQuota.html?clear"; + } break; default: diff --git a/dom/tests/mochitest/localstorage/test_localStorageQuotaSessionOnly.html b/dom/tests/mochitest/localstorage/test_localStorageQuotaSessionOnly.html index 585da7ceb9d9..643efcbeb73b 100644 --- a/dom/tests/mochitest/localstorage/test_localStorageQuotaSessionOnly.html +++ b/dom/tests/mochitest/localstorage/test_localStorageQuotaSessionOnly.html @@ -96,6 +96,12 @@ function doStep() SimpleTest.waitForExplicitFinish(); function startTest() { + if (SpecialPowers.Services.lsm.nextGenLocalStorageEnabled) { + ok(true, "Test ignored when the next gen local storage is enabled."); + SimpleTest.finish(); + return; + } + SpecialPowers.pushPermissions([{'type': 'cookie', 'allow': SpecialPowers.Ci.nsICookiePermission.ACCESS_SESSION, 'context': document}], function() { // Initialy setup the quota to testing value of 1024B and // set a 500 bytes key with name length 1 (allocate 501 bytes) diff --git a/dom/tests/mochitest/localstorage/test_localStorageSessionPrefOverride.html b/dom/tests/mochitest/localstorage/test_localStorageSessionPrefOverride.html index 97d7bb3b208f..57f9cacbdcac 100644 --- a/dom/tests/mochitest/localstorage/test_localStorageSessionPrefOverride.html +++ b/dom/tests/mochitest/localstorage/test_localStorageSessionPrefOverride.html @@ -8,6 +8,11 @@ const ACCEPT_SESSION = 2; add_task(async function() { + if (SpecialPowers.Services.lsm.nextGenLocalStorageEnabled) { + ok(true, "Test ignored when the next gen local storage is enabled."); + return; + } + await SpecialPowers.pushPrefEnv({"set": [["network.cookie.lifetimePolicy", ACCEPT_SESSION]]}); diff --git a/dom/webidl/Storage.webidl b/dom/webidl/Storage.webidl index b64974a287b2..3b35ada642d2 100644 --- a/dom/webidl/Storage.webidl +++ b/dom/webidl/Storage.webidl @@ -33,3 +33,41 @@ interface Storage { [ChromeOnly] readonly attribute boolean isSessionOnly; }; + +/** + * Testing methods that exist only for the benefit of automated glass-box + * testing. Will never be exposed to content at large and unlikely to be useful + * in a WebDriver context. + */ +partial interface Storage { + /** + * Does a security-check and ensures the underlying database has been opened + * without actually calling any database methods. (Read-only methods will + * have a similar effect but also impact the state of the snapshot.) + */ + [Throws, NeedsSubjectPrincipal, Pref="dom.storage.testing"] + void open(); + + /** + * Automatically ends any explicit snapshot and drops the reference to the + * underlying database, but does not otherwise perturb the database. + */ + [Throws, NeedsSubjectPrincipal, Pref="dom.storage.testing"] + void close(); + + /** + * Ensures the database has been opened and initiates an explicit snapshot. + * Snapshots are normally automatically ended and checkpointed back to the + * parent, but explicitly opened snapshots must be explicitly ended via + * `endExplicitSnapshot` or `close`. + */ + [Throws, NeedsSubjectPrincipal, Pref="dom.storage.testing"] + void beginExplicitSnapshot(); + + /** + * Ends the explicitly begun snapshot and retains the underlying database. + * Compare with `close` which also drops the reference to the database. + */ + [Throws, NeedsSubjectPrincipal, Pref="dom.storage.testing"] + void endExplicitSnapshot(); +}; diff --git a/editor/libeditor/tests/mochitest.ini b/editor/libeditor/tests/mochitest.ini index e3e67377e7df..c334d745d9aa 100644 --- a/editor/libeditor/tests/mochitest.ini +++ b/editor/libeditor/tests/mochitest.ini @@ -107,15 +107,15 @@ skip-if = toolkit == 'android' [test_bug596506.html] [test_bug597331.html] subsuite = clipboard -skip-if = toolkit == 'android' # Bug 718316 +skip-if = toolkit == 'android' && !e10s # Bug 718316 [test_bug597784.html] [test_bug599322.html] subsuite = clipboard -skip-if = toolkit == 'android' +skip-if = toolkit == 'android' && !e10s [test_bug599983.html] [test_bug600570.html] subsuite = clipboard -skip-if = toolkit == 'android' # Bug 718316 +skip-if = toolkit == 'android' && !e10s # Bug 718316 [test_bug602130.html] [test_bug603556.html] subsuite = clipboard @@ -185,6 +185,7 @@ subsuite = clipboard [test_bug796839.html] [test_bug830600.html] subsuite = clipboard +skip-if = toolkit == 'android' && e10s [test_bug832025.html] [test_bug850043.html] [test_bug857487.html] @@ -214,7 +215,7 @@ subsuite = clipboard [test_bug1140105.html] [test_bug1140617.html] subsuite = clipboard -skip-if = toolkit == 'android' # bug 1299578 +skip-if = toolkit == 'android' && !e10s # bug 1299578 [test_bug1153237.html] [test_bug1154791.html] skip-if = os == 'android' diff --git a/gfx/2d/2D.h b/gfx/2d/2D.h index 11704fb6b4ed..6c7d873dd5e8 100644 --- a/gfx/2d/2D.h +++ b/gfx/2d/2D.h @@ -1865,7 +1865,7 @@ public: private: static FT_Library mFTLibrary; - static Mutex* mFTLock; + static StaticMutex mFTLock; public: #endif diff --git a/gfx/2d/Factory.cpp b/gfx/2d/Factory.cpp index 8696fbed008d..125e9fc4a8b7 100644 --- a/gfx/2d/Factory.cpp +++ b/gfx/2d/Factory.cpp @@ -46,7 +46,6 @@ #include #include "HelpersD2D.h" #include "HelpersWinFonts.h" -#include "mozilla/Mutex.h" #endif #include "DrawTargetCapture.h" @@ -67,8 +66,6 @@ #ifdef MOZ_ENABLE_FREETYPE #include "ft2build.h" #include FT_FREETYPE_H - -#include "mozilla/Mutex.h" #endif #include "MainThreadUtils.h" @@ -219,7 +216,7 @@ int32_t LoggingPrefs::sGfxLogLevel = LOG_DEFAULT; #ifdef MOZ_ENABLE_FREETYPE FT_Library Factory::mFTLibrary = nullptr; -Mutex* Factory::mFTLock = nullptr; +StaticMutex Factory::mFTLock; #endif #ifdef WIN32 @@ -243,10 +240,6 @@ Factory::Init(const Config& aConfig) { MOZ_ASSERT(!sConfig); sConfig = new Config(aConfig); - -#ifdef MOZ_ENABLE_FREETYPE - mFTLock = new Mutex("Factory::mFTLock"); -#endif } void @@ -260,10 +253,6 @@ Factory::ShutDown() #ifdef MOZ_ENABLE_FREETYPE mFTLibrary = nullptr; - if (mFTLock) { - delete mFTLock; - mFTLock = nullptr; - } #endif } @@ -764,22 +753,19 @@ Factory::ReleaseFTLibrary(FT_Library aFTLibrary) void Factory::LockFTLibrary(FT_Library aFTLibrary) { - MOZ_ASSERT(mFTLock); - mFTLock->Lock(); + mFTLock.Lock(); } void Factory::UnlockFTLibrary(FT_Library aFTLibrary) { - MOZ_ASSERT(mFTLock); - mFTLock->Unlock(); + mFTLock.Unlock(); } FT_Face Factory::NewFTFace(FT_Library aFTLibrary, const char* aFileName, int aFaceIndex) { - MOZ_ASSERT(mFTLock); - MutexAutoLock lock(*mFTLock); + StaticMutexAutoLock lock(mFTLock); if (!aFTLibrary) { aFTLibrary = mFTLibrary; } @@ -793,8 +779,7 @@ Factory::NewFTFace(FT_Library aFTLibrary, const char* aFileName, int aFaceIndex) FT_Face Factory::NewFTFaceFromData(FT_Library aFTLibrary, const uint8_t* aData, size_t aDataSize, int aFaceIndex) { - MOZ_ASSERT(mFTLock); - MutexAutoLock lock(*mFTLock); + StaticMutexAutoLock lock(mFTLock); if (!aFTLibrary) { aFTLibrary = mFTLibrary; } @@ -808,23 +793,14 @@ Factory::NewFTFaceFromData(FT_Library aFTLibrary, const uint8_t* aData, size_t a void Factory::ReleaseFTFace(FT_Face aFace) { - // May be called during shutdown when the lock is already destroyed. - // However, there are no other threads using the face by this point, - // so it is safe to skip locking if the lock is not around. - if (mFTLock) { - mFTLock->Lock(); - } + StaticMutexAutoLock lock(mFTLock); FT_Done_Face(aFace); - if (mFTLock) { - mFTLock->Unlock(); - } } FT_Error Factory::LoadFTGlyph(FT_Face aFace, uint32_t aGlyphIndex, int32_t aFlags) { - MOZ_ASSERT(mFTLock); - MutexAutoLock lock(*mFTLock); + StaticMutexAutoLock lock(mFTLock); return FT_Load_Glyph(aFace, aGlyphIndex, aFlags); } #endif @@ -1028,7 +1004,7 @@ Factory::CreateScaledFontForDWriteFont(IDWriteFontFace* aFontFace, aStyle); } -#endif // XP_WIN +#endif // WIN32 #ifdef USE_SKIA_GPU already_AddRefed diff --git a/gfx/ipc/GPUParent.cpp b/gfx/ipc/GPUParent.cpp index 4452fea83fe2..6e6be98a54ac 100644 --- a/gfx/ipc/GPUParent.cpp +++ b/gfx/ipc/GPUParent.cpp @@ -38,6 +38,7 @@ #include "mozilla/webrender/RenderThread.h" #include "mozilla/webrender/WebRenderAPI.h" #include "mozilla/HangDetails.h" +#include "nscore.h" #include "nsDebugImpl.h" #include "nsIGfxInfo.h" #include "nsThreadManager.h" @@ -47,6 +48,8 @@ #include "VRManager.h" #include "VRManagerParent.h" #include "VsyncBridgeParent.h" +#include "cairo.h" +#include "skia/include/core/SkGraphics.h" #if defined(XP_WIN) # include "mozilla/gfx/DeviceManagerDx.h" # include @@ -54,6 +57,7 @@ #endif #ifdef MOZ_WIDGET_GTK # include +# include "skia/include/ports/SkTypeface_cairo.h" #endif #ifdef MOZ_GECKO_PROFILER #include "ChildProfilerController.h" @@ -220,6 +224,10 @@ GPUParent::RecvInit(nsTArray&& prefs, LayerTreeOwnerTracker::Get()->Map(map.layersId(), map.ownerId()); } + // We bypass gfxPlatform::Init, so we must initialize any relevant libraries + // here that would normally be initialized there. + SkGraphics::Init(); + #if defined(XP_WIN) if (gfxConfig::IsEnabled(Feature::D3D11_COMPOSITING)) { DeviceManagerDx::Get()->CreateCompositorDevices(); @@ -260,6 +268,8 @@ GPUParent::RecvInit(nsTArray&& prefs, FT_Library library = Factory::NewFTLibrary(); MOZ_ASSERT(library); Factory::SetFTLibrary(library); + + SkInitCairoFT(true); } #endif @@ -552,6 +562,14 @@ GPUParent::ActorDestroy(ActorDestroyReason aWhy) #endif Factory::ShutDown(); + + // We bypass gfxPlatform shutdown, so we must shutdown any libraries here + // that would normally be handled by it. +#ifdef NS_FREE_PERMANENT_DATA + SkGraphics::PurgeFontCache(); + cairo_debug_reset_static_data(); +#endif + #if defined(XP_WIN) DeviceManagerDx::Shutdown(); #endif diff --git a/gfx/ipc/moz.build b/gfx/ipc/moz.build index 2179010c6390..8bf2b2c97578 100644 --- a/gfx/ipc/moz.build +++ b/gfx/ipc/moz.build @@ -86,3 +86,5 @@ FINAL_LIBRARY = 'xul' CXXFLAGS += CONFIG['MOZ_CAIRO_CFLAGS'] CXXFLAGS += CONFIG['TK_CFLAGS'] + +LOCAL_INCLUDES += CONFIG['SKIA_INCLUDES'] diff --git a/gfx/layers/ipc/WebRenderMessages.ipdlh b/gfx/layers/ipc/WebRenderMessages.ipdlh index 0a7a83058a70..43a7711f9712 100644 --- a/gfx/layers/ipc/WebRenderMessages.ipdlh +++ b/gfx/layers/ipc/WebRenderMessages.ipdlh @@ -26,7 +26,6 @@ using mozilla::wr::BlobImageKey from "mozilla/webrender/WebRenderTypes.h"; using mozilla::wr::PipelineId from "mozilla/webrender/WebRenderTypes.h"; using mozilla::gfx::MaybeIntSize from "mozilla/gfx/Point.h"; using mozilla::LayoutDeviceRect from "Units.h"; -using mozilla::LayoutDeviceSize from "Units.h"; using mozilla::ImageIntRect from "Units.h"; using mozilla::gfx::Rect from "mozilla/gfx/Rect.h"; using class mozilla::gfx::Matrix4x4 from "mozilla/gfx/Matrix.h"; @@ -81,7 +80,7 @@ struct OpReleaseTextureOfImage { struct OpUpdateAsyncImagePipeline { PipelineId pipelineId; - LayoutDeviceSize scSize; + LayoutDeviceRect scBounds; Matrix4x4 scTransform; MaybeIntSize scaleToSize; ImageRendering filter; diff --git a/gfx/layers/wr/AsyncImagePipelineManager.cpp b/gfx/layers/wr/AsyncImagePipelineManager.cpp index 1fb192361465..c0d4c04a6fd0 100644 --- a/gfx/layers/wr/AsyncImagePipelineManager.cpp +++ b/gfx/layers/wr/AsyncImagePipelineManager.cpp @@ -195,7 +195,7 @@ AsyncImagePipelineManager::RemoveAsyncImagePipeline(const wr::PipelineId& aPipel void AsyncImagePipelineManager::UpdateAsyncImagePipeline(const wr::PipelineId& aPipelineId, - const LayoutDeviceSize& aSize, + const LayoutDeviceRect& aScBounds, const gfx::Matrix4x4& aScTransform, const gfx::MaybeIntSize& aScaleToSize, const wr::ImageRendering& aFilter, @@ -209,7 +209,7 @@ AsyncImagePipelineManager::UpdateAsyncImagePipeline(const wr::PipelineId& aPipel return; } pipeline->mInitialised = true; - pipeline->Update(aSize, + pipeline->Update(aScBounds, aScTransform, aScaleToSize, aFilter, @@ -417,10 +417,13 @@ AsyncImagePipelineManager::ApplyAsyncImageForPipeline(const wr::Epoch& aEpoch, } aPipeline->mIsChanged = false; - wr::DisplayListBuilder builder(aPipelineId, wr::ToLayoutSize(aPipeline->mSize)); + + wr::LayoutSize contentSize { aPipeline->mScBounds.Width(), aPipeline->mScBounds.Height() }; + wr::DisplayListBuilder builder(aPipelineId, contentSize); float opacity = 1.0f; Maybe referenceFrameId = builder.PushStackingContext( + wr::ToRoundedLayoutRect(aPipeline->mScBounds), nullptr, nullptr, &opacity, @@ -466,7 +469,7 @@ AsyncImagePipelineManager::ApplyAsyncImageForPipeline(const wr::Epoch& aEpoch, aSceneBuilderTxn.SetDisplayList( gfx::Color(0.f, 0.f, 0.f, 0.f), aEpoch, - aPipeline->mSize, + LayerSize(aPipeline->mScBounds.Width(), aPipeline->mScBounds.Height()), aPipelineId, builderContentSize, dl.dl_desc, dl.dl); } @@ -513,17 +516,17 @@ AsyncImagePipelineManager::SetEmptyDisplayList(const wr::PipelineId& aPipelineId auto& txn = pipeline->mImageHost->GetAsyncRef() ? aTxnForImageBridge : aTxn; wr::Epoch epoch = GetNextImageEpoch(); - wr::DisplayListBuilder builder(aPipelineId, wr::ToLayoutSize(pipeline->mSize)); + wr::LayoutSize contentSize { pipeline->mScBounds.Width(), pipeline->mScBounds.Height() }; + wr::DisplayListBuilder builder(aPipelineId, contentSize); wr::BuiltDisplayList dl; wr::LayoutSize builderContentSize; builder.Finalize(builderContentSize, dl); - txn.SetDisplayList( - gfx::Color(0.f, 0.f, 0.f, 0.f), - epoch, - pipeline->mSize, - aPipelineId, builderContentSize, - dl.dl_desc, dl.dl); + txn.SetDisplayList(gfx::Color(0.f, 0.f, 0.f, 0.f), + epoch, + LayerSize(pipeline->mScBounds.Width(), pipeline->mScBounds.Height()), + aPipelineId, builderContentSize, + dl.dl_desc, dl.dl); } void diff --git a/gfx/layers/wr/AsyncImagePipelineManager.h b/gfx/layers/wr/AsyncImagePipelineManager.h index feaaaecd44df..bfacab01ad61 100644 --- a/gfx/layers/wr/AsyncImagePipelineManager.h +++ b/gfx/layers/wr/AsyncImagePipelineManager.h @@ -89,7 +89,7 @@ public: wr::TransactionBuilder& aTxn); void UpdateAsyncImagePipeline(const wr::PipelineId& aPipelineId, - const LayoutDeviceSize& aSize, + const LayoutDeviceRect& aScBounds, const gfx::Matrix4x4& aScTransform, const gfx::MaybeIntSize& aScaleToSize, const wr::ImageRendering& aFilter, @@ -170,18 +170,18 @@ private: struct AsyncImagePipeline { AsyncImagePipeline(); - void Update(const LayoutDeviceSize& aSize, + void Update(const LayoutDeviceRect& aScBounds, const gfx::Matrix4x4& aScTransform, const gfx::MaybeIntSize& aScaleToSize, const wr::ImageRendering& aFilter, const wr::MixBlendMode& aMixBlendMode) { - mIsChanged |= mSize != aSize || + mIsChanged |= !mScBounds.IsEqualEdges(aScBounds) || mScTransform != aScTransform || mScaleToSize != aScaleToSize || mFilter != aFilter || mMixBlendMode != aMixBlendMode; - mSize = aSize; + mScBounds = aScBounds; mScTransform = aScTransform; mScaleToSize = aScaleToSize; mFilter = aFilter; @@ -191,7 +191,7 @@ private: bool mInitialised; bool mIsChanged; bool mUseExternalImage; - LayoutDeviceSize mSize; + LayoutDeviceRect mScBounds; gfx::Matrix4x4 mScTransform; gfx::MaybeIntSize mScaleToSize; wr::ImageRendering mFilter; diff --git a/gfx/layers/wr/ClipManager.cpp b/gfx/layers/wr/ClipManager.cpp index bd87eb70faf3..e61a57cca5e5 100644 --- a/gfx/layers/wr/ClipManager.cpp +++ b/gfx/layers/wr/ClipManager.cpp @@ -61,10 +61,15 @@ ClipManager::EndBuild() void ClipManager::BeginList(const StackingContextHelper& aStackingContext) { - if (aStackingContext.ReferenceFrameId()) { - PushOverrideForASR( - mItemClipStack.empty() ? nullptr : mItemClipStack.top().mASR, - aStackingContext.ReferenceFrameId().ref()); + if (aStackingContext.AffectsClipPositioning()) { + if (aStackingContext.ReferenceFrameId()) { + PushOverrideForASR( + mItemClipStack.empty() ? nullptr : mItemClipStack.top().mASR, + aStackingContext.ReferenceFrameId().ref()); + } else { + // Start a new cache + mCacheStack.emplace(); + } } ItemClips clips(nullptr, nullptr, false); @@ -81,9 +86,14 @@ ClipManager::EndList(const StackingContextHelper& aStackingContext) mItemClipStack.top().Unapply(mBuilder); mItemClipStack.pop(); - if (aStackingContext.ReferenceFrameId()) { - PopOverrideForASR( + if (aStackingContext.AffectsClipPositioning()) { + if (aStackingContext.ReferenceFrameId()) { + PopOverrideForASR( mItemClipStack.empty() ? nullptr : mItemClipStack.top().mASR); + } else { + MOZ_ASSERT(!mCacheStack.empty()); + mCacheStack.pop(); + } } } diff --git a/gfx/layers/wr/StackingContextHelper.cpp b/gfx/layers/wr/StackingContextHelper.cpp index 13d8b004fcbc..7ede1042b034 100644 --- a/gfx/layers/wr/StackingContextHelper.cpp +++ b/gfx/layers/wr/StackingContextHelper.cpp @@ -15,6 +15,7 @@ namespace layers { StackingContextHelper::StackingContextHelper() : mBuilder(nullptr) , mScale(1.0f, 1.0f) + , mAffectsClipPositioning(false) , mIsPreserve3D(false) , mRasterizeLocally(false) { @@ -25,10 +26,10 @@ StackingContextHelper::StackingContextHelper(const StackingContextHelper& aParen const ActiveScrolledRoot* aAsr, wr::DisplayListBuilder& aBuilder, const nsTArray& aFilters, + const LayoutDeviceRect& aBounds, const gfx::Matrix4x4* aBoundTransform, const wr::WrAnimationProperty* aAnimation, const float* aOpacityPtr, - const LayoutDevicePoint& aOrigin, const gfx::Matrix4x4* aTransformPtr, const gfx::Matrix4x4* aPerspectivePtr, const gfx::CompositionOp& aMixBlendMode, @@ -39,15 +40,10 @@ StackingContextHelper::StackingContextHelper(const StackingContextHelper& aParen bool aAnimated) : mBuilder(&aBuilder) , mScale(1.0f, 1.0f) - , mInheritedStickyOrigin(aOrigin) , mDeferredTransformItem(aDeferredTransformItem) , mIsPreserve3D(aIsPreserve3D) , mRasterizeLocally(aAnimated || aParentSC.mRasterizeLocally) { - if (aOrigin != LayoutDevicePoint()) { - mOriginFrameId = Some(mBuilder->PushOrigin(wr::ToLayoutPoint(aOrigin))); - } - // Compute scale for fallback rendering. We don't try to guess a scale for 3d // transformed items gfx::Matrix transform2d; @@ -71,6 +67,7 @@ StackingContextHelper::StackingContextHelper(const StackingContextHelper& aParen : wr::RasterSpace::Screen(); mReferenceFrameId = mBuilder->PushStackingContext( + wr::ToLayoutRect(aBounds), aClipNodeId, aAnimation, aOpacityPtr, @@ -82,6 +79,9 @@ StackingContextHelper::StackingContextHelper(const StackingContextHelper& aParen aBackfaceVisible, rasterSpace); + mAffectsClipPositioning = mReferenceFrameId.isSome() || + (aBounds.TopLeft() != LayoutDevicePoint()); + // If the parent stacking context has a deferred transform item, inherit it // into this stacking context, as long as the ASR hasn't changed. Refer to // the comments on StackingContextHelper::mDeferredTransformItem for an @@ -99,29 +99,15 @@ StackingContextHelper::StackingContextHelper(const StackingContextHelper& aParen mDeferredAncestorTransform = aParentSC.mDeferredAncestorTransform; } } - - // Update the origin for use by sticky frames. - if (!mReferenceFrameId) { - mInheritedStickyOrigin += aParentSC.mInheritedStickyOrigin; - } } StackingContextHelper::~StackingContextHelper() { if (mBuilder) { mBuilder->PopStackingContext(mReferenceFrameId.isSome()); - if (mOriginFrameId) { - mBuilder->PopOrigin(); - } } } -Maybe -StackingContextHelper::ReferenceFrameId() const -{ - return mReferenceFrameId ? mReferenceFrameId : mOriginFrameId; -} - const Maybe& StackingContextHelper::GetDeferredTransformItem() const { diff --git a/gfx/layers/wr/StackingContextHelper.h b/gfx/layers/wr/StackingContextHelper.h index 1b5996226784..9deaf4468817 100644 --- a/gfx/layers/wr/StackingContextHelper.h +++ b/gfx/layers/wr/StackingContextHelper.h @@ -32,10 +32,10 @@ public: const ActiveScrolledRoot* aAsr, wr::DisplayListBuilder& aBuilder, const nsTArray& aFilters = nsTArray(), + const LayoutDeviceRect& aBounds = LayoutDeviceRect(), const gfx::Matrix4x4* aBoundTransform = nullptr, const wr::WrAnimationProperty* aAnimation = nullptr, const float* aOpacityPtr = nullptr, - const LayoutDevicePoint& aOrigin = LayoutDevicePoint(), const gfx::Matrix4x4* aTransformPtr = nullptr, const gfx::Matrix4x4* aPerspectivePtr = nullptr, const gfx::CompositionOp& aMixBlendMode = gfx::CompositionOp::OP_OVER, @@ -69,30 +69,22 @@ public: const Maybe& GetDeferredTransformItem() const; Maybe GetDeferredTransformMatrix() const; - Maybe ReferenceFrameId() const; - - const LayoutDevicePoint& GetInheritedStickyOrigin() const { - return mInheritedStickyOrigin; - } + bool AffectsClipPositioning() const { return mAffectsClipPositioning; } + Maybe ReferenceFrameId() const { return mReferenceFrameId; } private: wr::DisplayListBuilder* mBuilder; gfx::Size mScale; gfx::Matrix mInheritedTransform; - // A stacking context may insert a special WR reference frame if we have - // origin provided. It only affects sticky frames below it, which need to - // compensate for the origin when computing the viewport. - LayoutDevicePoint mInheritedStickyOrigin; - // The "snapping surface" defines the space that we want to snap in. // You can think of it as the nearest physical surface. // Animated transforms create a new snapping surface, so that changes to their transform don't affect the snapping of their contents. // Non-animated transforms do *not* create a new snapping surface, // so that for example the existence of a non-animated identity transform does not affect snapping. gfx::Matrix mSnappingSurfaceTransform; + bool mAffectsClipPositioning; Maybe mReferenceFrameId; - Maybe mOriginFrameId; // The deferred transform item is used when building the WebRenderScrollData // structure. The backstory is that APZ needs to know about transforms that diff --git a/gfx/layers/wr/WebRenderBridgeParent.cpp b/gfx/layers/wr/WebRenderBridgeParent.cpp index c6994dec9c65..34bb2c7b79cd 100644 --- a/gfx/layers/wr/WebRenderBridgeParent.cpp +++ b/gfx/layers/wr/WebRenderBridgeParent.cpp @@ -994,7 +994,7 @@ WebRenderBridgeParent::RecvSetDisplayList(const gfx::IntSize& aSize, txn.SetWindowParameters(widgetSize, docRect); } gfx::Color clearColor(0.f, 0.f, 0.f, 0.f); - txn.SetDisplayList(clearColor, wrEpoch, LayoutDeviceSize(aSize.width, aSize.height), + txn.SetDisplayList(clearColor, wrEpoch, LayerSize(aSize.width, aSize.height), mPipelineId, aContentSize, dlDesc, dlData); @@ -1233,7 +1233,7 @@ WebRenderBridgeParent::ProcessWebRenderParentCommands(const InfallibleTArrayUpdateAsyncImagePipeline(op.pipelineId(), - op.scSize(), + op.scBounds(), op.scTransform(), op.scaleToSize(), op.filter(), diff --git a/gfx/layers/wr/WebRenderCommandBuilder.cpp b/gfx/layers/wr/WebRenderCommandBuilder.cpp index fbc00c0bfdca..296c993d0448 100644 --- a/gfx/layers/wr/WebRenderCommandBuilder.cpp +++ b/gfx/layers/wr/WebRenderCommandBuilder.cpp @@ -1149,7 +1149,7 @@ Grouper::ConstructItemInsideInactive(WebRenderCommandBuilder* aCommandBuilder, // we compute the geometry change here because we have the transform around still aGroup->ComputeGeometryChange(aItem, data, mTransform, mDisplayListBuilder); - + // Temporarily restrict the image bounds to the bounds of the container so that // clipped children within the container know about the clip. IntRect oldImageBounds = aGroup->mImageBounds; @@ -1362,7 +1362,7 @@ WebRenderCommandBuilder::BuildWebRenderCommands(wr::DisplayListBuilder& aBuilder } StackingContextHelper pageRootSc(sc, nullptr, aBuilder, aFilters, - nullptr, mZoomProp.ptrOr(nullptr)); + LayoutDeviceRect(), nullptr, mZoomProp.ptrOr(nullptr)); if (ShouldDumpDisplayList(aDisplayListBuilder)) { mBuilderDumpIndex = aBuilder.Dump(mDumpIndent + 1, Some(mBuilderDumpIndex), Nothing()); } @@ -1624,6 +1624,7 @@ WebRenderCommandBuilder::CreateImageKey(nsDisplayItem* aItem, MOZ_ASSERT(aAsyncImageBounds); LayoutDeviceRect rect = aAsyncImageBounds.value(); + LayoutDeviceRect scBounds(LayoutDevicePoint(0, 0), rect.Size()); gfx::MaybeIntSize scaleToSize; if (!aContainer->GetScaleHint().IsEmpty()) { scaleToSize = Some(aContainer->GetScaleHint()); @@ -1636,6 +1637,7 @@ WebRenderCommandBuilder::CreateImageKey(nsDisplayItem* aItem, aContainer, aSc, rect, + scBounds, transform, scaleToSize, aRendering, diff --git a/gfx/layers/wr/WebRenderUserData.cpp b/gfx/layers/wr/WebRenderUserData.cpp index aafcface38ab..352d4065f4b3 100644 --- a/gfx/layers/wr/WebRenderUserData.cpp +++ b/gfx/layers/wr/WebRenderUserData.cpp @@ -229,6 +229,7 @@ WebRenderImageData::CreateAsyncImageWebRenderCommands(mozilla::wr::DisplayListBu ImageContainer* aContainer, const StackingContextHelper& aSc, const LayoutDeviceRect& aBounds, + const LayoutDeviceRect& aSCBounds, const gfx::Matrix4x4& aSCTransform, const gfx::MaybeIntSize& aScaleToSize, const wr::ImageRendering& aFilter, @@ -262,21 +263,10 @@ WebRenderImageData::CreateAsyncImageWebRenderCommands(mozilla::wr::DisplayListBu // where it will be done when we build the display list for the iframe. // That happens in AsyncImagePipelineManager. wr::LayoutRect r = wr::ToRoundedLayoutRect(aBounds); - - Maybe originFrameId; - if (r.origin.x != 0.0 || r.origin.y != 0.0) { - originFrameId = Some(aBuilder.PushOrigin(r.origin)); - r.origin = wr::LayoutPoint { 0.0, 0.0 }; - } - aBuilder.PushIFrame(r, aIsBackfaceVisible, mPipelineId.ref(), /*ignoreMissingPipelines*/ false); - if (originFrameId) { - aBuilder.PopOrigin(); - } - WrBridge()->AddWebRenderParentCommand(OpUpdateAsyncImagePipeline(mPipelineId.value(), - aBounds.Size(), + aSCBounds, aSCTransform, aScaleToSize, aFilter, @@ -306,6 +296,7 @@ WebRenderFallbackData::WebRenderFallbackData(WebRenderLayerManager* aWRManager, WebRenderFallbackData::~WebRenderFallbackData() { + ClearImageKey(); } nsDisplayItemGeometry* diff --git a/gfx/layers/wr/WebRenderUserData.h b/gfx/layers/wr/WebRenderUserData.h index dc281aaf93f3..a91cd6ced4f9 100644 --- a/gfx/layers/wr/WebRenderUserData.h +++ b/gfx/layers/wr/WebRenderUserData.h @@ -139,6 +139,7 @@ public: ImageContainer* aContainer, const StackingContextHelper& aSc, const LayoutDeviceRect& aBounds, + const LayoutDeviceRect& aSCBounds, const gfx::Matrix4x4& aSCTransform, const gfx::MaybeIntSize& aScaleToSize, const wr::ImageRendering& aFilter, diff --git a/gfx/webrender_bindings/WebRenderAPI.cpp b/gfx/webrender_bindings/WebRenderAPI.cpp index 2e97083dde9a..38cf26e3d778 100644 --- a/gfx/webrender_bindings/WebRenderAPI.cpp +++ b/gfx/webrender_bindings/WebRenderAPI.cpp @@ -189,7 +189,7 @@ TransactionBuilder::RemovePipeline(PipelineId aPipelineId) void TransactionBuilder::SetDisplayList(gfx::Color aBgColor, Epoch aEpoch, - LayoutDeviceSize aViewportSize, + mozilla::LayerSize aViewportSize, wr::WrPipelineId pipeline_id, const wr::LayoutSize& content_size, wr::BuiltDisplayListDescriptor dl_descriptor, @@ -870,29 +870,9 @@ DisplayListBuilder::Finalize(wr::LayoutSize& aOutContentSize, &aOutDisplayList.dl.inner); } -wr::WrClipId -DisplayListBuilder::PushOrigin(const wr::LayoutPoint& aOrigin) -{ - WRDL_LOG("PushOrigin t=%s\n", mWrState, - Stringify(aOrigin).c_str()); - - //Note: there could be a simpler way to convert LayoutPoint -> LayoutTransform - wr::LayoutTransform transform = ToLayoutTransform( - gfx::Matrix4x4::Translation(aOrigin.x, aOrigin.y, 0.0)); - - auto id = wr_dp_push_reference_frame(mWrState, &transform); - return wr::WrClipId { id }; -} - -void -DisplayListBuilder::PopOrigin() -{ - WRDL_LOG("PopOrigin\n", mWrState); - wr_dp_pop_reference_frame(mWrState); -} - Maybe -DisplayListBuilder::PushStackingContext(const wr::WrClipId* aClipNodeId, +DisplayListBuilder::PushStackingContext(const wr::LayoutRect& aBounds, + const wr::WrClipId* aClipNodeId, const WrAnimationProperty* aAnimation, const float* aOpacity, const gfx::Matrix4x4* aTransform, @@ -915,13 +895,14 @@ DisplayListBuilder::PushStackingContext(const wr::WrClipId* aClipNodeId, if (aPerspective) { perspective = ToLayoutTransform(*aPerspective); } + const wr::LayoutTransform* maybePerspective = aPerspective ? &perspective : nullptr; - WRDL_LOG("PushStackingContext t=%s\n", mWrState, + WRDL_LOG("PushStackingContext b=%s t=%s\n", mWrState, Stringify(aBounds).c_str(), aTransform ? Stringify(*aTransform).c_str() : "none"); bool outIsReferenceFrame = false; uintptr_t outReferenceFrameId = 0; - wr_dp_push_stacking_context(mWrState, aClipNodeId, aAnimation, + wr_dp_push_stacking_context(mWrState, aBounds, aClipNodeId, aAnimation, aOpacity, maybeTransform, aTransformStyle, maybePerspective, aMixBlendMode, aFilters.Elements(), aFilters.Length(), diff --git a/gfx/webrender_bindings/WebRenderAPI.h b/gfx/webrender_bindings/WebRenderAPI.h index 526d8c073dd0..575366c5be82 100644 --- a/gfx/webrender_bindings/WebRenderAPI.h +++ b/gfx/webrender_bindings/WebRenderAPI.h @@ -87,7 +87,7 @@ public: void SetDisplayList(gfx::Color aBgColor, Epoch aEpoch, - LayoutDeviceSize aViewportSize, + mozilla::LayerSize aViewportSize, wr::WrPipelineId pipeline_id, const wr::LayoutSize& content_size, wr::BuiltDisplayListDescriptor dl_descriptor, @@ -335,10 +335,8 @@ public: void Finalize(wr::LayoutSize& aOutContentSize, wr::BuiltDisplayList& aOutDisplayList); - wr::WrClipId PushOrigin(const wr::LayoutPoint& aOrigin); - void PopOrigin(); - Maybe PushStackingContext( + const wr::LayoutRect& aBounds, // TODO: We should work with strongly typed rects const wr::WrClipId* aClipNodeId, const wr::WrAnimationProperty* aAnimation, const float* aOpacity, diff --git a/gfx/webrender_bindings/src/bindings.rs b/gfx/webrender_bindings/src/bindings.rs index 3d2854166dce..3d5e6b4e5311 100644 --- a/gfx/webrender_bindings/src/bindings.rs +++ b/gfx/webrender_bindings/src/bindings.rs @@ -1853,32 +1853,9 @@ pub extern "C" fn wr_dp_clear_save(state: &mut WrState) { state.frame_builder.dl_builder.clear_save(); } -#[no_mangle] -pub extern "C" fn wr_dp_push_reference_frame(state: &mut WrState, - transform: &LayoutTransform) -> usize { - debug_assert!(unsafe { !is_in_render_thread() }); - - let perspective = None; - let ref_frame_id = state.frame_builder.dl_builder.push_reference_frame( - &LayoutPrimitiveInfo::new(LayoutRect::zero()), - Some(PropertyBinding::Value(*transform)), - perspective, - ); - - state.frame_builder.dl_builder.push_clip_id(ref_frame_id); - pack_clip_id(ref_frame_id) -} - -#[no_mangle] -pub extern "C" fn wr_dp_pop_reference_frame(state: &mut WrState) { - debug_assert!(unsafe { !is_in_render_thread() }); - - state.frame_builder.dl_builder.pop_clip_id(); - state.frame_builder.dl_builder.pop_reference_frame(); -} - #[no_mangle] pub extern "C" fn wr_dp_push_stacking_context(state: &mut WrState, + bounds: LayoutRect, clip_node_id: *const WrClipId, animation: *const WrAnimationProperty, opacity: *const f32, @@ -1915,54 +1892,56 @@ pub extern "C" fn wr_dp_push_stacking_context(state: &mut WrState, } }).collect(); - let clip_node_id = match unsafe { clip_node_id.as_ref() } { + let clip_node_id_ref = unsafe { clip_node_id.as_ref() }; + let clip_node_id = match clip_node_id_ref { Some(clip_node_id) => Some(unpack_clip_id(*clip_node_id, state.pipeline_id)), None => None, }; - let transform = unsafe { transform.as_ref() }.cloned(); - let mut transform_binding = transform.map(PropertyBinding::Value); - let opacity = unsafe { opacity.as_ref() }.cloned(); + let transform_ref = unsafe { transform.as_ref() }; + let mut transform_binding = match transform_ref { + Some(transform) => Some(PropertyBinding::Value(transform.clone())), + None => None, + }; + let opacity_ref = unsafe { opacity.as_ref() }; let mut has_opacity_animation = false; - if let Some(anim) = unsafe { animation.as_ref() } { + let anim = unsafe { animation.as_ref() }; + if let Some(anim) = anim { debug_assert!(anim.id > 0); match anim.effect_type { WrAnimationType::Opacity => { - filters.push(FilterOp::Opacity( - PropertyBinding::Binding( - PropertyBindingKey::new(anim.id), - // We have to set the static opacity value as - // the value for the case where the animation is - // in not in-effect (e.g. in the delay phase - // with no corresponding fill mode). - opacity.unwrap_or(1.0), - ), - 1.0, - )); + filters.push(FilterOp::Opacity(PropertyBinding::Binding(PropertyBindingKey::new(anim.id), + // We have to set the static opacity value as + // the value for the case where the animation is + // in not in-effect (e.g. in the delay phase + // with no corresponding fill mode). + opacity_ref.cloned().unwrap_or(1.0)), + 1.0)); has_opacity_animation = true; }, WrAnimationType::Transform => { transform_binding = - Some(PropertyBinding::Binding( - PropertyBindingKey::new(anim.id), - // Same as above opacity case. - transform.unwrap_or(LayoutTransform::identity()), - )); + Some(PropertyBinding::Binding(PropertyBindingKey::new(anim.id), + // Same as above opacity case. + transform_ref.cloned().unwrap_or(LayoutTransform::identity()))); }, } } - if let Some(opacity) = opacity { - if !has_opacity_animation && opacity < 1.0 { - filters.push(FilterOp::Opacity(PropertyBinding::Value(opacity), opacity)); + if let Some(opacity) = opacity_ref { + if !has_opacity_animation && *opacity < 1.0 { + filters.push(FilterOp::Opacity(PropertyBinding::Value(*opacity), *opacity)); } } - let perspective = unsafe { perspective.as_ref() }.cloned(); - // The only field of primitive info currently used by WR for stacking contexts - // is backface visibility. Layout rectangles don't matter. - let mut prim_info = LayoutPrimitiveInfo::new(LayoutRect::zero()); + let perspective_ref = unsafe { perspective.as_ref() }; + let perspective = match perspective_ref { + Some(perspective) => Some(perspective.clone()), + None => None, + }; + + let mut prim_info = LayoutPrimitiveInfo::new(bounds); *out_is_reference_frame = transform_binding.is_some() || perspective.is_some(); if *out_is_reference_frame { @@ -1971,27 +1950,28 @@ pub extern "C" fn wr_dp_push_stacking_context(state: &mut WrState, .push_reference_frame(&prim_info, transform_binding, perspective); *out_reference_frame_id = pack_clip_id(ref_frame_id); + prim_info.rect.origin = LayoutPoint::zero(); + prim_info.clip_rect.origin = LayoutPoint::zero(); state.frame_builder.dl_builder.push_clip_id(ref_frame_id); } prim_info.is_backface_visible = is_backface_visible; prim_info.tag = state.current_tag; - state.frame_builder.dl_builder.push_stacking_context( - &prim_info, - clip_node_id, - transform_style, - mix_blend_mode, - &filters, - glyph_raster_space, - ); + state.frame_builder + .dl_builder + .push_stacking_context(&prim_info, + clip_node_id, + transform_style, + mix_blend_mode, + &filters, + glyph_raster_space); } #[no_mangle] pub extern "C" fn wr_dp_pop_stacking_context(state: &mut WrState, is_reference_frame: bool) { debug_assert!(unsafe { !is_in_render_thread() }); - state.frame_builder.dl_builder.pop_stacking_context(); if is_reference_frame { state.frame_builder.dl_builder.pop_clip_id(); @@ -2006,7 +1986,6 @@ pub extern "C" fn wr_dp_define_clipchain(state: &mut WrState, clips_count: usize) -> u64 { debug_assert!(unsafe { is_in_main_thread() }); - let parent = unsafe { parent_clipchain_id.as_ref() }.map(|id| ClipChainId(*id, state.pipeline_id)); let pipeline_id = state.pipeline_id; let clips = make_slice(clips, clips_count) diff --git a/gfx/webrender_bindings/webrender_ffi_generated.h b/gfx/webrender_bindings/webrender_ffi_generated.h index 25b8d23a7a91..2da5db1a1e51 100644 --- a/gfx/webrender_bindings/webrender_ffi_generated.h +++ b/gfx/webrender_bindings/webrender_ffi_generated.h @@ -816,6 +816,28 @@ struct GradientStop { } }; +struct Shadow { + LayoutVector2D offset; + ColorF color; + float blur_radius; + + bool operator==(const Shadow& aOther) const { + return offset == aOther.offset && + color == aOther.color && + blur_radius == aOther.blur_radius; + } +}; + +struct WrAnimationProperty { + WrAnimationType effect_type; + uint64_t id; + + bool operator==(const WrAnimationProperty& aOther) const { + return effect_type == aOther.effect_type && + id == aOther.id; + } +}; + // A 3d transform stored as a 4 by 4 matrix in row-major order in memory. // // Transforms can be parametrized over the source and destination units, to describe a @@ -868,28 +890,6 @@ struct TypedTransform3D { using LayoutTransform = TypedTransform3D; -struct Shadow { - LayoutVector2D offset; - ColorF color; - float blur_radius; - - bool operator==(const Shadow& aOther) const { - return offset == aOther.offset && - color == aOther.color && - blur_radius == aOther.blur_radius; - } -}; - -struct WrAnimationProperty { - WrAnimationType effect_type; - uint64_t id; - - bool operator==(const WrAnimationProperty& aOther) const { - return effect_type == aOther.effect_type && - id == aOther.id; - } -}; - struct WrFilterOp { WrFilterOpType filter_type; float argument; @@ -1359,10 +1359,6 @@ WR_INLINE void wr_dp_pop_clip_and_scroll_info(WrState *aState) WR_FUNC; -WR_INLINE -void wr_dp_pop_reference_frame(WrState *aState) -WR_FUNC; - WR_INLINE void wr_dp_pop_scroll_layer(WrState *aState) WR_FUNC; @@ -1532,11 +1528,6 @@ void wr_dp_push_rect(WrState *aState, ColorF aColor) WR_FUNC; -WR_INLINE -uintptr_t wr_dp_push_reference_frame(WrState *aState, - const LayoutTransform *aTransform) -WR_FUNC; - WR_INLINE void wr_dp_push_scroll_layer(WrState *aState, WrClipId aScrollId) @@ -1552,6 +1543,7 @@ WR_FUNC; WR_INLINE void wr_dp_push_stacking_context(WrState *aState, + LayoutRect aBounds, const WrClipId *aClipNodeId, const WrAnimationProperty *aAnimation, const float *aOpacity, diff --git a/ipc/glue/BackgroundChild.h b/ipc/glue/BackgroundChild.h index 34590c23c75c..efecb67951b5 100644 --- a/ipc/glue/BackgroundChild.h +++ b/ipc/glue/BackgroundChild.h @@ -10,6 +10,8 @@ #include "mozilla/Attributes.h" #include "mozilla/ipc/Transport.h" +class nsIEventTarget; + namespace mozilla { namespace dom { @@ -55,7 +57,7 @@ public: // See above. static PBackgroundChild* - GetOrCreateForCurrentThread(); + GetOrCreateForCurrentThread(nsIEventTarget* aMainEventTarget = nullptr); // See above. static void diff --git a/ipc/glue/BackgroundChildImpl.cpp b/ipc/glue/BackgroundChildImpl.cpp index d284323c3b7e..0215c7e32fc3 100644 --- a/ipc/glue/BackgroundChildImpl.cpp +++ b/ipc/glue/BackgroundChildImpl.cpp @@ -16,6 +16,10 @@ #include "mozilla/Assertions.h" #include "mozilla/SchedulerGroup.h" #include "mozilla/dom/ClientManagerActors.h" +#include "mozilla/dom/PBackgroundLSDatabaseChild.h" +#include "mozilla/dom/PBackgroundLSObserverChild.h" +#include "mozilla/dom/PBackgroundLSRequestChild.h" +#include "mozilla/dom/PBackgroundLSSimpleRequestChild.h" #include "mozilla/dom/PBackgroundSDBConnectionChild.h" #include "mozilla/dom/PFileSystemRequestChild.h" #include "mozilla/dom/FileSystemTaskBase.h" @@ -223,6 +227,77 @@ BackgroundChildImpl::DeallocPBackgroundIndexedDBUtilsChild( return true; } +BackgroundChildImpl::PBackgroundSDBConnectionChild* +BackgroundChildImpl::AllocPBackgroundSDBConnectionChild( + const PrincipalInfo& aPrincipalInfo) +{ + MOZ_CRASH("PBackgroundSDBConnectionChild actor should be manually " + "constructed!"); +} + +bool +BackgroundChildImpl::DeallocPBackgroundSDBConnectionChild( + PBackgroundSDBConnectionChild* aActor) +{ + MOZ_ASSERT(aActor); + + delete aActor; + return true; +} + +BackgroundChildImpl::PBackgroundLSDatabaseChild* +BackgroundChildImpl::AllocPBackgroundLSDatabaseChild( + const PrincipalInfo& aPrincipalInfo, + const uint32_t& aPrivateBrowsingId, + const uint64_t& aDatastoreId) +{ + MOZ_CRASH("PBackgroundLSDatabaseChild actor should be manually constructed!"); +} + +bool +BackgroundChildImpl::DeallocPBackgroundLSDatabaseChild( + PBackgroundLSDatabaseChild* aActor) +{ + MOZ_ASSERT(aActor); + + delete aActor; + return true; +} + +BackgroundChildImpl::PBackgroundLSObserverChild* +BackgroundChildImpl::AllocPBackgroundLSObserverChild( + const uint64_t& aObserverId) +{ + MOZ_CRASH("PBackgroundLSObserverChild actor should be manually constructed!"); +} + +bool +BackgroundChildImpl::DeallocPBackgroundLSObserverChild( + PBackgroundLSObserverChild* aActor) +{ + MOZ_ASSERT(aActor); + + delete aActor; + return true; +} + +BackgroundChildImpl::PBackgroundLSRequestChild* +BackgroundChildImpl::AllocPBackgroundLSRequestChild( + const LSRequestParams& aParams) +{ + MOZ_CRASH("PBackgroundLSRequestChild actor should be manually constructed!"); +} + +bool +BackgroundChildImpl::DeallocPBackgroundLSRequestChild( + PBackgroundLSRequestChild* aActor) +{ + MOZ_ASSERT(aActor); + + delete aActor; + return true; +} + BackgroundChildImpl::PBackgroundLocalStorageCacheChild* BackgroundChildImpl::AllocPBackgroundLocalStorageCacheChild( const PrincipalInfo& aPrincipalInfo, @@ -243,17 +318,17 @@ BackgroundChildImpl::DeallocPBackgroundLocalStorageCacheChild( return true; } -BackgroundChildImpl::PBackgroundSDBConnectionChild* -BackgroundChildImpl::AllocPBackgroundSDBConnectionChild( - const PrincipalInfo& aPrincipalInfo) +BackgroundChildImpl::PBackgroundLSSimpleRequestChild* +BackgroundChildImpl::AllocPBackgroundLSSimpleRequestChild( + const LSSimpleRequestParams& aParams) { - MOZ_CRASH("PBackgroundSDBConnectionChild actor should be manually " + MOZ_CRASH("PBackgroundLSSimpleRequestChild actor should be manually " "constructed!"); } bool -BackgroundChildImpl::DeallocPBackgroundSDBConnectionChild( - PBackgroundSDBConnectionChild* aActor) +BackgroundChildImpl::DeallocPBackgroundLSSimpleRequestChild( + PBackgroundLSSimpleRequestChild* aActor) { MOZ_ASSERT(aActor); diff --git a/ipc/glue/BackgroundChildImpl.h b/ipc/glue/BackgroundChildImpl.h index c0320f9afe46..aeec24bf688e 100644 --- a/ipc/glue/BackgroundChildImpl.h +++ b/ipc/glue/BackgroundChildImpl.h @@ -78,6 +78,37 @@ protected: DeallocPBackgroundSDBConnectionChild(PBackgroundSDBConnectionChild* aActor) override; + virtual PBackgroundLSDatabaseChild* + AllocPBackgroundLSDatabaseChild(const PrincipalInfo& aPrincipalInfo, + const uint32_t& aPrivateBrowsingId, + const uint64_t& aDatastoreId) override; + + virtual bool + DeallocPBackgroundLSDatabaseChild(PBackgroundLSDatabaseChild* aActor) + override; + + virtual PBackgroundLSObserverChild* + AllocPBackgroundLSObserverChild(const uint64_t& aObserverId) override; + + virtual bool + DeallocPBackgroundLSObserverChild(PBackgroundLSObserverChild* aActor) + override; + + virtual PBackgroundLSRequestChild* + AllocPBackgroundLSRequestChild(const LSRequestParams& aParams) override; + + virtual bool + DeallocPBackgroundLSRequestChild(PBackgroundLSRequestChild* aActor) override; + + virtual PBackgroundLSSimpleRequestChild* + AllocPBackgroundLSSimpleRequestChild(const LSSimpleRequestParams& aParams) + override; + + virtual bool + DeallocPBackgroundLSSimpleRequestChild( + PBackgroundLSSimpleRequestChild* aActor) + override; + virtual PBackgroundLocalStorageCacheChild* AllocPBackgroundLocalStorageCacheChild(const PrincipalInfo& aPrincipalInfo, const nsCString& aOriginKey, diff --git a/ipc/glue/BackgroundImpl.cpp b/ipc/glue/BackgroundImpl.cpp index 528e1ce8f6dc..ea7ce8479aa0 100644 --- a/ipc/glue/BackgroundImpl.cpp +++ b/ipc/glue/BackgroundImpl.cpp @@ -24,6 +24,8 @@ #include "mozilla/dom/ContentChild.h" #include "mozilla/dom/ContentParent.h" #include "mozilla/dom/File.h" +#include "mozilla/dom/WorkerPrivate.h" +#include "mozilla/dom/WorkerRef.h" #include "mozilla/ipc/ProtocolTypes.h" #include "nsAutoPtr.h" #include "nsCOMPtr.h" @@ -177,7 +179,7 @@ private: public: static already_AddRefed - CreateActorForSameProcess(); + CreateActorForSameProcess(nsIEventTarget* aMainEventTarget); static bool IsOnBackgroundThread() @@ -293,7 +295,7 @@ class ChildImpl final : public BackgroundChildImpl typedef mozilla::ipc::Transport Transport; class ShutdownObserver; - class ActorCreatedRunnable; + class SendInitBackgroundRunnable; // A thread-local index that is not valid. static const unsigned int kBadThreadLocalIndex = @@ -313,6 +315,7 @@ class ChildImpl final : public BackgroundChildImpl } RefPtr mActor; + RefPtr mSendInitBackgroundRunnable; nsAutoPtr mConsumerThreadLocal; #ifdef DEBUG bool mClosed; @@ -399,7 +402,7 @@ private: // Forwarded from BackgroundChild. static PBackgroundChild* - GetOrCreateForCurrentThread(); + GetOrCreateForCurrentThread(nsIEventTarget* aMainEventTarget); // Forwarded from BackgroundChild. static void @@ -410,20 +413,7 @@ private: GetThreadLocalForCurrentThread(); static void - ThreadLocalDestructor(void* aThreadLocal) - { - auto threadLocalInfo = static_cast(aThreadLocal); - - if (threadLocalInfo) { - MOZ_ASSERT(threadLocalInfo->mClosed); - - if (threadLocalInfo->mActor) { - threadLocalInfo->mActor->Close(); - threadLocalInfo->mActor->AssertActorDestroyed(); - } - delete threadLocalInfo; - } - } + ThreadLocalDestructor(void* aThreadLocal); // This class is reference counted. ~ChildImpl() @@ -569,7 +559,8 @@ public: } nsresult - BlockAndGetResults(RefPtr& aParentActor, + BlockAndGetResults(nsIEventTarget* aMainEventTarget, + RefPtr& aParentActor, nsCOMPtr& aThread); private: @@ -622,6 +613,43 @@ private: } }; +class ChildImpl::SendInitBackgroundRunnable final + : public CancelableRunnable +{ + nsCOMPtr mOwningEventTarget; + RefPtr mWorkerRef; + Endpoint mParent; + mozilla::Mutex mMutex; + bool mSentInitBackground; + +public: + static already_AddRefed + Create(Endpoint&& aParent); + + void + ClearEventTarget() + { + mWorkerRef = nullptr; + + mozilla::MutexAutoLock lock(mMutex); + mOwningEventTarget = nullptr; + } + +private: + explicit SendInitBackgroundRunnable(Endpoint&& aParent) + : CancelableRunnable("Background::ChildImpl::SendInitBackgroundRunnable") + , mOwningEventTarget(GetCurrentThreadSerialEventTarget()) + , mParent(std::move(aParent)) + , mMutex("SendInitBackgroundRunnable::mMutex") + , mSentInitBackground(false) + { } + + ~SendInitBackgroundRunnable() + { } + + NS_DECL_NSIRUNNABLE +}; + } // namespace namespace mozilla { @@ -716,9 +744,9 @@ BackgroundChild::GetForCurrentThread() // static PBackgroundChild* -BackgroundChild::GetOrCreateForCurrentThread() +BackgroundChild::GetOrCreateForCurrentThread(nsIEventTarget* aMainEventTarget) { - return ChildImpl::GetOrCreateForCurrentThread(); + return ChildImpl::GetOrCreateForCurrentThread(aMainEventTarget); } // static @@ -914,7 +942,7 @@ ParentImpl::Alloc(ContentParent* aContent, // static already_AddRefed -ParentImpl::CreateActorForSameProcess() +ParentImpl::CreateActorForSameProcess(nsIEventTarget* aMainEventTarget) { AssertIsInMainProcess(); @@ -936,7 +964,9 @@ ParentImpl::CreateActorForSameProcess() } else { RefPtr helper = new CreateActorHelper(); - nsresult rv = helper->BlockAndGetResults(parentActor, backgroundThread); + nsresult rv = helper->BlockAndGetResults(aMainEventTarget, + parentActor, + backgroundThread); if (NS_WARN_IF(NS_FAILED(rv))) { return nullptr; } @@ -1302,10 +1332,17 @@ ParentImpl::ConnectActorRunnable::Run() nsresult ParentImpl:: -CreateActorHelper::BlockAndGetResults(RefPtr& aParentActor, +CreateActorHelper::BlockAndGetResults(nsIEventTarget* aMainEventTarget, + RefPtr& aParentActor, nsCOMPtr& aThread) { - MOZ_ALWAYS_SUCCEEDS(NS_DispatchToMainThread(this)); + AssertIsNotOnMainThread(); + + if (aMainEventTarget) { + MOZ_ALWAYS_SUCCEEDS(aMainEventTarget->Dispatch(this, NS_DISPATCH_NORMAL)); + } else { + MOZ_ALWAYS_SUCCEEDS(NS_DispatchToMainThread(this)); + } mozilla::MonitorAutoLock lock(mMonitor); while (mWaiting) { @@ -1449,8 +1486,10 @@ ChildImpl::GetForCurrentThread() /* static */ PBackgroundChild* -ChildImpl::GetOrCreateForCurrentThread() +ChildImpl::GetOrCreateForCurrentThread(nsIEventTarget* aMainEventTarget) { + MOZ_ASSERT_IF(NS_IsMainThread(), !aMainEventTarget); + MOZ_ASSERT(sThreadLocalIndex != kBadThreadLocalIndex, "BackgroundChild::Startup() was never called!"); @@ -1477,11 +1516,35 @@ ChildImpl::GetOrCreateForCurrentThread() } if (threadLocalInfo->mActor) { + RefPtr& runnable = + threadLocalInfo->mSendInitBackgroundRunnable; + + if (aMainEventTarget && runnable) { + // The SendInitBackgroundRunnable was already dispatched to the main + // thread to finish initialization of a new background child actor. + // However, the caller passed a custom main event target which indicates + // that synchronous blocking of the main thread is happening (done by + // creating a nested event target and spinning the event loop). + // It can happen that the SendInitBackgroundRunnable didn't have a chance + // to run before the synchronous blocking has occured. Unblocking of the + // main thread can depend on an IPC message received on this thread, so + // we have to dispatch the SendInitBackgroundRunnable to the custom main + // event target too, otherwise IPC will be only queueing messages on this + // thread. The runnable will run twice in the end, but that's a harmless + // race between the main and nested event queue of the main thread. + // There's a guard in the runnable implementation for calling + // SendInitBackground only once. + + MOZ_ALWAYS_SUCCEEDS(aMainEventTarget->Dispatch(runnable, + NS_DISPATCH_NORMAL)); + } + return threadLocalInfo->mActor; } if (XRE_IsParentProcess()) { - RefPtr strongActor = ParentImpl::CreateActorForSameProcess(); + RefPtr strongActor = + ParentImpl::CreateActorForSameProcess(aMainEventTarget); if (NS_WARN_IF(!strongActor)) { return nullptr; } @@ -1512,6 +1575,14 @@ ChildImpl::GetOrCreateForCurrentThread() return nullptr; } + RefPtr runnable; + if (!NS_IsMainThread()) { + runnable = SendInitBackgroundRunnable::Create(std::move(parent)); + if (!runnable) { + return nullptr; + } + } + RefPtr strongActor = new ChildImpl(); if (!child.Bind(strongActor)) { @@ -1528,13 +1599,14 @@ ChildImpl::GetOrCreateForCurrentThread() return nullptr; } } else { - nsCOMPtr runnable = - NewRunnableMethod&&>( - "dom::ContentChild::SendInitBackground", - content, - &ContentChild::SendInitBackground, - std::move(parent)); - MOZ_ALWAYS_SUCCEEDS(NS_DispatchToMainThread(runnable)); + if (aMainEventTarget) { + MOZ_ALWAYS_SUCCEEDS(aMainEventTarget->Dispatch(runnable, + NS_DISPATCH_NORMAL)); + } else { + MOZ_ALWAYS_SUCCEEDS(NS_DispatchToMainThread(runnable)); + } + + threadLocalInfo->mSendInitBackgroundRunnable = runnable; } RefPtr& actor = threadLocalInfo->mActor; @@ -1593,6 +1665,28 @@ ChildImpl::GetThreadLocalForCurrentThread() return threadLocalInfo->mConsumerThreadLocal; } +// static +void +ChildImpl::ThreadLocalDestructor(void* aThreadLocal) +{ + auto threadLocalInfo = static_cast(aThreadLocal); + + if (threadLocalInfo) { + MOZ_ASSERT(threadLocalInfo->mClosed); + + if (threadLocalInfo->mActor) { + threadLocalInfo->mActor->Close(); + threadLocalInfo->mActor->AssertActorDestroyed(); + } + + if (threadLocalInfo->mSendInitBackgroundRunnable) { + threadLocalInfo->mSendInitBackgroundRunnable->ClearEventTarget(); + } + + delete threadLocalInfo; + } +} + void ChildImpl::ActorDestroy(ActorDestroyReason aWhy) { @@ -1620,3 +1714,80 @@ ChildImpl::ShutdownObserver::Observe(nsISupports* aSubject, return NS_OK; } + +// static +already_AddRefed +ChildImpl:: +SendInitBackgroundRunnable::Create(Endpoint&& aParent) +{ + MOZ_ASSERT(!NS_IsMainThread()); + + RefPtr runnable = + new SendInitBackgroundRunnable(std::move(aParent)); + + WorkerPrivate* workerPrivate = mozilla::dom::GetCurrentThreadWorkerPrivate(); + if (!workerPrivate) { + return runnable.forget(); + } + + workerPrivate->AssertIsOnWorkerThread(); + + runnable->mWorkerRef = + StrongWorkerRef::Create(workerPrivate, + "ChildImpl::SendInitBackgroundRunnable"); + if (NS_WARN_IF(!runnable->mWorkerRef)) { + return nullptr; + } + + return runnable.forget(); +} + +NS_IMETHODIMP +ChildImpl:: +SendInitBackgroundRunnable::Run() +{ + if (NS_IsMainThread()) { + if (mSentInitBackground) { + return NS_OK; + } + + mSentInitBackground = true; + + RefPtr content = ContentChild::GetSingleton(); + MOZ_ASSERT(content); + + if (!content->SendInitBackground(std::move(mParent))) { + MOZ_CRASH("Failed to create top level actor!"); + } + + nsCOMPtr owningEventTarget; + { + mozilla::MutexAutoLock lock(mMutex); + owningEventTarget = mOwningEventTarget; + } + + if (!owningEventTarget) { + return NS_OK; + } + + nsresult rv = owningEventTarget->Dispatch(this, NS_DISPATCH_NORMAL); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + return NS_OK; + } + + ClearEventTarget(); + + auto threadLocalInfo = + static_cast(PR_GetThreadPrivate(sThreadLocalIndex)); + + if (!threadLocalInfo) { + return NS_OK; + } + + threadLocalInfo->mSendInitBackgroundRunnable = nullptr; + + return NS_OK; +} diff --git a/ipc/glue/BackgroundParentImpl.cpp b/ipc/glue/BackgroundParentImpl.cpp index b17b30aec9b2..70efd0873e60 100644 --- a/ipc/glue/BackgroundParentImpl.cpp +++ b/ipc/glue/BackgroundParentImpl.cpp @@ -33,6 +33,7 @@ #include "mozilla/dom/ipc/IPCBlobInputStreamParent.h" #include "mozilla/dom/ipc/PendingIPCBlobParent.h" #include "mozilla/dom/ipc/TemporaryIPCBlobParent.h" +#include "mozilla/dom/localstorage/ActorsParent.h" #include "mozilla/dom/quota/ActorsParent.h" #include "mozilla/dom/simpledb/ActorsParent.h" #include "mozilla/dom/RemoteWorkerParent.h" @@ -293,6 +294,161 @@ BackgroundParentImpl::DeallocPBackgroundSDBConnectionParent( return mozilla::dom::DeallocPBackgroundSDBConnectionParent(aActor); } +BackgroundParentImpl::PBackgroundLSDatabaseParent* +BackgroundParentImpl::AllocPBackgroundLSDatabaseParent( + const PrincipalInfo& aPrincipalInfo, + const uint32_t& aPrivateBrowsingId, + const uint64_t& aDatastoreId) +{ + AssertIsInMainProcess(); + AssertIsOnBackgroundThread(); + + return mozilla::dom::AllocPBackgroundLSDatabaseParent(aPrincipalInfo, + aPrivateBrowsingId, + aDatastoreId); +} + +mozilla::ipc::IPCResult +BackgroundParentImpl::RecvPBackgroundLSDatabaseConstructor( + PBackgroundLSDatabaseParent* aActor, + const PrincipalInfo& aPrincipalInfo, + const uint32_t& aPrivateBrowsingId, + const uint64_t& aDatastoreId) +{ + AssertIsInMainProcess(); + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aActor); + + if (!mozilla::dom::RecvPBackgroundLSDatabaseConstructor(aActor, + aPrincipalInfo, + aPrivateBrowsingId, + aDatastoreId)) { + return IPC_FAIL_NO_REASON(this); + } + return IPC_OK(); +} + +bool +BackgroundParentImpl::DeallocPBackgroundLSDatabaseParent( + PBackgroundLSDatabaseParent* aActor) +{ + AssertIsInMainProcess(); + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aActor); + + return mozilla::dom::DeallocPBackgroundLSDatabaseParent(aActor); +} + +BackgroundParentImpl::PBackgroundLSObserverParent* +BackgroundParentImpl::AllocPBackgroundLSObserverParent( + const uint64_t& aObserverId) +{ + AssertIsInMainProcess(); + AssertIsOnBackgroundThread(); + + return mozilla::dom::AllocPBackgroundLSObserverParent(aObserverId); +} + +mozilla::ipc::IPCResult +BackgroundParentImpl::RecvPBackgroundLSObserverConstructor( + PBackgroundLSObserverParent* aActor, + const uint64_t& aObserverId) +{ + AssertIsInMainProcess(); + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aActor); + + if (!mozilla::dom::RecvPBackgroundLSObserverConstructor(aActor, + aObserverId)) { + return IPC_FAIL_NO_REASON(this); + } + return IPC_OK(); +} + +bool +BackgroundParentImpl::DeallocPBackgroundLSObserverParent( + PBackgroundLSObserverParent* aActor) +{ + AssertIsInMainProcess(); + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aActor); + + return mozilla::dom::DeallocPBackgroundLSObserverParent(aActor); +} + +BackgroundParentImpl::PBackgroundLSRequestParent* +BackgroundParentImpl::AllocPBackgroundLSRequestParent( + const LSRequestParams& aParams) +{ + AssertIsInMainProcess(); + AssertIsOnBackgroundThread(); + + return mozilla::dom::AllocPBackgroundLSRequestParent(this, aParams); +} + +mozilla::ipc::IPCResult +BackgroundParentImpl::RecvPBackgroundLSRequestConstructor( + PBackgroundLSRequestParent* aActor, + const LSRequestParams& aParams) +{ + AssertIsInMainProcess(); + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aActor); + + if (!mozilla::dom::RecvPBackgroundLSRequestConstructor(aActor, aParams)) { + return IPC_FAIL_NO_REASON(this); + } + return IPC_OK(); +} + +bool +BackgroundParentImpl::DeallocPBackgroundLSRequestParent( + PBackgroundLSRequestParent* aActor) +{ + AssertIsInMainProcess(); + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aActor); + + return mozilla::dom::DeallocPBackgroundLSRequestParent(aActor); +} + +BackgroundParentImpl::PBackgroundLSSimpleRequestParent* +BackgroundParentImpl::AllocPBackgroundLSSimpleRequestParent( + const LSSimpleRequestParams& aParams) +{ + AssertIsInMainProcess(); + AssertIsOnBackgroundThread(); + + return mozilla::dom::AllocPBackgroundLSSimpleRequestParent(aParams); +} + +mozilla::ipc::IPCResult +BackgroundParentImpl::RecvPBackgroundLSSimpleRequestConstructor( + PBackgroundLSSimpleRequestParent* aActor, + const LSSimpleRequestParams& aParams) +{ + AssertIsInMainProcess(); + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aActor); + + if (!mozilla::dom::RecvPBackgroundLSSimpleRequestConstructor(aActor, + aParams)) { + return IPC_FAIL_NO_REASON(this); + } + return IPC_OK(); +} + +bool +BackgroundParentImpl::DeallocPBackgroundLSSimpleRequestParent( + PBackgroundLSSimpleRequestParent* aActor) +{ + AssertIsInMainProcess(); + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aActor); + + return mozilla::dom::DeallocPBackgroundLSSimpleRequestParent(aActor); +} + BackgroundParentImpl::PBackgroundLocalStorageCacheParent* BackgroundParentImpl::AllocPBackgroundLocalStorageCacheParent( const PrincipalInfo& aPrincipalInfo, diff --git a/ipc/glue/BackgroundParentImpl.h b/ipc/glue/BackgroundParentImpl.h index 64a1ba40737c..03b0524e08a5 100644 --- a/ipc/glue/BackgroundParentImpl.h +++ b/ipc/glue/BackgroundParentImpl.h @@ -77,6 +77,58 @@ protected: DeallocPBackgroundSDBConnectionParent(PBackgroundSDBConnectionParent* aActor) override; + virtual PBackgroundLSDatabaseParent* + AllocPBackgroundLSDatabaseParent(const PrincipalInfo& aPrincipalInfo, + const uint32_t& aPrivateBrowsingId, + const uint64_t& aDatastoreId) override; + + virtual mozilla::ipc::IPCResult + RecvPBackgroundLSDatabaseConstructor(PBackgroundLSDatabaseParent* aActor, + const PrincipalInfo& aPrincipalInfo, + const uint32_t& aPrivateBrowsingId, + const uint64_t& aDatastoreId) override; + + virtual bool + DeallocPBackgroundLSDatabaseParent(PBackgroundLSDatabaseParent* aActor) + override; + + virtual PBackgroundLSObserverParent* + AllocPBackgroundLSObserverParent(const uint64_t& aObserverId) override; + + virtual mozilla::ipc::IPCResult + RecvPBackgroundLSObserverConstructor(PBackgroundLSObserverParent* aActor, + const uint64_t& aObserverId) override; + + virtual bool + DeallocPBackgroundLSObserverParent(PBackgroundLSObserverParent* aActor) + override; + + virtual PBackgroundLSRequestParent* + AllocPBackgroundLSRequestParent(const LSRequestParams& aParams) override; + + virtual mozilla::ipc::IPCResult + RecvPBackgroundLSRequestConstructor(PBackgroundLSRequestParent* aActor, + const LSRequestParams& aParams) override; + + virtual bool + DeallocPBackgroundLSRequestParent(PBackgroundLSRequestParent* aActor) + override; + + virtual PBackgroundLSSimpleRequestParent* + AllocPBackgroundLSSimpleRequestParent(const LSSimpleRequestParams& aParams) + override; + + virtual mozilla::ipc::IPCResult + RecvPBackgroundLSSimpleRequestConstructor( + PBackgroundLSSimpleRequestParent* aActor, + const LSSimpleRequestParams& aParams) + override; + + virtual bool + DeallocPBackgroundLSSimpleRequestParent( + PBackgroundLSSimpleRequestParent* aActor) + override; + virtual PBackgroundLocalStorageCacheParent* AllocPBackgroundLocalStorageCacheParent(const PrincipalInfo& aPrincipalInfo, const nsCString& aOriginKey, diff --git a/ipc/glue/PBackground.ipdl b/ipc/glue/PBackground.ipdl index 7b6d4304de7f..47bff68fdb02 100644 --- a/ipc/glue/PBackground.ipdl +++ b/ipc/glue/PBackground.ipdl @@ -6,6 +6,10 @@ include protocol PAsmJSCacheEntry; include protocol PBackgroundIDBFactory; include protocol PBackgroundIndexedDBUtils; include protocol PBackgroundSDBConnection; +include protocol PBackgroundLSDatabase; +include protocol PBackgroundLSObserver; +include protocol PBackgroundLSRequest; +include protocol PBackgroundLSSimpleRequest; include protocol PBackgroundLocalStorageCache; include protocol PBackgroundStorage; include protocol PBackgroundTest; @@ -44,6 +48,7 @@ include DOMTypes; include IPCBlob; include IPCServiceWorkerDescriptor; include IPCServiceWorkerRegistrationDescriptor; +include PBackgroundLSSharedTypes; include PBackgroundSharedTypes; include PBackgroundIDBSharedTypes; include PFileSystemParams; @@ -72,6 +77,10 @@ sync protocol PBackground manages PBackgroundIDBFactory; manages PBackgroundIndexedDBUtils; manages PBackgroundSDBConnection; + manages PBackgroundLSDatabase; + manages PBackgroundLSObserver; + manages PBackgroundLSRequest; + manages PBackgroundLSSimpleRequest; manages PBackgroundLocalStorageCache; manages PBackgroundStorage; manages PBackgroundTest; @@ -119,6 +128,27 @@ parent: async PBackgroundSDBConnection(PrincipalInfo principalInfo); + async PBackgroundLSDatabase(PrincipalInfo principalInfo, + uint32_t privateBrowsingId, + uint64_t datastoreId); + + async PBackgroundLSObserver(uint64_t observerId); + + /** + * Issue an asynchronous request that will be used in a synchronous fashion + * through complex machinations described in `PBackgroundLSRequest.ipdl` and + * `LSObject.h`. + */ + async PBackgroundLSRequest(LSRequestParams params); + + /** + * Issues a simple, non-cancelable asynchronous request that's used in an + * asynchronous fashion by callers. (LSRequest is not simple because it used + * in a synchronous fashion which leads to complexities regarding cancelation, + * see `PBackgroundLSRequest.ipdl` for details.) + */ + async PBackgroundLSSimpleRequest(LSSimpleRequestParams params); + async PBackgroundLocalStorageCache(PrincipalInfo principalInfo, nsCString originKey, uint32_t privateBrowsingId); diff --git a/ipc/ipdl/sync-messages.ini b/ipc/ipdl/sync-messages.ini index 2a2fe5f2484e..a753f6530e75 100644 --- a/ipc/ipdl/sync-messages.ini +++ b/ipc/ipdl/sync-messages.ini @@ -922,6 +922,16 @@ description = description = [PBackgroundStorage::Preload] description = +[PBackgroundLSDatabase::PBackgroundLSSnapshot] +description = See corresponding comment in PBackgroundLSDatabase.ipdl +[PBackgroundLSSnapshot::LoadItem] +description = See corresponding comment in PBackgroundLSSnapshot.ipdl +[PBackgroundLSSnapshot::LoadKeys] +description = See corresponding comment in PBackgroundLSSnapshot.ipdl +[PBackgroundLSSnapshot::IncreasePeakUsage] +description = See corresponding comment in PBackgroundLSSnapshot.ipdl +[PBackgroundLSSnapshot::Ping] +description = See corresponding comment in PBackgroundLSSnapshot.ipdl [PRemoteSpellcheckEngine::Check] description = [PRemoteSpellcheckEngine::CheckAndSuggest] diff --git a/js/src/jit/CacheIRSpewer.cpp b/js/src/jit/CacheIRSpewer.cpp index f4fd6aa784bf..3c5f82777ede 100644 --- a/js/src/jit/CacheIRSpewer.cpp +++ b/js/src/jit/CacheIRSpewer.cpp @@ -31,8 +31,18 @@ using namespace js::jit; CacheIRSpewer CacheIRSpewer::cacheIRspewer; CacheIRSpewer::CacheIRSpewer() - : outputLock(mutexid::CacheIRSpewer) -{ } + : outputLock(mutexid::CacheIRSpewer), + guardCount_(0) +{ + + spewInterval_ = getenv("CACHEIR_LOG_FLUSH") ? + atoi(getenv("CACHEIR_LOG_FLUSH")) : + 10000; + + if (spewInterval_ < 1) { + spewInterval_ = 1; + } +} CacheIRSpewer::~CacheIRSpewer() { diff --git a/js/src/jit/CacheIRSpewer.h b/js/src/jit/CacheIRSpewer.h index edf4254ab324..f44bc2141284 100644 --- a/js/src/jit/CacheIRSpewer.h +++ b/js/src/jit/CacheIRSpewer.h @@ -27,6 +27,16 @@ class CacheIRSpewer mozilla::Maybe json; static CacheIRSpewer cacheIRspewer; + // Counter to record how many times Guard class is called. This is used to + // determine when to flush outputs based on the given interval value. + // For example, if |spewInterval_ = 2|, outputs will be flushed on + // guardCount_ values 0,2,4,6,... + uint32_t guardCount_; + + // Interval at which to flush output files. This value can be set with the + // environment variable |CACHEIR_LOG_FLUSH|. + uint32_t spewInterval_; + CacheIRSpewer(); ~CacheIRSpewer(); @@ -68,6 +78,9 @@ class CacheIRSpewer sp_.attached(name_); } sp_.endCache(); + if (sp_.guardCount_++ % sp_.spewInterval_ == 0) { + sp_.output.flush(); + } sp_.lock().unlock(); } } diff --git a/js/src/jit/MacroAssembler-inl.h b/js/src/jit/MacroAssembler-inl.h index dead510c82a5..be6a641ef9ef 100644 --- a/js/src/jit/MacroAssembler-inl.h +++ b/js/src/jit/MacroAssembler-inl.h @@ -33,7 +33,6 @@ namespace js { namespace jit { -// clang-format off //{{{ check_macroassembler_style // =============================================================== // Stack manipulation functions. @@ -800,7 +799,6 @@ template void MacroAssembler::storeFloat32(FloatRegister src, const Address& des template void MacroAssembler::storeFloat32(FloatRegister src, const BaseIndex& dest); //}}} check_macroassembler_style -// clang-format on // =============================================================== #ifndef JS_CODEGEN_ARM64 diff --git a/js/src/jit/MacroAssembler.cpp b/js/src/jit/MacroAssembler.cpp index c8c0554369d2..4eabb20b9060 100644 --- a/js/src/jit/MacroAssembler.cpp +++ b/js/src/jit/MacroAssembler.cpp @@ -2954,7 +2954,6 @@ MacroAssembler::subFromStackPtr(Register reg) } #endif // JS_CODEGEN_ARM64 -// clang-format off //{{{ check_macroassembler_style // =============================================================== // Stack manipulation functions. @@ -3807,7 +3806,6 @@ MacroAssembler::boundsCheck32PowerOfTwo(Register index, uint32_t length, Label* } //}}} check_macroassembler_style -// clang-format on void MacroAssembler::memoryBarrierBefore(const Synchronization& sync) { diff --git a/js/src/jit/MacroAssembler.h b/js/src/jit/MacroAssembler.h index 98cbd4b05e23..657d9fa1b304 100644 --- a/js/src/jit/MacroAssembler.h +++ b/js/src/jit/MacroAssembler.h @@ -350,7 +350,6 @@ class MacroAssembler : public MacroAssemblerSpecific void Push(RegisterOrSP reg); #endif - // clang-format off //{{{ check_macroassembler_decl_style public: // =============================================================== @@ -2136,7 +2135,6 @@ class MacroAssembler : public MacroAssemblerSpecific void speculationBarrier() PER_SHARED_ARCH; //}}} check_macroassembler_decl_style - // clang-format on public: // Emits a test of a value against all types in a TypeSet. A scratch @@ -2902,7 +2900,6 @@ class IonHeapMacroAssembler : public MacroAssembler } }; -// clang-format off //{{{ check_macroassembler_style inline uint32_t MacroAssembler::framePushed() const @@ -2931,7 +2928,6 @@ MacroAssembler::implicitPop(uint32_t bytes) adjustFrame(-int32_t(bytes)); } //}}} check_macroassembler_style -// clang-format on static inline Assembler::DoubleCondition JSOpToDoubleCondition(JSOp op) diff --git a/js/src/jit/arm/MacroAssembler-arm-inl.h b/js/src/jit/arm/MacroAssembler-arm-inl.h index de8808ca971b..370e68960522 100644 --- a/js/src/jit/arm/MacroAssembler-arm-inl.h +++ b/js/src/jit/arm/MacroAssembler-arm-inl.h @@ -12,7 +12,6 @@ namespace js { namespace jit { -// clang-format off //{{{ check_macroassembler_style void @@ -2335,7 +2334,6 @@ MacroAssembler::clampIntToUint8(Register reg) } //}}} check_macroassembler_style -// clang-format on // =============================================================== void diff --git a/js/src/jit/arm/MacroAssembler-arm.cpp b/js/src/jit/arm/MacroAssembler-arm.cpp index c5330f2f45c1..51e3ec34d8e6 100644 --- a/js/src/jit/arm/MacroAssembler-arm.cpp +++ b/js/src/jit/arm/MacroAssembler-arm.cpp @@ -4362,7 +4362,6 @@ MacroAssembler::subFromStackPtr(Imm32 imm32) } } -// clang-format off //{{{ check_macroassembler_style // =============================================================== // MacroAssembler high-level usage. @@ -6176,7 +6175,6 @@ MacroAssembler::speculationBarrier() } //}}} check_macroassembler_style -// clang-format on void MacroAssemblerARM::wasmTruncateToInt32(FloatRegister input, Register output, MIRType fromType, diff --git a/js/src/jit/arm64/MacroAssembler-arm64-inl.h b/js/src/jit/arm64/MacroAssembler-arm64-inl.h index 07fbf4b5b196..21196471ee39 100644 --- a/js/src/jit/arm64/MacroAssembler-arm64-inl.h +++ b/js/src/jit/arm64/MacroAssembler-arm64-inl.h @@ -12,7 +12,6 @@ namespace js { namespace jit { -// clang-format off //{{{ check_macroassembler_style void @@ -1982,7 +1981,6 @@ MacroAssembler::clampIntToUint8(Register reg) } //}}} check_macroassembler_style -// clang-format on // =============================================================== void diff --git a/js/src/jit/arm64/MacroAssembler-arm64.cpp b/js/src/jit/arm64/MacroAssembler-arm64.cpp index 49a015fdb2d8..823cff5b00ed 100644 --- a/js/src/jit/arm64/MacroAssembler-arm64.cpp +++ b/js/src/jit/arm64/MacroAssembler-arm64.cpp @@ -413,7 +413,6 @@ MacroAssembler::Push(RegisterOrSP reg) adjustFrame(sizeof(intptr_t)); } -// clang-format off //{{{ check_macroassembler_style // =============================================================== // MacroAssembler high-level usage. @@ -2025,7 +2024,6 @@ MacroAssembler::speculationBarrier() } //}}} check_macroassembler_style -// clang-format off } // namespace jit } // namespace js diff --git a/js/src/jit/mips-shared/MacroAssembler-mips-shared-inl.h b/js/src/jit/mips-shared/MacroAssembler-mips-shared-inl.h index a1513230494f..dcbb4868a8d6 100644 --- a/js/src/jit/mips-shared/MacroAssembler-mips-shared-inl.h +++ b/js/src/jit/mips-shared/MacroAssembler-mips-shared-inl.h @@ -12,7 +12,6 @@ namespace js { namespace jit { -// clang-format off //{{{ check_macroassembler_style void @@ -1111,7 +1110,6 @@ MacroAssembler::clampIntToUint8(Register reg) } //}}} check_macroassembler_style -// clang-format on // =============================================================== } // namespace jit diff --git a/js/src/jit/mips-shared/MacroAssembler-mips-shared.cpp b/js/src/jit/mips-shared/MacroAssembler-mips-shared.cpp index 6ab1f9a3151c..28efa47c4379 100644 --- a/js/src/jit/mips-shared/MacroAssembler-mips-shared.cpp +++ b/js/src/jit/mips-shared/MacroAssembler-mips-shared.cpp @@ -1439,7 +1439,6 @@ MacroAssemblerMIPSShared::asMasm() const return *static_cast(this); } -// clang-format off //{{{ check_macroassembler_style // =============================================================== // MacroAssembler high-level usage. @@ -2929,4 +2928,3 @@ MacroAssembler::speculationBarrier() MOZ_CRASH(); } //}}} check_macroassembler_style -// clang-format on diff --git a/js/src/jit/mips32/MacroAssembler-mips32-inl.h b/js/src/jit/mips32/MacroAssembler-mips32-inl.h index 894055dd2697..3d1592ce0e56 100644 --- a/js/src/jit/mips32/MacroAssembler-mips32-inl.h +++ b/js/src/jit/mips32/MacroAssembler-mips32-inl.h @@ -14,7 +14,6 @@ namespace js { namespace jit { -// clang-format off //{{{ check_macroassembler_style void @@ -1012,7 +1011,6 @@ MacroAssembler::branchTruncateFloat32MaybeModUint32(FloatRegister src, Register } //}}} check_macroassembler_style -// clang-format on // =============================================================== void diff --git a/js/src/jit/mips32/MacroAssembler-mips32.cpp b/js/src/jit/mips32/MacroAssembler-mips32.cpp index 7f4baec910c5..5907140c9cb5 100644 --- a/js/src/jit/mips32/MacroAssembler-mips32.cpp +++ b/js/src/jit/mips32/MacroAssembler-mips32.cpp @@ -2109,7 +2109,6 @@ MacroAssembler::subFromStackPtr(Imm32 imm32) } } -// clang-format on //{{{ check_macroassembler_style // =============================================================== // Stack manipulation functions. @@ -2968,4 +2967,3 @@ MacroAssembler::convertUInt64ToDouble(Register64 src, FloatRegister dest, Regist } //}}} check_macroassembler_style -// clang-format on diff --git a/js/src/jit/mips64/MacroAssembler-mips64-inl.h b/js/src/jit/mips64/MacroAssembler-mips64-inl.h index 58859812a2b6..867aade6383e 100644 --- a/js/src/jit/mips64/MacroAssembler-mips64-inl.h +++ b/js/src/jit/mips64/MacroAssembler-mips64-inl.h @@ -14,7 +14,6 @@ namespace js { namespace jit { -// clang-format off //{{{ check_macroassembler_style void @@ -759,7 +758,6 @@ MacroAssembler::branchTruncateFloat32MaybeModUint32(FloatRegister src, Register } //}}} check_macroassembler_style -// clang-format on // =============================================================== // The specializations for cmpPtrSet are outside the braces because check_macroassembler_style can't yet diff --git a/js/src/jit/mips64/MacroAssembler-mips64.cpp b/js/src/jit/mips64/MacroAssembler-mips64.cpp index 64381d4ec651..4bbc4d4e87e8 100644 --- a/js/src/jit/mips64/MacroAssembler-mips64.cpp +++ b/js/src/jit/mips64/MacroAssembler-mips64.cpp @@ -1948,7 +1948,6 @@ MacroAssembler::subFromStackPtr(Imm32 imm32) } } -// clang-format off //{{{ check_macroassembler_style // =============================================================== // Stack manipulation functions. @@ -2760,4 +2759,3 @@ MacroAssembler::convertUInt64ToFloat32(Register64 src_, FloatRegister dest, Regi } //}}} check_macroassembler_style -// clang-format on diff --git a/js/src/jit/x64/MacroAssembler-x64-inl.h b/js/src/jit/x64/MacroAssembler-x64-inl.h index 4361cb73773a..e192c0c32ba9 100644 --- a/js/src/jit/x64/MacroAssembler-x64-inl.h +++ b/js/src/jit/x64/MacroAssembler-x64-inl.h @@ -14,7 +14,6 @@ namespace js { namespace jit { -// clang-format off //{{{ check_macroassembler_style // =============================================================== @@ -965,7 +964,6 @@ MacroAssembler::truncateDoubleToUInt64(Address src, Address dest, Register temp, } //}}} check_macroassembler_style -// clang-format on // =============================================================== void diff --git a/js/src/jit/x64/MacroAssembler-x64.cpp b/js/src/jit/x64/MacroAssembler-x64.cpp index 827741269d5d..4e41b499de9e 100644 --- a/js/src/jit/x64/MacroAssembler-x64.cpp +++ b/js/src/jit/x64/MacroAssembler-x64.cpp @@ -303,7 +303,6 @@ MacroAssembler::subFromStackPtr(Imm32 imm32) } } -// clang-format off //{{{ check_macroassembler_style // =============================================================== // ABI function calls. @@ -1039,4 +1038,3 @@ MacroAssembler::wasmAtomicEffectOp64(const wasm::MemoryAccessDesc& access, Atomi } //}}} check_macroassembler_style -// clang-format on diff --git a/js/src/jit/x86-shared/MacroAssembler-x86-shared-inl.h b/js/src/jit/x86-shared/MacroAssembler-x86-shared-inl.h index a2612cf28535..4bbfbb388ffb 100644 --- a/js/src/jit/x86-shared/MacroAssembler-x86-shared-inl.h +++ b/js/src/jit/x86-shared/MacroAssembler-x86-shared-inl.h @@ -12,7 +12,6 @@ namespace js { namespace jit { -// clang-format off //{{{ check_macroassembler_style // =============================================================== // Move instructions @@ -1332,7 +1331,6 @@ MacroAssembler::clampIntToUint8(Register reg) } //}}} check_macroassembler_style -// clang-format on // =============================================================== } // namespace jit diff --git a/js/src/jit/x86-shared/MacroAssembler-x86-shared.cpp b/js/src/jit/x86-shared/MacroAssembler-x86-shared.cpp index a0ebe90d646b..d50e8e74bb19 100644 --- a/js/src/jit/x86-shared/MacroAssembler-x86-shared.cpp +++ b/js/src/jit/x86-shared/MacroAssembler-x86-shared.cpp @@ -270,7 +270,6 @@ MacroAssemblerX86Shared::minMaxFloat32(FloatRegister first, FloatRegister second bind(&done); } -// clang-format off //{{{ check_macroassembler_style // =============================================================== // MacroAssembler high-level usage. @@ -1674,4 +1673,3 @@ MacroAssembler::speculationBarrier() } //}}} check_macroassembler_style -// clang-format on diff --git a/js/src/jit/x86/MacroAssembler-x86-inl.h b/js/src/jit/x86/MacroAssembler-x86-inl.h index ecddad7558ab..6a80a4c97266 100644 --- a/js/src/jit/x86/MacroAssembler-x86-inl.h +++ b/js/src/jit/x86/MacroAssembler-x86-inl.h @@ -14,7 +14,6 @@ namespace js { namespace jit { -// clang-format off //{{{ check_macroassembler_style void @@ -1183,7 +1182,6 @@ MacroAssembler::truncateDoubleToUInt64(Address src, Address dest, Register temp, } //}}} check_macroassembler_style -// clang-format on // =============================================================== // Note: this function clobbers the source register. diff --git a/js/src/jit/x86/MacroAssembler-x86.cpp b/js/src/jit/x86/MacroAssembler-x86.cpp index 60ffda510ef5..bccf7e42dacb 100644 --- a/js/src/jit/x86/MacroAssembler-x86.cpp +++ b/js/src/jit/x86/MacroAssembler-x86.cpp @@ -302,7 +302,6 @@ MacroAssembler::subFromStackPtr(Imm32 imm32) } } -// clang-format off //{{{ check_macroassembler_style // =============================================================== // ABI function calls. @@ -1282,5 +1281,4 @@ MacroAssembler::convertInt64ToFloat32(Register64 input, FloatRegister output) } //}}} check_macroassembler_style -// clang-format on diff --git a/layout/base/nsLayoutUtils.h b/layout/base/nsLayoutUtils.h index 73af47db815d..557c1289a54a 100644 --- a/layout/base/nsLayoutUtils.h +++ b/layout/base/nsLayoutUtils.h @@ -187,7 +187,6 @@ public: typedef mozilla::ScreenMargin ScreenMargin; typedef mozilla::LayoutDeviceIntSize LayoutDeviceIntSize; typedef mozilla::LayoutDeviceRect LayoutDeviceRect; - typedef mozilla::LayoutDeviceSize LayoutDeviceSize; typedef mozilla::StyleGeometryBox StyleGeometryBox; typedef mozilla::SVGImageContext SVGImageContext; typedef mozilla::LogicalSize LogicalSize; diff --git a/layout/build/nsLayoutModule.cpp b/layout/build/nsLayoutModule.cpp index c984775f5381..6500066bf83a 100644 --- a/layout/build/nsLayoutModule.cpp +++ b/layout/build/nsLayoutModule.cpp @@ -63,7 +63,9 @@ #include "mozilla/dom/BlobURL.h" #include "mozilla/dom/DOMRequest.h" #include "mozilla/dom/SDBConnection.h" +#include "mozilla/dom/LocalStorageCommon.h" #include "mozilla/dom/LocalStorageManager.h" +#include "mozilla/dom/LocalStorageManager2.h" #include "mozilla/dom/quota/QuotaManagerService.h" #include "mozilla/dom/ServiceWorkerManager.h" #include "mozilla/dom/StorageActivityService.h" @@ -165,7 +167,6 @@ already_AddRefed NS_CreatePresentationService(); // Factory Constructor typedef mozilla::dom::BlobURL::Mutator BlobURLMutator; NS_GENERIC_FACTORY_CONSTRUCTOR(BlobURLMutator) -NS_GENERIC_FACTORY_CONSTRUCTOR(LocalStorageManager) NS_GENERIC_FACTORY_SINGLETON_CONSTRUCTOR(DOMRequestService, DOMRequestService::FactoryCreate) NS_GENERIC_FACTORY_SINGLETON_CONSTRUCTOR(QuotaManagerService, @@ -494,6 +495,19 @@ NS_DEFINE_NAMED_CID(TEXT_INPUT_PROCESSOR_CID); NS_DEFINE_NAMED_CID(NS_SCRIPTERROR_CID); +static nsresult +LocalStorageManagerConstructor(nsISupports *aOuter, REFNSIID aIID, + void **aResult) +{ + if (NextGenLocalStorageEnabled()) { + RefPtr manager = new LocalStorageManager2(); + return manager->QueryInterface(aIID, aResult); + } + + RefPtr manager = new LocalStorageManager(); + return manager->QueryInterface(aIID, aResult); +} + static const mozilla::Module::CIDEntry kLayoutCIDs[] = { // clang-format off XPCONNECT_CIDENTRIES diff --git a/layout/generic/nsHTMLCanvasFrame.cpp b/layout/generic/nsHTMLCanvasFrame.cpp index 6be166c646ff..3912b9c4c8bb 100644 --- a/layout/generic/nsHTMLCanvasFrame.cpp +++ b/layout/generic/nsHTMLCanvasFrame.cpp @@ -185,10 +185,11 @@ public: } MaybeIntSize scaleToSize; + LayoutDeviceRect scBounds(LayoutDevicePoint(0, 0), bounds.Size()); wr::ImageRendering filter = wr::ToImageRendering(nsLayoutUtils::GetSamplingFilterForFrame(mFrame)); wr::MixBlendMode mixBlendMode = wr::MixBlendMode::Normal; aManager->WrBridge()->AddWebRenderParentCommand(OpUpdateAsyncImagePipeline(data->GetPipelineId().value(), - bounds.Size(), + scBounds, scTransform, scaleToSize, filter, diff --git a/layout/painting/nsDisplayList.cpp b/layout/painting/nsDisplayList.cpp index 39b50a6c192c..173e89125916 100644 --- a/layout/painting/nsDisplayList.cpp +++ b/layout/painting/nsDisplayList.cpp @@ -6830,6 +6830,7 @@ nsDisplayOpacity::CreateWebRenderCommands( GetActiveScrolledRoot(), aBuilder, filters, + LayoutDeviceRect(), nullptr, animationsId ? &prop : nullptr, opacityForSC); @@ -6883,10 +6884,10 @@ nsDisplayBlendMode::CreateWebRenderCommands( GetActiveScrolledRoot(), aBuilder, filters, + LayoutDeviceRect(), nullptr, nullptr, nullptr, - LayoutDevicePoint(), nullptr, nullptr, nsCSSRendering::GetGFXBlendMode(mBlendMode)); @@ -7175,6 +7176,7 @@ nsDisplayOwnLayer::CreateWebRenderCommands( GetActiveScrolledRoot(), aBuilder, nsTArray(), + LayoutDeviceRect(), nullptr, &prop); @@ -7833,14 +7835,6 @@ nsDisplayStickyPosition::CreateWebRenderCommands( nsRect scrollPort = stickyScrollContainer->ScrollFrame()->GetScrollPortRect(); scrollPort += offset; - // It would be cleaner to just modify `scrollPort` here instead of - // adjusting the computed margins, but the `scrollOrigin` is in a different - // space. - // Note: we aren't applying the reference frame origin if the scroll frame - // is already attached to it. - auto& scrollOrigin = - nsLayoutUtils::GetReferenceFrame(scrollFrame) == ReferenceFrame() ? - LayoutDevicePoint() : aSc.GetInheritedStickyOrigin(); // The following computations make more sense upon understanding the // semantics of "inner" and "outer", which is explained in the comment on @@ -7864,8 +7858,7 @@ nsDisplayStickyPosition::CreateWebRenderCommands( // -distance works. nscoord distance = DistanceToRange(inner.YMost(), outer.YMost()); topMargin = Some(NSAppUnitsToFloatPixels( - itemBounds.y - scrollPort.y - distance, auPerDevPixel - ) - scrollOrigin.y); + itemBounds.y - scrollPort.y - distance, auPerDevPixel)); // Question: What is the maximum positive ("downward") offset that WR // will have to apply to this item in order to prevent the item from // visually moving? @@ -7891,8 +7884,7 @@ nsDisplayStickyPosition::CreateWebRenderCommands( // the distance from itemBounds.YMost() to scrollPort.YMost(). nscoord distance = DistanceToRange(outer.Y(), inner.Y()); bottomMargin = Some(NSAppUnitsToFloatPixels( - scrollPort.YMost() - itemBounds.YMost() + distance, auPerDevPixel - ) - scrollOrigin.y); + scrollPort.YMost() - itemBounds.YMost() + distance, auPerDevPixel)); // And here WR will be moving the item upwards rather than downwards so // again things are inverted from the previous block. vBounds.min = @@ -7909,8 +7901,7 @@ nsDisplayStickyPosition::CreateWebRenderCommands( if (outer.XMost() != inner.XMost()) { nscoord distance = DistanceToRange(inner.XMost(), outer.XMost()); leftMargin = Some(NSAppUnitsToFloatPixels( - itemBounds.x - scrollPort.x - distance, auPerDevPixel - ) - scrollOrigin.x); + itemBounds.x - scrollPort.x - distance, auPerDevPixel)); hBounds.max = NSAppUnitsToFloatPixels(outer.XMost() - inner.XMost(), auPerDevPixel); if (inner.XMost() < 0) { @@ -7921,8 +7912,7 @@ nsDisplayStickyPosition::CreateWebRenderCommands( if (outer.X() != inner.X()) { nscoord distance = DistanceToRange(outer.X(), inner.X()); rightMargin = Some(NSAppUnitsToFloatPixels( - scrollPort.XMost() - itemBounds.XMost() + distance, auPerDevPixel - ) - scrollOrigin.x); + scrollPort.XMost() - itemBounds.XMost() + distance, auPerDevPixel)); hBounds.min = NSAppUnitsToFloatPixels(outer.X() - inner.X(), auPerDevPixel); if (appliedOffset.x == 0 && inner.X() > 0) { @@ -8926,10 +8916,10 @@ nsDisplayTransform::CreateWebRenderCommands( GetActiveScrolledRoot(), aBuilder, filters, + LayoutDeviceRect(position, LayoutDeviceSize()), &newTransformMatrix, animationsId ? &prop : nullptr, nullptr, - position, transformForSC, nullptr, gfx::CompositionOp::OP_OVER, @@ -9576,10 +9566,10 @@ nsDisplayPerspective::CreateWebRenderCommands( GetActiveScrolledRoot(), aBuilder, filters, + LayoutDeviceRect(), nullptr, nullptr, nullptr, - LayoutDevicePoint(), &transformForSC, &perspectiveMatrix, gfx::CompositionOp::OP_OVER, @@ -10279,10 +10269,10 @@ nsDisplayMasksAndClipPaths::CreateWebRenderCommands( GetActiveScrolledRoot(), aBuilder, /*aFilters: */ nsTArray(), + /*aBounds: */ bounds, /*aBoundTransform: */ nullptr, /*aAnimation: */ nullptr, /*aOpacity: */ opacity.ptrOr(nullptr), - /*aOrigin: */ LayoutDevicePoint(), /*aTransform: */ nullptr, /*aPerspective: */ nullptr, /*aMixBlendMode: */ gfx::CompositionOp::OP_OVER, @@ -10602,11 +10592,11 @@ nsDisplayFilters::CreateWebRenderCommands( GetActiveScrolledRoot(), aBuilder, wrFilters, + LayoutDeviceRect(), nullptr, nullptr, opacity != 1.0f && mHandleOpacity ? &opacity : nullptr, - LayoutDevicePoint(), nullptr, nullptr, gfx::CompositionOp::OP_OVER, diff --git a/layout/reftests/bugs/reftest.list b/layout/reftests/bugs/reftest.list index 984e5f604f70..7e82e0229cb6 100644 --- a/layout/reftests/bugs/reftest.list +++ b/layout/reftests/bugs/reftest.list @@ -305,7 +305,7 @@ fuzzy-if(Android,0-3,0-50) fuzzy-if(skiaContent,0-1,0-133) == 273681-1.html 2736 == 283686-2.html 283686-2-ref.html == 283686-3.html about:blank == 289384-1.xhtml 289384-ref.xhtml -fails-if(webrender&>kWidget) random-if(d2d) fuzzy-if(Android,0-8,0-1439) HTTP == 289480.html#top 289480-ref.html # basically-verbatim acid2 test, HTTP for a 404 page -- bug 578114 for the d2d failures +fails-if(webrender&>kWidget) random-if(d2d) fuzzy-if(webrender&&cocoaWidget,1-2,400-900) fuzzy-if(Android,0-8,0-1439) HTTP == 289480.html#top 289480-ref.html # basically-verbatim acid2 test, HTTP for a 404 page -- bug 578114 for the d2d failures == 290129-1.html 290129-1-ref.html == 291078-1.html 291078-1-ref.html == 291078-2.html 291078-2-ref.html @@ -2077,7 +2077,7 @@ test-pref(font.size.systemFontScale,200) == 1412743.html 1412743-ref.html == 1424680.html 1424680-ref.html == 1424798-1.html 1424798-ref.html fuzzy-if(!webrender,0-74,0-2234) == 1425243-1.html 1425243-1-ref.html -fuzzy-if(Android,0-66,0-574) fuzzy-if(d2d,0-89,0-777) fuzzy-if(!Android&&!d2d,0-1,0-31341) fuzzy-if(webrender&&winWidget,1-1,31308-31320) == 1425243-2.html 1425243-2-ref.html +fuzzy-if(Android,0-66,0-574) fuzzy-if(d2d,0-89,0-777) fuzzy-if(!Android&&!d2d,0-1,0-31341) fuzzy-if(webrender&&winWidget,1-1,31320-31320) == 1425243-2.html 1425243-2-ref.html == 1430869.html 1430869-ref.html == 1432541.html 1432541-ref.html pref(layout.css.moz-document.url-prefix-hack.enabled,true) == 1446470.html 1035091-ref.html diff --git a/layout/reftests/svg/conditions-07.svg b/layout/reftests/svg/conditions-07.svg index 7ed4ef12b81e..15b5ff0dbdc9 100644 --- a/layout/reftests/svg/conditions-07.svg +++ b/layout/reftests/svg/conditions-07.svg @@ -17,7 +17,7 @@ return; } f1.removeAttribute("systemLanguage"); - + } catch(e) { var f = document.getElementById("fail"); f.setAttribute("fill", "red"); @@ -28,7 +28,7 @@ - + diff --git a/layout/reftests/svg/reftest.list b/layout/reftests/svg/reftest.list index 9849518f7571..9128aef8a60b 100644 --- a/layout/reftests/svg/reftest.list +++ b/layout/reftests/svg/reftest.list @@ -199,10 +199,11 @@ fuzzy-if(skiaContent,0-1,0-2) == fallback-color-04.svg pass.svg == filter-basic-03.svg pass.svg == filter-bounds-01.svg pass.svg == filter-bounds-02.svg pass.svg +# Disabled for now, see bug 1286798 comment 180, 187, 190 and 196. # This pref is normally on by default, but we turn it off in reftest runs to # disable an unnecessary security-check. This reftest is actually testing that # the security check works, though, so it needs the pref to be turned on: -fails-if(Android) pref(security.fileuri.strict_origin_policy,true) == filter-extref-differentOrigin-01.svg pass.svg # Bug 695385 +skip pref(security.fileuri.strict_origin_policy,true) == filter-extref-differentOrigin-01.svg pass.svg # Bug 695385 == filter-foreignObject-01.svg pass.svg == filter-in-mask-01.svg pass.svg == filter-invalidation-01.svg pass.svg diff --git a/mobile/android/base/AndroidManifest.xml.in b/mobile/android/base/AndroidManifest.xml.in index 341fbce6629c..78f4cdc37130 100644 --- a/mobile/android/base/AndroidManifest.xml.in +++ b/mobile/android/base/AndroidManifest.xml.in @@ -40,8 +40,6 @@ #endif - - { - let req = quotaManagerService.clearStoragesForPrincipal(principal, null, false); + let req = quotaManagerService.clearStoragesForPrincipal(principal); req.callback = () => { r(); }; })); } diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testDistribution.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testDistribution.java index 54d7aa6c7d31..0af012acc8f3 100644 --- a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testDistribution.java +++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testDistribution.java @@ -385,6 +385,7 @@ public class testDistribution extends ContentProviderTest { } private void checkAddon() { + /* Bug 1511211 try { final String[] prefNames = { "distribution.test.addonEnabled" }; final JSONArray preferences = getPrefs(prefNames); @@ -393,6 +394,7 @@ public class testDistribution extends ContentProviderTest { } catch (JSONException e) { mAsserter.ok(false, "exception getting preferences", e.toString()); } + */ } private JSONArray getPrefs(String[] prefNames) throws JSONException { diff --git a/modules/libpref/init/all.js b/modules/libpref/init/all.js index d74bfa0c87a3..e9707ec6da88 100644 --- a/modules/libpref/init/all.js +++ b/modules/libpref/init/all.js @@ -1285,7 +1285,17 @@ pref("dom.disable_open_click_delay", 1000); pref("dom.serviceWorkers.disable_open_click_delay", 1000); pref("dom.storage.enabled", true); +// Whether or not LSNG (Next Generation Local Storage) is enabled. +// See bug 1510410 for enabling this on Nightly. +#ifdef NIGHTLY_BUILD +pref("dom.storage.next_gen", false); +#else +pref("dom.storage.next_gen", false); +#endif pref("dom.storage.default_quota", 5120); +pref("dom.storage.shadow_writes", true); +pref("dom.storage.snapshot_prefill", 16384); +pref("dom.storage.snapshot_reusing", true); pref("dom.storage.testing", false); pref("dom.send_after_paint_to_content", false); diff --git a/taskcluster/ci/build/linux.yml b/taskcluster/ci/build/linux.yml index 6c21f174d2b9..fe6f4cfb92b0 100644 --- a/taskcluster/ci/build/linux.yml +++ b/taskcluster/ci/build/linux.yml @@ -106,7 +106,7 @@ linux64-fuzzing/debug: symbol: Bf worker-type: aws-provisioner-v1/gecko-{level}-b-linux worker: - max-run-time: 3600 + max-run-time: 5400 env: PERFHERDER_EXTRA_OPTIONS: fuzzing FORCE_GCC: '1' @@ -143,7 +143,7 @@ linux64/debug: symbol: B worker-type: aws-provisioner-v1/gecko-{level}-b-linux worker: - max-run-time: 3600 + max-run-time: 5400 run: using: mozharness actions: [get-secrets, build, check-test] @@ -353,7 +353,7 @@ linux/debug: worker-type: aws-provisioner-v1/gecko-{level}-b-linux worker: docker-image: {in-tree: debian7-i386-build} - max-run-time: 3600 + max-run-time: 5400 run: using: mozharness actions: [get-secrets, build, check-test] @@ -575,7 +575,7 @@ linux64-asan/opt: worker: env: PERFHERDER_EXTRA_OPTIONS: "opt asan" - max-run-time: 3600 + max-run-time: 5400 run: using: mozharness actions: [get-secrets, build, check-test] @@ -609,7 +609,7 @@ linux64-asan-fuzzing/opt: worker: env: PERFHERDER_EXTRA_OPTIONS: asan-fuzzing - max-run-time: 3600 + max-run-time: 5400 run: using: mozharness actions: [get-secrets, build, check-test] @@ -644,7 +644,7 @@ linux64-asan-fuzzing-ccov/opt: worker: env: PERFHERDER_EXTRA_OPTIONS: asan-fuzzing-ccov - max-run-time: 3600 + max-run-time: 5400 run: using: mozharness actions: [get-secrets, build, check-test] @@ -676,7 +676,7 @@ linux64-fuzzing-ccov/opt: run-on-projects: ['mozilla-central', 'try'] worker-type: aws-provisioner-v1/gecko-{level}-b-linux worker: - max-run-time: 3600 + max-run-time: 5400 env: PERFHERDER_EXTRA_OPTIONS: fuzzing-ccov FORCE_GCC: '1' @@ -750,7 +750,7 @@ linux64-asan/debug: worker: env: PERFHERDER_EXTRA_OPTIONS: "debug asan" - max-run-time: 3600 + max-run-time: 5400 run: using: mozharness actions: [get-secrets, build, check-test] diff --git a/testing/mochitest/tests/Harness_sanity/mochitest.ini b/testing/mochitest/tests/Harness_sanity/mochitest.ini index 61c9fcbc41aa..c34ed6679be0 100644 --- a/testing/mochitest/tests/Harness_sanity/mochitest.ini +++ b/testing/mochitest/tests/Harness_sanity/mochitest.ini @@ -34,7 +34,7 @@ support-files = SpecialPowersLoadChromeScript.js skip-if = toolkit == 'android' || (verify && (os == 'win')) # bug 688052 [test_sanitySimpletest.html] subsuite = clipboard -skip-if = toolkit == 'android' # bug 688052 +skip-if = toolkit == 'android' && !e10s # bug 688052 [test_sanity_manifest.html] skip-if = toolkit == 'android' # we use the old manifest style on android fail-if = true diff --git a/testing/web-platform/tests/tools/wptrunner/wptrunner/executors/executormarionette.py b/testing/web-platform/tests/tools/wptrunner/wptrunner/executors/executormarionette.py index 3d7282c33475..f5b67c0b9c73 100644 --- a/testing/web-platform/tests/tools/wptrunner/wptrunner/executors/executormarionette.py +++ b/testing/web-platform/tests/tools/wptrunner/wptrunner/executors/executormarionette.py @@ -292,7 +292,7 @@ class MarionetteStorageProtocolPart(StorageProtocolPart): let principal = ssm.createCodebasePrincipal(uri, {}); let qms = Components.classes["@mozilla.org/dom/quota-manager-service;1"] .getService(Components.interfaces.nsIQuotaManagerService); - qms.clearStoragesForPrincipal(principal, "default", true); + qms.clearStoragesForPrincipal(principal, "default", null, true); """ % url with self.marionette.using_context(self.marionette.CONTEXT_CHROME): self.marionette.execute_script(script) diff --git a/toolkit/components/antitracking/test/browser/localStorage.html b/toolkit/components/antitracking/test/browser/localStorage.html index 934d7a5efa67..e08c25f2c42c 100644 --- a/toolkit/components/antitracking/test/browser/localStorage.html +++ b/toolkit/components/antitracking/test/browser/localStorage.html @@ -4,7 +4,14 @@ if (window.opener) { SpecialPowers.wrap(document).userInteractionForTesting(); localStorage.foo = "opener" + Math.random(); - window.close(); + // Don't call window.close immediatelly. It can happen that adding the + // "storage" event listener below takes more time than usual (it may need to + // synchronously subscribe in the parent process to receive storage + // notifications). Spending more time in the initial script can prevent + // the "load" event from being fired for the window opened by "open and test". + setTimeout(() => { + window.close(); + }, 0); } if (parent) { diff --git a/toolkit/components/cleardata/ClearDataService.js b/toolkit/components/cleardata/ClearDataService.js index 2464610d0059..45a0f74542ba 100644 --- a/toolkit/components/cleardata/ClearDataService.js +++ b/toolkit/components/cleardata/ClearDataService.js @@ -291,20 +291,25 @@ const AppCacheCleaner = { const QuotaCleaner = { deleteByPrincipal(aPrincipal) { - // localStorage - Services.obs.notifyObservers(null, "browser:purge-domain-data", - aPrincipal.URI.host); + if (!Services.lsm.nextGenLocalStorageEnabled) { + // localStorage: The legacy LocalStorage implementation that will + // eventually be removed depends on this observer notification to clear by + // principal. Only generate it if we're using the legacy implementation. + Services.obs.notifyObservers(null, "browser:purge-domain-data", + aPrincipal.URI.host); + } // ServiceWorkers: they must be removed before cleaning QuotaManager. return ServiceWorkerCleanUp.removeFromPrincipal(aPrincipal) .then(_ => /* exceptionThrown = */ false, _ => /* exceptionThrown = */ true) .then(exceptionThrown => { - // QuotaManager + // QuotaManager: In the event of a failure, we call reject to propagate + // the error upwards. return new Promise((aResolve, aReject) => { - let req = Services.qms.clearStoragesForPrincipal(aPrincipal, null, false); + let req = Services.qms.clearStoragesForPrincipal(aPrincipal); req.callback = () => { - if (exceptionThrown) { - aReject(); + if (exceptionThrown || req.resultCode != Cr.NS_OK) { + aReject({message: "Delete by principal failed"}); } else { aResolve(); } @@ -314,8 +319,12 @@ const QuotaCleaner = { }, deleteByHost(aHost, aOriginAttributes) { - // localStorage - Services.obs.notifyObservers(null, "browser:purge-domain-data", aHost); + if (!Services.lsm.nextGenLocalStorageEnabled) { + // localStorage: The legacy LocalStorage implementation that will + // eventually be removed depends on this observer notification to clear by + // principal. Only generate it if we're using the legacy implementation. + Services.obs.notifyObservers(null, "browser:purge-domain-data", aHost); + } let exceptionThrown = false; @@ -324,7 +333,9 @@ const QuotaCleaner = { ServiceWorkerCleanUp.removeFromHost("http://" + aHost).catch(_ => { exceptionThrown = true; }), ServiceWorkerCleanUp.removeFromHost("https://" + aHost).catch(_ => { exceptionThrown = true; }), ]).then(() => { - // QuotaManager + // QuotaManager: In the event of a failure, we call reject to propagate + // the error upwards. + // delete data from both HTTP and HTTPS sites let httpURI = Services.io.newURI("http://" + aHost); let httpsURI = Services.io.newURI("https://" + aHost); @@ -332,16 +343,62 @@ const QuotaCleaner = { .createCodebasePrincipal(httpURI, aOriginAttributes); let httpsPrincipal = Services.scriptSecurityManager .createCodebasePrincipal(httpsURI, aOriginAttributes); - return Promise.all([ - new Promise(aResolve => { - let req = Services.qms.clearStoragesForPrincipal(httpPrincipal, null, true); - req.callback = () => { aResolve(); }; - }), - new Promise(aResolve => { - let req = Services.qms.clearStoragesForPrincipal(httpsPrincipal, null, true); - req.callback = () => { aResolve(); }; - }), - ]).then(() => { + let promises = []; + promises.push(new Promise((aResolve, aReject) => { + let req = Services.qms.clearStoragesForPrincipal(httpPrincipal, null, null, true); + req.callback = () => { + if (req.resultCode == Cr.NS_OK) { + aResolve(); + } else { + aReject({message: "Delete by host failed"}); + } + }; + })); + promises.push(new Promise((aResolve, aReject) => { + let req = Services.qms.clearStoragesForPrincipal(httpsPrincipal, null, null, true); + req.callback = () => { + if (req.resultCode == Cr.NS_OK) { + aResolve(); + } else { + aReject({message: "Delete by host failed"}); + } + }; + })); + if (Services.lsm.nextGenLocalStorageEnabled) { + // deleteByHost has the semantics that "foo.example.com" should be + // wiped if we are provided an aHost of "example.com". QuotaManager + // doesn't have a way to directly do this, so we use getUsage() to + // get a list of all of the origins known to QuotaManager and then + // check whether the domain is a sub-domain of aHost. + promises.push(new Promise((aResolve, aReject) => { + Services.qms.getUsage(aRequest => { + if (aRequest.resultCode != Cr.NS_OK) { + aReject({message: "Delete by host failed"}); + return; + } + + let promises = []; + for (let item of aRequest.result) { + let principal = Services.scriptSecurityManager.createCodebasePrincipalFromOrigin(item.origin); + if (eTLDService.hasRootDomain(principal.URI.host, aHost)) { + promises.push(new Promise((aResolve, aReject) => { + let clearRequest = Services.qms.clearStoragesForPrincipal(principal, null, "ls"); + clearRequest.callback = () => { + if (clearRequest.resultCode == Cr.NS_OK) { + aResolve(); + } else { + aReject({message: "Delete by host failed"}); + } + }; + })); + } + } + + Promise.all(promises).then(aResolve); + }); + })); + } + return Promise.all(promises).then(() => { return exceptionThrown ? Promise.reject() : Promise.resolve(); }); }); @@ -375,16 +432,12 @@ const QuotaCleaner = { return ServiceWorkerCleanUp.removeAll() .then(_ => /* exceptionThrown = */ false, _ => /* exceptionThrown = */ true) .then(exceptionThrown => { - // QuotaManager + // QuotaManager: In the event of a failure, we call reject to propagate + // the error upwards. return new Promise((aResolve, aReject) => { Services.qms.getUsage(aRequest => { if (aRequest.resultCode != Cr.NS_OK) { - // We are probably shutting down. - if (exceptionThrown) { - aReject(); - } else { - aResolve(); - } + aReject({message: "Delete all failed"}); return; } @@ -394,9 +447,15 @@ const QuotaCleaner = { if (principal.URI.scheme == "http" || principal.URI.scheme == "https" || principal.URI.scheme == "file") { - promises.push(new Promise(aResolve => { - let req = Services.qms.clearStoragesForPrincipal(principal, null, false); - req.callback = () => { aResolve(); }; + promises.push(new Promise((aResolve, aReject) => { + let req = Services.qms.clearStoragesForPrincipal(principal); + req.callback = () => { + if (req.resultCode == Cr.NS_OK) { + aResolve(); + } else { + aReject({message: "Delete all failed"}); + } + }; })); } } diff --git a/toolkit/components/extensions/Extension.jsm b/toolkit/components/extensions/Extension.jsm index 39e819b1aef9..126e8fb0c0e4 100644 --- a/toolkit/components/extensions/Extension.jsm +++ b/toolkit/components/extensions/Extension.jsm @@ -244,6 +244,7 @@ var UninstallObserver = { ExtensionStorage.clear(addon.id, {shouldNotifyListeners: false})); // Clear any IndexedDB storage created by the extension + // If LSNG is enabled, this also clears localStorage. let baseURI = Services.io.newURI(`moz-extension://${uuid}/`); let principal = Services.scriptSecurityManager.createCodebasePrincipal( baseURI, {}); @@ -257,10 +258,14 @@ var UninstallObserver = { ExtensionStorageIDB.clearMigratedExtensionPref(addon.id); - // Clear localStorage created by the extension - let storage = Services.domStorageManager.getStorage(null, principal); - if (storage) { - storage.clear(); + // If LSNG is not enabled, we need to clear localStorage explicitly using + // the old API. + if (!Services.lsm.nextGenLocalStorageEnabled) { + // Clear localStorage created by the extension + let storage = Services.domStorageManager.getStorage(null, principal); + if (storage) { + storage.clear(); + } } // Remove any permissions related to the unlimitedStorage permission diff --git a/toolkit/components/telemetry/Histograms.json b/toolkit/components/telemetry/Histograms.json index 0d3b6e3817fc..7f033a739677 100644 --- a/toolkit/components/telemetry/Histograms.json +++ b/toolkit/components/telemetry/Histograms.json @@ -9866,6 +9866,17 @@ "releaseChannelCollection": "opt-out", "description": "How long has the application panel been active (seconds)." }, + "DEVTOOLS_FLEXBOX_HIGHLIGHTER_TIME_ACTIVE_SECONDS": { + "record_in_processes": ["main", "content"], + "expires_in_version": "never", + "kind": "exponential", + "high": 86400, + "n_buckets": 100, + "bug_numbers": [1509907], + "alert_emails": ["dev-developer-tools@lists.mozilla.org", "mbalfanz@mozilla.com"], + "releaseChannelCollection": "opt-out", + "description": "How long has the flexbox highlighter been active (seconds)." + }, "DEVTOOLS_TOOLBOX_TIME_ACTIVE_SECONDS": { "record_in_processes": ["main", "content"], "expires_in_version": "never", diff --git a/toolkit/components/telemetry/Scalars.yaml b/toolkit/components/telemetry/Scalars.yaml index 9999df3f554c..2d041639caf6 100644 --- a/toolkit/components/telemetry/Scalars.yaml +++ b/toolkit/components/telemetry/Scalars.yaml @@ -1038,6 +1038,62 @@ devtools.copy.xpath: record_in_processes: - 'main' +devtools.layout.flexboxhighlighter: + opened: + bug_numbers: + - 1509907 + description: > + Number of times the DevTools flexbox highlighter was activated from the layout view. + expires: never + kind: uint + notification_emails: + - dev-developer-tools@lists.mozilla.org + release_channel_collection: opt-out + record_in_processes: + - 'main' + +devtools.markup.flexboxhighlighter: + opened: + bug_numbers: + - 1509907 + description: > + Number of times the DevTools flexbox highlighter was activated from the markup view. + expires: never + kind: uint + notification_emails: + - dev-developer-tools@lists.mozilla.org + release_channel_collection: opt-out + record_in_processes: + - 'main' + +devtools.rules.flexboxhighlighter: + opened: + bug_numbers: + - 1509907 + description: > + Number of times the DevTools flexbox highlighter was activated from the rules view. + expires: never + kind: uint + notification_emails: + - dev-developer-tools@lists.mozilla.org + release_channel_collection: opt-out + record_in_processes: + - 'main' + +devtools.markup.gridinspector: + opened: + bug_numbers: + - 1509907 + description: > + Number of times the DevTools grid inspector was opened from the markup view. + expires: never + kind: uint + notification_emails: + - dev-developer-tools@lists.mozilla.org + release_channel_collection: opt-out + record_in_processes: + - 'main' + devtools.rules.gridinspector: opened: bug_numbers: diff --git a/toolkit/forgetaboutsite/test/unit/test_removeDataFromDomain.js b/toolkit/forgetaboutsite/test/unit/test_removeDataFromDomain.js index 3927e4fe1d44..20f6ba7d8a9b 100644 --- a/toolkit/forgetaboutsite/test/unit/test_removeDataFromDomain.js +++ b/toolkit/forgetaboutsite/test/unit/test_removeDataFromDomain.js @@ -308,34 +308,13 @@ async function test_permission_manager_not_cleared_with_uri_contains_domain() { check_permission_exists(TEST_URI, false); } -function waitForPurgeNotification() { - return new Promise(resolve => { - - let observer = { - observe(aSubject, aTopic, aData) { - Services.obs.removeObserver(observer, "browser:purge-domain-data"); - // test_storage_cleared needs this extra executeSoon because - // the DOMStorage clean-up is also listening to this same observer - // which is run synchronously. - Services.tm.dispatchToMainThread(function() { - resolve(); - }); - }, - }; - Services.obs.addObserver(observer, "browser:purge-domain-data"); - - }); -} - // Content Preferences async function test_content_preferences_cleared_with_direct_match() { const TEST_URI = Services.io.newURI("http://mozilla.org"); Assert.equal(false, await preference_exists(TEST_URI)); await add_preference(TEST_URI); Assert.ok(await preference_exists(TEST_URI)); - let promisePurgeNotification = waitForPurgeNotification(); await ForgetAboutSite.removeDataFromDomain("mozilla.org"); - await promisePurgeNotification; Assert.equal(false, await preference_exists(TEST_URI)); } @@ -344,9 +323,7 @@ async function test_content_preferences_cleared_with_subdomain() { Assert.equal(false, await preference_exists(TEST_URI)); await add_preference(TEST_URI); Assert.ok(await preference_exists(TEST_URI)); - let promisePurgeNotification = waitForPurgeNotification(); await ForgetAboutSite.removeDataFromDomain("mozilla.org"); - await promisePurgeNotification; Assert.equal(false, await preference_exists(TEST_URI)); } @@ -355,15 +332,11 @@ async function test_content_preferences_not_cleared_with_uri_contains_domain() { Assert.equal(false, await preference_exists(TEST_URI)); await add_preference(TEST_URI); Assert.ok(await preference_exists(TEST_URI)); - let promisePurgeNotification = waitForPurgeNotification(); await ForgetAboutSite.removeDataFromDomain("mozilla.org"); - await promisePurgeNotification; Assert.ok(await preference_exists(TEST_URI)); // Reset state - promisePurgeNotification = waitForPurgeNotification(); await ForgetAboutSite.removeDataFromDomain("ilovemozilla.org"); - await promisePurgeNotification; Assert.equal(false, await preference_exists(TEST_URI)); } @@ -428,9 +401,7 @@ async function test_push_cleared() { }); Assert.ok(await push_registration_exists(TEST_URL, ps)); - let promisePurgeNotification = waitForPurgeNotification(); await ForgetAboutSite.removeDataFromDomain("mozilla.org"); - await promisePurgeNotification; Assert.equal(false, await push_registration_exists(TEST_URL, ps)); } finally { @@ -482,9 +453,7 @@ async function test_storage_cleared() { Assert.equal(storage.getItem("test"), "value" + i); } - let promisePurgeNotification = waitForPurgeNotification(); await ForgetAboutSite.removeDataFromDomain("mozilla.org"); - await promisePurgeNotification; Assert.equal(s[0].getItem("test"), null); Assert.equal(s[0].length, 0); diff --git a/toolkit/modules/Services.jsm b/toolkit/modules/Services.jsm index 3e56626ec7ff..bf464534e9ed 100644 --- a/toolkit/modules/Services.jsm +++ b/toolkit/modules/Services.jsm @@ -80,6 +80,7 @@ var initTable = { scriptSecurityManager: ["@mozilla.org/scriptsecuritymanager;1", "nsIScriptSecurityManager"], storage: ["@mozilla.org/storage/service;1", "mozIStorageService"], domStorageManager: ["@mozilla.org/dom/localStorage-manager;1", "nsIDOMStorageManager"], + lsm: ["@mozilla.org/dom/localStorage-manager;1", "nsILocalStorageManager"], strings: ["@mozilla.org/intl/stringbundle;1", "nsIStringBundleService"], telemetry: ["@mozilla.org/base/telemetry;1", "nsITelemetry"], textToSubURI: ["@mozilla.org/intl/texttosuburi;1", "nsITextToSubURI"], diff --git a/toolkit/mozapps/extensions/AddonManager.jsm b/toolkit/mozapps/extensions/AddonManager.jsm index 3f0ab7affbd8..60bbf764e19a 100644 --- a/toolkit/mozapps/extensions/AddonManager.jsm +++ b/toolkit/mozapps/extensions/AddonManager.jsm @@ -551,6 +551,7 @@ var AddonManagerInternal = { // Store telemetry details per addon provider telemetryDetails: {}, upgradeListeners: new Map(), + externalExtensionLoaders: new Map(), recordTimestamp(name, value) { this.TelemetryTimestamps.add(name, value); @@ -2032,6 +2033,10 @@ var AddonManagerInternal = { } }, + addExternalExtensionLoader(loader) { + this.externalExtensionLoaders.set(loader.name, loader); + }, + /** * Installs a temporary add-on from a local file or directory. * @@ -2941,6 +2946,10 @@ var AddonManagerPrivate = { return AddonManagerInternal.upgradeListeners.get(aId); }, + get externalExtensionLoaders() { + return AddonManagerInternal.externalExtensionLoaders; + }, + /** * Predicate that returns true if we think the given extension ID * might have been generated by XPIProvider. @@ -3353,6 +3362,11 @@ var AddonManager = { removeUpgradeListener(aInstanceID) { return AddonManagerInternal.removeUpgradeListener(aInstanceID); }, + + addExternalExtensionLoader(types, loader) { + return AddonManagerInternal.addExternalExtensionLoader(types, loader); + }, + addAddonListener(aListener) { AddonManagerInternal.addAddonListener(aListener); }, diff --git a/toolkit/mozapps/extensions/AddonManagerStartup.cpp b/toolkit/mozapps/extensions/AddonManagerStartup.cpp index f46c4d02fe60..a0306f5b42e7 100644 --- a/toolkit/mozapps/extensions/AddonManagerStartup.cpp +++ b/toolkit/mozapps/extensions/AddonManagerStartup.cpp @@ -418,7 +418,7 @@ public: bool ShouldCheckStartupModifications() { - return Type().EqualsLiteral("webextension-langpack"); + return Type().EqualsLiteral("locale"); } diff --git a/toolkit/mozapps/extensions/internal/AddonTestUtils.jsm b/toolkit/mozapps/extensions/internal/AddonTestUtils.jsm index e811b727b0a9..7af1b528fb30 100644 --- a/toolkit/mozapps/extensions/internal/AddonTestUtils.jsm +++ b/toolkit/mozapps/extensions/internal/AddonTestUtils.jsm @@ -672,9 +672,15 @@ var AddonTestUtils = { callback = callback.wrappedJSObject; try { - let manifestURI = this.getManifestURI(file); - - let id = await this.getIDFromManifest(manifestURI); + let id; + try { + let manifestURI = this.getManifestURI(file); + id = await this.getIDFromManifest(manifestURI); + } catch (err) { + if (file.leafName.endsWith(".xpi")) { + id = file.leafName.slice(0, -4); + } + } let fakeCert = {commonName: id}; if (this.usePrivilegedSignatures) { diff --git a/toolkit/mozapps/extensions/internal/RDFManifestConverter.jsm b/toolkit/mozapps/extensions/internal/RDFManifestConverter.jsm index c792a8b60c09..0b63d7c6e7a9 100644 --- a/toolkit/mozapps/extensions/internal/RDFManifestConverter.jsm +++ b/toolkit/mozapps/extensions/internal/RDFManifestConverter.jsm @@ -3,7 +3,7 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ "use strict"; -var EXPORTED_SYMBOLS = ["InstallRDF", "UpdateRDFConverter"]; +var EXPORTED_SYMBOLS = ["UpdateRDFConverter"]; ChromeUtils.defineModuleGetter(this, "RDFDataSource", "resource://gre/modules/addons/RDFDataSource.jsm"); @@ -16,8 +16,6 @@ const PREFIX_THEME = "urn:mozilla:theme:"; const TOOLKIT_ID = "toolkit@mozilla.org"; -const RDFURI_INSTALL_MANIFEST_ROOT = "urn:mozilla:install-manifest"; - function EM_R(aProperty) { return `http://www.mozilla.org/2004/em-rdf#${aProperty}`; } @@ -52,75 +50,6 @@ class Manifest { } } -class InstallRDF extends Manifest { - _readProps(source, obj, props) { - for (let prop of props) { - let val = getProperty(source, prop); - if (val != null) { - obj[prop] = val; - } - } - } - - _readArrayProp(source, obj, prop, target, decode = getValue) { - let result = Array.from(source.getObjects(EM_R(prop)), - target => decode(target)); - if (result.length) { - obj[target] = result; - } - } - - _readArrayProps(source, obj, props, decode = getValue) { - for (let [prop, target] of Object.entries(props)) { - this._readArrayProp(source, obj, prop, target, decode); - } - } - - _readLocaleStrings(source, obj) { - this._readProps(source, obj, ["name", "description", "creator", "homepageURL"]); - this._readArrayProps(source, obj, { - locale: "locales", - developer: "developers", - translator: "translators", - contributor: "contributors", - }); - } - - decode() { - let root = this.ds.getResource(RDFURI_INSTALL_MANIFEST_ROOT); - let result = {}; - - let props = ["id", "version", "type", "updateURL", "optionsURL", - "optionsType", "aboutURL", "iconURL", - "bootstrap", "unpack", "strictCompatibility"]; - this._readProps(root, result, props); - - let decodeTargetApplication = source => { - let app = {}; - this._readProps(source, app, ["id", "minVersion", "maxVersion"]); - return app; - }; - - let decodeLocale = source => { - let localized = {}; - this._readLocaleStrings(source, localized); - return localized; - }; - - this._readLocaleStrings(root, result); - - this._readArrayProps(root, result, {"targetPlatform": "targetPlatforms"}); - this._readArrayProps(root, result, {"targetApplication": "targetApplications"}, - decodeTargetApplication); - this._readArrayProps(root, result, {"localized": "localized"}, - decodeLocale); - this._readArrayProps(root, result, {"dependency": "dependencies"}, - source => getProperty(source, "id")); - - return result; - } -} - class UpdateRDF extends Manifest { decode() { let addons = {}; diff --git a/toolkit/mozapps/extensions/internal/XPIDatabase.jsm b/toolkit/mozapps/extensions/internal/XPIDatabase.jsm index f4ac2ad02493..d4e549a0ca08 100644 --- a/toolkit/mozapps/extensions/internal/XPIDatabase.jsm +++ b/toolkit/mozapps/extensions/internal/XPIDatabase.jsm @@ -47,14 +47,12 @@ const {nsIBlocklistService} = Ci; * BOOTSTRAP_REASONS, * DB_SCHEMA, * XPIStates, - * isWebExtension, */ for (let sym of [ "BOOTSTRAP_REASONS", "DB_SCHEMA", "XPIStates", - "isWebExtension", ]) { XPCOMUtils.defineLazyGetter(this, sym, () => XPIInternal[sym]); } @@ -97,15 +95,9 @@ const PENDING_INSTALL_METADATA = "updateDate", "applyBackgroundUpdates", "compatibilityOverrides", "installTelemetryInfo"]; -const COMPATIBLE_BY_DEFAULT_TYPES = { - extension: true, - dictionary: true, - "webextension-dictionary": true, -}; - // Properties to save in JSON file const PROP_JSON_FIELDS = ["id", "syncGUID", "version", "type", - "updateURL", "optionsURL", + "loader", "updateURL", "optionsURL", "optionsType", "optionsBrowserStyle", "aboutURL", "defaultLocale", "visible", "active", "userDisabled", "appDisabled", "pendingUninstall", "installDate", @@ -123,20 +115,10 @@ const LEGACY_TYPES = new Set([ "extension", ]); -// Some add-on types that we track internally are presented as other types -// externally -const TYPE_ALIASES = { - "webextension": "extension", - "webextension-dictionary": "dictionary", - "webextension-langpack": "locale", - "webextension-theme": "theme", -}; - const SIGNED_TYPES = new Set([ "extension", - "webextension", - "webextension-langpack", - "webextension-theme", + "locale", + "theme", ]); // Time to wait before async save of XPI JSON database, in milliseconds @@ -197,46 +179,6 @@ async function getRepositoryAddon(aAddon) { return aAddon; } -/** - * Helper function that determines whether an addon of a certain type is a - * theme. - * - * @param {string} type - * The add-on type to check. - * @returns {boolean} - */ -function isTheme(type) { - return type == "theme" || TYPE_ALIASES[type] == "theme"; -} - -/** - * Converts a list of API types to a list of API types and any aliases for those - * types. - * - * @param {Array?} aTypes - * An array of types or null for all types - * @returns {Set?} - * An set of types or null for all types - */ -function getAllAliasesForTypes(aTypes) { - if (!aTypes) - return null; - - let types = new Set(aTypes); - for (let [alias, type] of Object.entries(TYPE_ALIASES)) { - // Add any alias for the internal type - if (types.has(type)) { - types.add(alias); - } else { - // If this internal type was explicitly requested and its external - // type wasn't, ignore it. - types.delete(alias); - } - } - - return types; -} - /** * Copies properties from one object to another. If no target object is passed * a new object will be created and returned. @@ -334,6 +276,10 @@ class AddonInternal { this.inDatabase = true; } + get isWebExtension() { + return this.loader == null; + } + get selectedLocale() { if (this._selectedLocale) return this._selectedLocale; @@ -494,10 +440,8 @@ class AddonInternal { // Only extensions and dictionaries can be compatible by default; themes // and language packs always use strict compatibility checking. // Dictionaries are compatible by default unless requested by the dictinary. - if (this.type in COMPATIBLE_BY_DEFAULT_TYPES && - !this.strictCompatibility && - (!AddonManager.strictCompatibility || - this.type == "webextension-dictionary")) { + if (!this.strictCompatibility && + (!AddonManager.strictCompatibility || this.type == "dictionary")) { // The repository can specify compatibility overrides. // Note: For now, only blacklisting is supported by overrides. @@ -729,14 +673,6 @@ AddonWrapper = class { return addon.installTelemetryInfo; } - get type() { - return XPIDatabase.getExternalType(addonFor(this).type); - } - - get isWebExtension() { - return isWebExtension(addonFor(this).type); - } - get temporarilyInstalled() { return addonFor(this).location.isTemporary; } @@ -987,8 +923,8 @@ AddonWrapper = class { if (addon.inDatabase) { // When softDisabling a theme just enable the active theme - if (isTheme(addon.type) && val && !addon.userDisabled) { - if (isWebExtension(addon.type)) + if (addon.type === "theme" && val && !addon.userDisabled) { + if (addon.isWebExtension) XPIDatabase.updateAddonDisabledState(addon, undefined, val); } else { XPIDatabase.updateAddonDisabledState(addon, undefined, val); @@ -1111,7 +1047,8 @@ function defineAddonWrapperProperty(name, getter) { }); } -["id", "syncGUID", "version", "isCompatible", "isPlatformCompatible", +["id", "syncGUID", "version", "type", "isWebExtension", + "isCompatible", "isPlatformCompatible", "providesUpdatesSecurely", "blocklistState", "appDisabled", "softDisabled", "skinnable", "foreignInstall", "strictCompatibility", "updateURL", "dependencies", @@ -1378,7 +1315,42 @@ this.XPIDatabase = { throw error; } - if (inputAddons.schemaVersion != DB_SCHEMA) { + if (inputAddons.schemaVersion == 27) { + // Types were translated in bug 857456. + for (let addon of inputAddons.addons) { + switch (addon.type) { + case "extension": + case "dictionary": + case "locale": + case "theme": + addon.loader = "bootstrap"; + break; + + case "webbextension": + addon.type = "extension"; + addon.loader = null; + break; + + case "webextension-dictionary": + addon.type = "dictionary"; + addon.loader = null; + break; + + case "webextension-langpack": + addon.type = "locale"; + addon.loader = null; + break; + + case "webextension-theme": + addon.type = "theme"; + addon.loader = null; + break; + + default: + logger.warn(`Not converting unknown addon type ${addon.type}`); + } + } + } else if (inputAddons.schemaVersion != DB_SCHEMA) { // For now, we assume compatibility for JSON data with a // mismatched schema version, though we throw away any fields we // don't know about (bug 902956) @@ -1622,10 +1594,10 @@ this.XPIDatabase = { */ async addonChanged(aId, aType) { // We only care about themes in this provider - if (!isTheme(aType)) + if (aType !== "theme") return; - let addons = this.getAddonsByType("webextension-theme"); + let addons = this.getAddonsByType("theme"); for (let theme of addons) { if (theme.visible && theme.id != aId) await this.updateAddonDisabledState(theme, true, undefined, true); @@ -1642,21 +1614,6 @@ this.XPIDatabase = { } }, - /** - * Converts an internal add-on type to the type presented through the API. - * - * @param {string} aType - * The internal add-on type - * @returns {string} - * An external add-on type - */ - getExternalType(aType) { - if (aType in TYPE_ALIASES) - return TYPE_ALIASES[aType]; - return aType; - }, - - isTheme, SIGNED_TYPES, /** @@ -1830,7 +1787,7 @@ this.XPIDatabase = { * @returns {Addon[]} */ async getAddonsByTypes(aTypes) { - let addons = await this.getVisibleAddons(getAllAliasesForTypes(aTypes)); + let addons = await this.getVisibleAddons(aTypes ? new Set(aTypes) : null); return addons.map(a => a.wrapper); }, @@ -1845,7 +1802,7 @@ this.XPIDatabase = { if (!SIGNED_TYPES.has(aType)) return false; - if (aType == "webextension-langpack") { + if (aType == "locale") { return AddonSettings.LANGPACKS_REQUIRE_SIGNING; } @@ -1861,6 +1818,7 @@ this.XPIDatabase = { */ isDisabledLegacy(addon) { return (!AddonSettings.ALLOW_LEGACY_EXTENSIONS && + !addon.isWebExtension && LEGACY_TYPES.has(addon.type) && // Legacy add-ons are allowed in the system location. @@ -2243,7 +2201,7 @@ this.XPIDatabase = { } // Notify any other providers that a new theme has been enabled - if (isTheme(aAddon.type)) { + if (aAddon.type === "theme") { if (!isDisabled) { AddonManagerPrivate.notifyAddonChanged(aAddon.id, aAddon.type); this.updateXPIStates(aAddon); @@ -2895,7 +2853,7 @@ this.XPIDatabaseReconcile = { } else if (xpiState && xpiState.wasRestored) { isActive = xpiState.enabled; - if (currentAddon.type == "webextension-theme") + if (currentAddon.isWebExtension && currentAddon.type == "theme") currentAddon.userDisabled = !isActive; // If the add-on wasn't active and it isn't already disabled in some way diff --git a/toolkit/mozapps/extensions/internal/XPIInstall.jsm b/toolkit/mozapps/extensions/internal/XPIInstall.jsm index 1191ba7704e5..c3c0dd4b1223 100644 --- a/toolkit/mozapps/extensions/internal/XPIInstall.jsm +++ b/toolkit/mozapps/extensions/internal/XPIInstall.jsm @@ -43,7 +43,6 @@ XPCOMUtils.defineLazyModuleGetters(this, { UpdateUtils: "resource://gre/modules/UpdateUtils.jsm", AddonInternal: "resource://gre/modules/addons/XPIDatabase.jsm", - InstallRDF: "resource://gre/modules/addons/RDFManifestConverter.jsm", XPIDatabase: "resource://gre/modules/addons/XPIDatabase.jsm", XPIInternal: "resource://gre/modules/addons/XPIProvider.jsm", }); @@ -75,8 +74,6 @@ XPCOMUtils.defineLazyServiceGetters(this, { gCertDB: ["@mozilla.org/security/x509certdb;1", "nsIX509CertDB"], }); -const hasOwnProperty = Function.call.bind(Object.prototype.hasOwnProperty); - const PREF_INSTALL_REQUIRESECUREORIGIN = "extensions.install.requireSecureOrigin"; const PREF_PENDING_OPERATIONS = "extensions.pendingOperations"; const PREF_SYSTEM_ADDON_UPDATE_URL = "extensions.systemAddon.update.url"; @@ -87,7 +84,7 @@ const PREF_XPI_WHITELIST_REQUIRED = "xpinstall.whitelist.required"; const TOOLKIT_ID = "toolkit@mozilla.org"; -/* globals BOOTSTRAP_REASONS, KEY_APP_SYSTEM_ADDONS, KEY_APP_SYSTEM_DEFAULTS, PREF_BRANCH_INSTALLED_ADDON, PREF_SYSTEM_ADDON_SET, TEMPORARY_ADDON_SUFFIX, XPI_PERMISSION, XPIStates, isWebExtension, iterDirectory */ +/* globals BOOTSTRAP_REASONS, KEY_APP_SYSTEM_ADDONS, KEY_APP_SYSTEM_DEFAULTS, PREF_BRANCH_INSTALLED_ADDON, PREF_SYSTEM_ADDON_SET, TEMPORARY_ADDON_SUFFIX, XPI_PERMISSION, XPIStates, iterDirectory */ const XPI_INTERNAL_SYMBOLS = [ "BOOTSTRAP_REASONS", "KEY_APP_SYSTEM_ADDONS", @@ -97,7 +94,6 @@ const XPI_INTERNAL_SYMBOLS = [ "TEMPORARY_ADDON_SUFFIX", "XPI_PERMISSION", "XPIStates", - "isWebExtension", "iterDirectory", ]; @@ -105,10 +101,6 @@ for (let name of XPI_INTERNAL_SYMBOLS) { XPCOMUtils.defineLazyGetter(this, name, () => XPIInternal[name]); } -function isTheme(type) { - return XPIDatabase.isTheme(type); -} - /** * Returns a nsIFile instance for the given path, relative to the given * base file, if provided. @@ -162,30 +154,6 @@ const KEY_APP_PROFILE = "app-profile"; const DIR_STAGE = "staged"; const DIR_TRASH = "trash"; -// Properties that exist in the install manifest -const PROP_METADATA = ["id", "version", "type", "internalName", "updateURL", - "optionsURL", "optionsType", "aboutURL", "iconURL"]; -const PROP_LOCALE_SINGLE = ["name", "description", "creator", "homepageURL"]; -const PROP_LOCALE_MULTI = ["developers", "translators", "contributors"]; - -// Map new string type identifiers to old style nsIUpdateItem types. -// Retired values: -// 32 = multipackage xpi file -// 8 = locale -// 256 = apiextension -// 128 = experiment -// theme = 4 -const TYPES = { - extension: 2, - dictionary: 64, -}; - -const COMPATIBLE_BY_DEFAULT_TYPES = { - extension: true, - dictionary: true, - "webextension-dictionary": true, -}; - // This is a random number array that can be used as "salt" when generating // an automatic ID based on the directory path of an add-on. It will prevent // someone from creating an ID for a permanent add-on that could be replaced @@ -412,7 +380,7 @@ function waitForAllPromises(promises) { } /** - * Reads an AddonInternal object from a manifest stream. + * Reads an AddonInternal object from a webextension manifest.json * * @param {nsIURI} aUri * A |file:| or |jar:| URL for the manifest @@ -454,8 +422,8 @@ async function loadManifestFromWebManifest(aUri, aPackage) { let addon = new AddonInternal(); addon.id = bss.id; addon.version = manifest.version; - addon.type = extension.type === "extension" ? - "webextension" : `webextension-${extension.type}`; + addon.type = extension.type === "langpack" ? "locale" : extension.type; + addon.loader = null; addon.strictCompatibility = true; addon.internalName = null; addon.updateURL = bss.update_url; @@ -467,7 +435,7 @@ async function loadManifestFromWebManifest(aUri, aPackage) { addon.startupData = extension.startupData; addon.hidden = manifest.hidden; - if (isTheme(addon.type) && await aPackage.hasResource("preview.png")) { + if (addon.type === "theme" && await aPackage.hasResource("preview.png")) { addon.previewImage = "preview.png"; } @@ -542,199 +510,6 @@ async function loadManifestFromWebManifest(aUri, aPackage) { return addon; } -/** - * Reads an AddonInternal object from an RDF stream. - * - * @param {nsIURI} aUri - * The URI that the manifest is being read from - * @param {string} aData - * The manifest text - * @param {InstallPackage} aPackage - * An install package instance for the extension. - * @returns {AddonInternal} - * @throws if the install manifest in the RDF stream is corrupt or could not - * be read - */ -async function loadManifestFromRDF(aUri, aData, aPackage) { - /** - * Reads locale properties from either the main install manifest root or - * an em:localized section in the install manifest. - * - * @param {Object} aSource - * The resource to read the properties from. - * @param {boolean} isDefault - * True if the locale is to be read from the main install manifest - * root - * @param {string[]} aSeenLocales - * An array of locale names already seen for this install manifest. - * Any locale names seen as a part of this function will be added to - * this array - * @returns {Object} - * an object containing the locale properties - */ - function readLocale(aSource, isDefault, aSeenLocales) { - let locale = {}; - if (!isDefault) { - locale.locales = []; - for (let localeName of aSource.locales || []) { - if (!localeName) { - logger.warn("Ignoring empty locale in localized properties"); - continue; - } - if (aSeenLocales.includes(localeName)) { - logger.warn("Ignoring duplicate locale in localized properties"); - continue; - } - aSeenLocales.push(localeName); - locale.locales.push(localeName); - } - - if (locale.locales.length == 0) { - logger.warn("Ignoring localized properties with no listed locales"); - return null; - } - } - - for (let prop of [...PROP_LOCALE_SINGLE, ...PROP_LOCALE_MULTI]) { - if (hasOwnProperty(aSource, prop)) { - locale[prop] = aSource[prop]; - } - } - - return locale; - } - - let manifest = InstallRDF.loadFromString(aData).decode(); - - let addon = new AddonInternal(); - for (let prop of PROP_METADATA) { - if (hasOwnProperty(manifest, prop)) { - addon[prop] = manifest[prop]; - } - } - - if (!addon.type) { - addon.type = "extension"; - } else { - let type = addon.type; - addon.type = null; - for (let name in TYPES) { - if (TYPES[name] == type) { - addon.type = name; - break; - } - } - } - - if (!(addon.type in TYPES)) - throw new Error("Install manifest specifies unknown type: " + addon.type); - - if (!addon.id) - throw new Error("No ID in install manifest"); - if (!gIDTest.test(addon.id)) - throw new Error("Illegal add-on ID " + addon.id); - if (!addon.version) - throw new Error("No version in install manifest"); - - addon.strictCompatibility = (!(addon.type in COMPATIBLE_BY_DEFAULT_TYPES) || - manifest.strictCompatibility == "true"); - - // Only read these properties for extensions. - if (addon.type == "extension") { - if (manifest.bootstrap != "true") { - throw new Error("Non-restartless extensions no longer supported"); - } - - if (addon.optionsType && - addon.optionsType != AddonManager.OPTIONS_TYPE_INLINE_BROWSER && - addon.optionsType != AddonManager.OPTIONS_TYPE_TAB) { - throw new Error("Install manifest specifies unknown optionsType: " + addon.optionsType); - } - } else { - // Convert legacy dictionaries into a format the WebExtension - // dictionary loader can process. - if (addon.type === "dictionary") { - addon.type = "webextension-dictionary"; - let dictionaries = {}; - await aPackage.iterFiles(({path}) => { - let match = /^dictionaries\/([^\/]+)\.dic$/.exec(path); - if (match) { - let lang = match[1].replace(/_/g, "-"); - dictionaries[lang] = match[0]; - } - }); - addon.startupData = {dictionaries}; - } - - // Only extensions are allowed to provide an optionsURL, optionsType, - // optionsBrowserStyle, or aboutURL. For all other types they are silently ignored - addon.aboutURL = null; - addon.optionsBrowserStyle = null; - addon.optionsType = null; - addon.optionsURL = null; - } - - addon.defaultLocale = readLocale(manifest, true); - - let seenLocales = []; - addon.locales = []; - for (let localeData of manifest.localized || []) { - let locale = readLocale(localeData, false, seenLocales); - if (locale) - addon.locales.push(locale); - } - - let dependencies = new Set(manifest.dependencies); - addon.dependencies = Object.freeze(Array.from(dependencies)); - - let seenApplications = []; - addon.targetApplications = []; - for (let targetApp of manifest.targetApplications || []) { - if (!targetApp.id || !targetApp.minVersion || - !targetApp.maxVersion) { - logger.warn("Ignoring invalid targetApplication entry in install manifest"); - continue; - } - if (seenApplications.includes(targetApp.id)) { - logger.warn("Ignoring duplicate targetApplication entry for " + targetApp.id + - " in install manifest"); - continue; - } - seenApplications.push(targetApp.id); - addon.targetApplications.push(targetApp); - } - - // Note that we don't need to check for duplicate targetPlatform entries since - // the RDF service coalesces them for us. - addon.targetPlatforms = []; - for (let targetPlatform of manifest.targetPlatforms || []) { - let platform = { - os: null, - abi: null, - }; - - let pos = targetPlatform.indexOf("_"); - if (pos != -1) { - platform.os = targetPlatform.substring(0, pos); - platform.abi = targetPlatform.substring(pos + 1); - } else { - platform.os = targetPlatform; - } - - addon.targetPlatforms.push(platform); - } - - addon.userDisabled = false; - addon.softDisabled = addon.blocklistState == nsIBlocklistService.STATE_SOFTBLOCKED; - addon.applyBackgroundUpdates = AddonManager.AUTOUPDATE_DEFAULT; - - // icons will be filled by the calling function - addon.icons = {}; - addon.userPermissions = null; - - return addon; -} - function defineSyncGUID(aAddon) { // Define .syncGUID as a lazy property which is also settable Object.defineProperty(aAddon, "syncGUID", { @@ -765,32 +540,23 @@ function generateTemporaryInstallID(aFile) { } var loadManifest = async function(aPackage, aLocation, aOldAddon) { - async function loadFromRDF(aUri) { - let manifest = await aPackage.readString("install.rdf"); - let addon = await loadManifestFromRDF(aUri, manifest, aPackage); - - if (await aPackage.hasResource("icon.png")) { - addon.icons[32] = "icon.png"; - addon.icons[48] = "icon.png"; + let addon; + if (await aPackage.hasResource("manifest.json")) { + addon = await loadManifestFromWebManifest(aPackage.rootURI, aPackage); + } else { + for (let loader of AddonManagerPrivate.externalExtensionLoaders.values()) { + if (await aPackage.hasResource(loader.manifestFile)) { + addon = await loader.loadManifest(aPackage); + addon.loader = loader.name; + break; + } } - - if (await aPackage.hasResource("icon64.png")) { - addon.icons[64] = "icon64.png"; - } - - return addon; } - let entry = await aPackage.getManifestFile(); - if (!entry) { + if (!addon) { throw new Error(`File ${aPackage.filePath} does not contain a valid manifest`); } - let isWebExtension = entry == "manifest.json"; - let addon = isWebExtension ? - await loadManifestFromWebManifest(aPackage.rootURI, aPackage) : - await loadFromRDF(aPackage.getURI("install.rdf")); - addon._sourceBundle = aPackage.file; addon.location = aLocation; @@ -800,11 +566,11 @@ var loadManifest = async function(aPackage, aLocation, aOldAddon) { addon.hidden = false; } - if (isWebExtension && !addon.id) { + if (!addon.id) { if (cert) { addon.id = cert.commonName; if (!gIDTest.test(addon.id)) { - throw new Error(`Webextension is signed with an invalid id (${addon.id})`); + throw new Error(`Extension is signed with an invalid id (${addon.id})`); } } if (!addon.id && aLocation.isTemporary) { @@ -1561,7 +1327,7 @@ class AddonInstall { `Refusing to upgrade addon ${this.existingAddon.id} to different ID ${this.addon.id}`]); } - if (isWebExtension(this.existingAddon.type) && !isWebExtension(this.addon.type)) { + if (this.existingAddon.isWebExtension && !this.addon.isWebExtension) { return Promise.reject([AddonManager.ERROR_UNEXPECTED_ADDON_TYPE, "WebExtensions may not be updated to other extension types"]); } @@ -1790,7 +1556,7 @@ class AddonInstall { XPIDatabase.recordAddonTelemetry(this.addon); // Notify providers that a new theme has been enabled. - if (isTheme(this.addon.type) && this.addon.active) + if (this.addon.type === "theme" && this.addon.active) AddonManagerPrivate.notifyAddonChanged(this.addon.id, this.addon.type); }; @@ -2499,7 +2265,7 @@ AddonInstallWrapper.prototype = { }, get type() { - return XPIDatabase.getExternalType(installFor(this).type); + return installFor(this).type; }, get iconURL() { @@ -2653,12 +2419,11 @@ UpdateChecker.prototype = { let AUC = AddonUpdateChecker; let ignoreMaxVersion = false; // Ignore strict compatibility for dictionaries by default. - let ignoreStrictCompat = (this.addon.type == "webextension-dictionary"); + let ignoreStrictCompat = (this.addon.type == "dictionary"); if (!AddonManager.checkCompatibility) { ignoreMaxVersion = true; ignoreStrictCompat = true; - } else if (this.addon.type in COMPATIBLE_BY_DEFAULT_TYPES && - !AddonManager.strictCompatibility && + } else if (!AddonManager.strictCompatibility && !this.addon.strictCompatibility) { ignoreMaxVersion = true; } @@ -3857,7 +3622,7 @@ var XPIInstall = { let results = [...this.installs]; if (aTypes) { results = results.filter(install => { - return aTypes.includes(XPIDatabase.getExternalType(install.type)); + return aTypes.includes(install.type); }); } @@ -3952,7 +3717,7 @@ var XPIInstall = { AddonManagerPrivate.callAddonListeners("onInstalled", addon.wrapper); // Notify providers that a new theme has been enabled. - if (isTheme(addon.type)) + if (addon.type === "theme") AddonManagerPrivate.notifyAddonChanged(addon.id, addon.type, false); return addon.wrapper; @@ -4069,7 +3834,7 @@ var XPIInstall = { } // Notify any other providers that a new theme has been enabled - if (isTheme(aAddon.type) && aAddon.active) + if (aAddon.type === "theme" && aAddon.active) AddonManagerPrivate.notifyAddonChanged(null, aAddon.type); }, @@ -4110,7 +3875,7 @@ var XPIInstall = { } // Notify any other providers that this theme is now enabled again. - if (isTheme(aAddon.type) && aAddon.active) + if (aAddon.type === "theme" && aAddon.active) AddonManagerPrivate.notifyAddonChanged(aAddon.id, aAddon.type, false); }, diff --git a/toolkit/mozapps/extensions/internal/XPIProvider.jsm b/toolkit/mozapps/extensions/internal/XPIProvider.jsm index 22be52811fd3..541cb9f9a200 100644 --- a/toolkit/mozapps/extensions/internal/XPIProvider.jsm +++ b/toolkit/mozapps/extensions/internal/XPIProvider.jsm @@ -33,7 +33,6 @@ XPCOMUtils.defineLazyModuleGetters(this, { Langpack: "resource://gre/modules/Extension.jsm", FileUtils: "resource://gre/modules/FileUtils.jsm", OS: "resource://gre/modules/osfile.jsm", - ConsoleAPI: "resource://gre/modules/Console.jsm", JSONFile: "resource://gre/modules/JSONFile.jsm", TelemetrySession: "resource://gre/modules/TelemetrySession.jsm", @@ -105,7 +104,7 @@ const XPI_PERMISSION = "install"; const XPI_SIGNATURE_CHECK_PERIOD = 24 * 60 * 60; -const DB_SCHEMA = 27; +const DB_SCHEMA = 28; const NOTIFICATION_TOOLBOX_CONNECTION_CHANGE = "toolbox-connection-change"; @@ -222,18 +221,6 @@ function getFile(path, base = null) { return file; } -/** - * Helper function that determines whether an addon of a certain type is a - * WebExtension. - * - * @param {string} type - * The add-on type to check. - * @returns {boolean} - */ -function isWebExtension(type) { - return type == "webextension" || type == "webextension-theme"; -} - /** * Returns true if the given file, based on its name, should be treated * as an XPI. If the file does not have an appropriate extension, it is @@ -375,6 +362,7 @@ const JSON_FIELDS = Object.freeze([ "dependencies", "enabled", "file", + "loader", "lastModifiedTime", "path", "runInSafeMode", @@ -459,6 +447,7 @@ class XPIState { dependencies: this.dependencies, enabled: this.enabled, lastModifiedTime: this.lastModifiedTime, + loader: this.loader, path: this.relativePath, runInSafeMode: this.runInSafeMode, signedState: this.signedState, @@ -474,6 +463,10 @@ class XPIState { return json; } + get isWebExtension() { + return this.loader == null; + } + /** * Update the last modified time for an add-on on disk. * @@ -531,6 +524,8 @@ class XPIState { this.version = aDBAddon.version; this.type = aDBAddon.type; + this.loader = aDBAddon.loader; + if (aDBAddon.startupData) { this.startupData = aDBAddon.startupData; } @@ -1521,11 +1516,6 @@ class BootstrapScope { if (Services.appinfo.inSafeMode && !runInSafeMode) return null; - if (addon.type == "extension" && aMethod == "startup") { - logger.debug(`Registering manifest for ${this.file.path}`); - Components.manager.addBootstrappedManifestLocation(this.file); - } - try { if (!this.scope) { this.loadBootstrapScope(aReason); @@ -1538,7 +1528,7 @@ class BootstrapScope { let method = undefined; let {scope} = this; try { - method = scope[aMethod] || Cu.evalInSandbox(`${aMethod};`, scope); + method = scope[aMethod]; } catch (e) { // An exception will be caught if the expected method is not defined. // That will be logged below. @@ -1597,12 +1587,6 @@ class BootstrapScope { XPIDatabase.updateAddonDisabledState(addon); } } - - if (addon.type == "extension" && aMethod == "shutdown" && - aReason != BOOTSTRAP_REASONS.APP_SHUTDOWN) { - logger.debug(`Removing manifest for ${this.file.path}`); - Components.manager.removeBootstrappedManifestLocation(this.file); - } } } @@ -1632,33 +1616,31 @@ class BootstrapScope { logger.debug(`Loading bootstrap scope from ${this.file.path}`); - if (isWebExtension(this.addon.type)) { - this.scope = Extension.getBootstrapScope(this.addon.id, this.file); - } else if (this.addon.type === "webextension-langpack") { - this.scope = Langpack.getBootstrapScope(this.addon.id, this.file); - } else if (this.addon.type === "webextension-dictionary") { - this.scope = Dictionary.getBootstrapScope(this.addon.id, this.file); - } else { - let uri = getURIForResourceInFile(this.file, "bootstrap.js").spec; + if (this.addon.isWebExtension) { + switch (this.addon.type) { + case "extension": + case "theme": + this.scope = Extension.getBootstrapScope(this.addon.id, this.file); + break; - let principal = Services.scriptSecurityManager.getSystemPrincipal(); - this.scope = - new Cu.Sandbox(principal, { sandboxName: uri, - addonId: this.addon.id, - wantGlobalProperties: ["ChromeUtils"], - metadata: { addonID: this.addon.id, URI: uri } }); + case "locale": + this.scope = Langpack.getBootstrapScope(this.addon.id, this.file); + break; - try { - Object.assign(this.scope, BOOTSTRAP_REASONS); + case "dictionary": + this.scope = Dictionary.getBootstrapScope(this.addon.id, this.file); + break; - XPCOMUtils.defineLazyGetter( - this.scope, "console", - () => new ConsoleAPI({ consoleID: `addon/${this.addon.id}` })); - - Services.scriptloader.loadSubScript(uri, this.scope); - } catch (e) { - logger.warn(`Error loading bootstrap.js for ${this.addon.id}`, e); + default: + throw new Error(`Unknown webextension type ${this.addon.type}`); } + } else { + let loader = AddonManagerPrivate.externalExtensionLoaders.get(this.addon.loader); + if (!loader) { + throw new Error(`Cannot find loader for ${this.addon.loader}`); + } + + this.scope = loader.loadScope(this.addon, this.file); } // Notify the BrowserToolboxProcess that a new addon has been loaded. @@ -1833,7 +1815,7 @@ class BootstrapScope { let reason = XPIInstall.newVersionReason(this.addon.version, newAddon.version); let extraArgs = {oldVersion: this.addon.version, newVersion: newAddon.version}; - let callUpdate = isWebExtension(this.addon.type) && isWebExtension(newAddon.type); + let callUpdate = this.addon.isWebExtension && newAddon.isWebExtension; await this._uninstall(reason, callUpdate, extraArgs); @@ -2668,7 +2650,7 @@ var XPIProvider = { updateDate: addon.lastModifiedTime, scope, isSystem, - isWebExtension: isWebExtension(addon), + isWebExtension: addon.isWebExtension, }); } @@ -2745,7 +2727,6 @@ var XPIInternal = { awaitPromise, canRunInSafeMode, getURIForResourceInFile, - isWebExtension, isXPI, iterDirectory, }; diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/test_update.json b/toolkit/mozapps/extensions/test/xpcshell/data/test_update.json index 6b4c72e48a5a..9e3483d5209d 100644 --- a/toolkit/mozapps/extensions/test/xpcshell/data/test_update.json +++ b/toolkit/mozapps/extensions/test/xpcshell/data/test_update.json @@ -119,84 +119,6 @@ ] }, - "addon9@tests.mozilla.org": { - "updates": [ - { - "version": "2.0", - "update_link": "http://example.com/addons/test_update9_2.xpi", - "applications": { - "gecko": { - "strict_min_version": "1", - "advisory_max_version": "1" - } - } - }, - { - "_comment_": "Incompatible when strict compatibility is enabled", - "version": "3.0", - "update_link": "http://example.com/addons/test_update9_3.xpi", - "applications": { - "gecko": { - "strict_min_version": "0.9", - "advisory_max_version": "0.9" - } - } - }, - { - "_comment_": "Incompatible due to compatibility override", - "version": "4.0", - "update_link": "http://example.com/addons/test_update9_4.xpi", - "applications": { - "gecko": { - "strict_min_version": "0.9", - "advisory_max_version": "0.9" - } - } - }, - { - "_comment_": "Addon for future version of app", - "version": "4.0", - "update_link": "http://example.com/addons/test_update9_5.xpi", - "applications": { - "gecko": { - "strict_min_version": "5", - "advisory_max_version": "6" - } - } - } - ] - }, - - "addon10@tests.mozilla.org": { - "updates": [ - { - "version": "1.0", - "update_link": "http://example.com/addons/test_update10.xpi", - "applications": { - "gecko": { - "strict_min_version": "0.1", - "advisory_max_version": "0.4" - } - } - } - ] - }, - - "addon11@tests.mozilla.org": { - "updates": [ - { - "version": "2.0", - "update_link": "http://example.com/addons/test_update11.xpi", - "applications": { - "gecko": { - "strict_min_version": "0.1", - "strict_max_version": "0.2" - } - } - } - ] - }, - "addon12@tests.mozilla.org": { "updates": [ { diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/test_updatecompatmode_ignore.json b/toolkit/mozapps/extensions/test/xpcshell/data/test_updatecompatmode_ignore.json deleted file mode 100644 index ac08c02f4e6d..000000000000 --- a/toolkit/mozapps/extensions/test/xpcshell/data/test_updatecompatmode_ignore.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "addons": { - "compatmode-ignore@tests.mozilla.org": { - "updates": [ - { - "applications": { - "gecko": { - "strict_min_version": "1", - "advisory_max_version": "2" - } - }, - "version": "2.0", - "update_link": "https://example.com/addons/test1.xpi" - } - ] - } - } -} diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/test_updatecompatmode_normal.json b/toolkit/mozapps/extensions/test/xpcshell/data/test_updatecompatmode_normal.json deleted file mode 100644 index ce43df1a4f8f..000000000000 --- a/toolkit/mozapps/extensions/test/xpcshell/data/test_updatecompatmode_normal.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "addons": { - "compatmode-normal@tests.mozilla.org": { - "updates": [ - { - "applications": { - "gecko": { - "strict_min_version": "1", - "advisory_max_version": "2" - } - }, - "version": "2.0", - "update_link": "https://example.com/addons/test1.xpi" - } - ] - } - } -} diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/test_updatecompatmode_strict.json b/toolkit/mozapps/extensions/test/xpcshell/data/test_updatecompatmode_strict.json deleted file mode 100644 index 7874865a4bf1..000000000000 --- a/toolkit/mozapps/extensions/test/xpcshell/data/test_updatecompatmode_strict.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "addons": { - "compatmode-strict@tests.mozilla.org": { - "updates": [ - { - "applications": { - "gecko": { - "strict_min_version": "1", - "advisory_max_version": "2" - } - }, - "version": "2.0", - "update_link": "https://example.com/addons/test1.xpi" - } - ] - } - } -} diff --git a/toolkit/mozapps/extensions/test/xpcshell/head_compat.js b/toolkit/mozapps/extensions/test/xpcshell/head_compat.js new file mode 100644 index 000000000000..b8d1ac4ae25b --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/head_compat.js @@ -0,0 +1,48 @@ +// +// This file provides helpers for tests of addons that use strictCompatibility. +// Since WebExtensions cannot opt out of strictCompatibility, we add a +// simple extension loader that lets tests directly set AddonInternal +// properties (including strictCompatibility) +// + +/* import-globals-from head_addons.js */ + +const MANIFEST = "compat_manifest.json"; + +AddonManager.addExternalExtensionLoader({ + name: "compat-test", + manifestFile: MANIFEST, + async loadManifest(pkg) { + // XPIDatabase.jsm gets unloaded in AddonTestUtils when the + // addon manager is restarted. Work around that by just importing + // it every time we need to create an AddonInternal. + ChromeUtils.import("resource://gre/modules/addons/XPIDatabase.jsm"); + /* globals AddonInternal */ + let addon = new AddonInternal(); + let manifest = JSON.parse(await pkg.readString(MANIFEST)); + Object.assign(addon, manifest); + return addon; + }, + loadScope(addon, file) { + return { + install() {}, + uninstall() {}, + startup() {}, + shutdonw() {}, + }; + }, +}); + +const DEFAULTS = { + defaultLocale: {}, + locales: [], + targetPlatforms: [], + type: "extension", + version: "1.0", +}; + +function createAddon(manifest) { + return AddonTestUtils.createTempXPIFile({ + [MANIFEST]: Object.assign({}, DEFAULTS, manifest), + }); +} diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_bootstrap.js b/toolkit/mozapps/extensions/test/xpcshell/test_bootstrap.js deleted file mode 100644 index 83d9bbcd029b..000000000000 --- a/toolkit/mozapps/extensions/test/xpcshell/test_bootstrap.js +++ /dev/null @@ -1,1183 +0,0 @@ -/* Any copyright is dedicated to the Public Domain. - * http://creativecommons.org/publicdomain/zero/1.0/ - */ - -const APP_STARTUP = 1; -const APP_SHUTDOWN = 2; -const ADDON_ENABLE = 3; -const ADDON_DISABLE = 4; -const ADDON_INSTALL = 5; -const ADDON_UNINSTALL = 6; -const ADDON_UPGRADE = 7; -const ADDON_DOWNGRADE = 8; - -const ID1 = "bootstrap1@tests.mozilla.org"; -const ID2 = "bootstrap2@tests.mozilla.org"; - -// This verifies that bootstrappable add-ons can be used without restarts. -ChromeUtils.import("resource://gre/modules/Services.jsm"); - -// Enable loading extensions from the user scopes -Services.prefs.setIntPref("extensions.enabledScopes", - AddonManager.SCOPE_PROFILE + AddonManager.SCOPE_USER); - -BootstrapMonitor.init(); - -createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9.2"); - -const profileDir = gProfD.clone(); -profileDir.append("extensions"); -const userExtDir = gProfD.clone(); -userExtDir.append("extensions2"); -userExtDir.append(gAppInfo.ID); -registerDirectory("XREUSysExt", userExtDir.parent); - - -const ADDONS = { - test_bootstrap1_1: { - "install.rdf": { - id: "bootstrap1@tests.mozilla.org", - - name: "Test Bootstrap 1", - - iconURL: "chrome://foo/skin/icon.png", - aboutURL: "chrome://foo/content/about.xul", - optionsURL: "chrome://foo/content/options.xul", - }, - "bootstrap.js": BOOTSTRAP_MONITOR_BOOTSTRAP_JS, - }, - test_bootstrap1_2: { - "install.rdf": { - id: "bootstrap1@tests.mozilla.org", - version: "2.0", - - name: "Test Bootstrap 1", - }, - "bootstrap.js": BOOTSTRAP_MONITOR_BOOTSTRAP_JS, - }, - test_bootstrap1_3: { - "install.rdf": { - id: "bootstrap1@tests.mozilla.org", - version: "3.0", - - name: "Test Bootstrap 1", - - targetApplications: [{ - id: "undefined", - minVersion: "1", - maxVersion: "1"}], - }, - "bootstrap.js": BOOTSTRAP_MONITOR_BOOTSTRAP_JS, - }, - test_bootstrap2_1: { - "install.rdf": { - id: "bootstrap2@tests.mozilla.org", - }, - "bootstrap.js": BOOTSTRAP_MONITOR_BOOTSTRAP_JS, - }, -}; - -var testserver = AddonTestUtils.createHttpServer({hosts: ["example.com"]}); - -const XPIS = {}; -for (let [name, addon] of Object.entries(ADDONS)) { - XPIS[name] = AddonTestUtils.createTempXPIFile(addon); - testserver.registerFile(`/addons/${name}.xpi`, XPIS[name]); -} - - -function getStartupReason() { - let info = BootstrapMonitor.started.get(ID1); - return info ? info.reason : undefined; -} - -function getShutdownReason() { - let info = BootstrapMonitor.stopped.get(ID1); - return info ? info.reason : undefined; -} - -function getInstallReason() { - let info = BootstrapMonitor.installed.get(ID1); - return info ? info.reason : undefined; -} - -function getUninstallReason() { - let info = BootstrapMonitor.uninstalled.get(ID1); - return info ? info.reason : undefined; -} - -function getStartupOldVersion() { - let info = BootstrapMonitor.started.get(ID1); - return info ? info.data.oldVersion : undefined; -} - -function getShutdownNewVersion() { - let info = BootstrapMonitor.stopped.get(ID1); - return info ? info.data.newVersion : undefined; -} - -function getInstallOldVersion() { - let info = BootstrapMonitor.installed.get(ID1); - return info ? info.data.oldVersion : undefined; -} - -function getUninstallNewVersion() { - let info = BootstrapMonitor.uninstalled.get(ID1); - return info ? info.data.newVersion : undefined; -} - -async function checkBootstrappedPref() { - let XPIScope = ChromeUtils.import("resource://gre/modules/addons/XPIProvider.jsm", {}); - - let data = new Map(); - for (let entry of XPIScope.XPIStates.enabledAddons()) { - data.set(entry.id, entry); - } - - let addons = await AddonManager.getAddonsByTypes(["extension"]); - for (let addon of addons) { - if (!addon.id.endsWith("@tests.mozilla.org")) - continue; - if (!addon.isActive) - continue; - if (addon.operationsRequiringRestart != AddonManager.OP_NEEDS_RESTART_NONE) - continue; - - ok(data.has(addon.id)); - let addonData = data.get(addon.id); - data.delete(addon.id); - - equal(addonData.version, addon.version); - equal(addonData.type, addon.type); - let file = addon.getResourceURI().QueryInterface(Ci.nsIFileURL).file; - equal(addonData.path, file.path); - } - equal(data.size, 0); -} - - -add_task(async function run_test() { - promiseStartupManager(); - - ok(!gExtensionsJSON.exists()); - ok(!gAddonStartup.exists()); -}); - -// Tests that installing doesn't require a restart -add_task(async function test_1() { - prepare_test({}, [ - "onNewInstall", - ]); - - let install = await AddonManager.getInstallForFile(XPIS.test_bootstrap1_1); - ensure_test_completed(); - - notEqual(install, null); - equal(install.type, "extension"); - equal(install.version, "1.0"); - equal(install.name, "Test Bootstrap 1"); - equal(install.state, AddonManager.STATE_DOWNLOADED); - notEqual(install.addon.syncGUID, null); - equal(install.addon.operationsRequiringRestart & - AddonManager.OP_NEEDS_RESTART_INSTALL, 0); - do_check_not_in_crash_annotation(ID1, "1.0"); - - let addon = install.addon; - - await Promise.all([ - BootstrapMonitor.promiseAddonStartup(ID1), - new Promise(resolve => { - prepare_test({ - [ID1]: [ - ["onInstalling", false], - "onInstalled", - ], - }, [ - "onInstallStarted", - "onInstallEnded", - ], function() { - // startup should not have been called yet. - BootstrapMonitor.checkAddonNotStarted(ID1); - resolve(); - }); - install.install(); - }), - ]); - - await checkBootstrappedPref(); - let installSyncGUID = addon.syncGUID; - - let installs = await AddonManager.getAllInstalls(); - // There should be no active installs now since the install completed and - // doesn't require a restart. - equal(installs.length, 0); - - let b1 = await AddonManager.getAddonByID(ID1); - notEqual(b1, null); - equal(b1.version, "1.0"); - notEqual(b1.syncGUID, null); - equal(b1.syncGUID, installSyncGUID); - ok(!b1.appDisabled); - ok(!b1.userDisabled); - ok(b1.isActive); - ok(!b1.isSystem); - BootstrapMonitor.checkAddonInstalled(ID1, "1.0"); - BootstrapMonitor.checkAddonStarted(ID1, "1.0"); - equal(getStartupReason(), ADDON_INSTALL); - equal(getStartupOldVersion(), undefined); - do_check_in_crash_annotation(ID1, "1.0"); - - let dir = do_get_addon_root_uri(profileDir, ID1); - equal(b1.getResourceURI("bootstrap.js").spec, dir + "bootstrap.js"); -}); - -// Tests that disabling doesn't require a restart -add_task(async function test_2() { - let b1 = await AddonManager.getAddonByID(ID1); - prepare_test({ - [ID1]: [ - ["onDisabling", false], - "onDisabled", - ], - }); - - equal(b1.operationsRequiringRestart & - AddonManager.OP_NEEDS_RESTART_DISABLE, 0); - await b1.disable(); - ensure_test_completed(); - - await new Promise(executeSoon); - - notEqual(b1, null); - equal(b1.version, "1.0"); - ok(!b1.appDisabled); - ok(b1.userDisabled); - ok(!b1.isActive); - BootstrapMonitor.checkAddonInstalled(ID1, "1.0"); - BootstrapMonitor.checkAddonNotStarted(ID1); - equal(getShutdownReason(), ADDON_DISABLE); - equal(getShutdownNewVersion(), undefined); - do_check_not_in_crash_annotation(ID1, "1.0"); - - let newb1 = await AddonManager.getAddonByID(ID1); - notEqual(newb1, null); - equal(newb1.version, "1.0"); - ok(!newb1.appDisabled); - ok(newb1.userDisabled); - ok(!newb1.isActive); - - await checkBootstrappedPref(); -}); - -// Test that restarting doesn't accidentally re-enable -add_task(async function test_3() { - await promiseShutdownManager(); - - BootstrapMonitor.checkAddonInstalled(ID1, "1.0"); - BootstrapMonitor.checkAddonNotStarted(ID1); - equal(getShutdownReason(), ADDON_DISABLE); - equal(getShutdownNewVersion(), undefined); - - await promiseStartupManager(); - - BootstrapMonitor.checkAddonInstalled(ID1, "1.0"); - BootstrapMonitor.checkAddonNotStarted(ID1); - equal(getShutdownReason(), ADDON_DISABLE); - equal(getShutdownNewVersion(), undefined); - do_check_not_in_crash_annotation(ID1, "1.0"); - - ok(gAddonStartup.exists()); - - let b1 = await AddonManager.getAddonByID(ID1); - notEqual(b1, null); - equal(b1.version, "1.0"); - ok(!b1.appDisabled); - ok(b1.userDisabled); - ok(!b1.isActive); - - await checkBootstrappedPref(); -}); - -// Tests that enabling doesn't require a restart -add_task(async function test_4() { - let b1 = await AddonManager.getAddonByID(ID1); - prepare_test({ - [ID1]: [ - ["onEnabling", false], - "onEnabled", - ], - }); - - equal(b1.operationsRequiringRestart & - AddonManager.OP_NEEDS_RESTART_ENABLE, 0); - await b1.enable(); - ensure_test_completed(); - - notEqual(b1, null); - equal(b1.version, "1.0"); - ok(!b1.appDisabled); - ok(!b1.userDisabled); - ok(b1.isActive); - ok(!b1.isSystem); - BootstrapMonitor.checkAddonInstalled(ID1, "1.0"); - BootstrapMonitor.checkAddonStarted(ID1, "1.0"); - equal(getStartupReason(), ADDON_ENABLE); - equal(getStartupOldVersion(), undefined); - do_check_in_crash_annotation(ID1, "1.0"); - - let newb1 = await AddonManager.getAddonByID(ID1); - notEqual(newb1, null); - equal(newb1.version, "1.0"); - ok(!newb1.appDisabled); - ok(!newb1.userDisabled); - ok(newb1.isActive); - - await checkBootstrappedPref(); -}); - -// Tests that a restart shuts down and restarts the add-on -add_task(async function test_5() { - await promiseShutdownManager(); - // By the time we've shut down, the database must have been written - ok(gExtensionsJSON.exists()); - - BootstrapMonitor.checkAddonInstalled(ID1, "1.0"); - BootstrapMonitor.checkAddonNotStarted(ID1); - equal(getShutdownReason(), APP_SHUTDOWN); - equal(getShutdownNewVersion(), undefined); - do_check_not_in_crash_annotation(ID1, "1.0"); - await promiseStartupManager(); - BootstrapMonitor.checkAddonInstalled(ID1, "1.0"); - BootstrapMonitor.checkAddonStarted(ID1, "1.0"); - equal(getStartupReason(), APP_STARTUP); - equal(getStartupOldVersion(), undefined); - do_check_in_crash_annotation(ID1, "1.0"); - - let b1 = await AddonManager.getAddonByID(ID1); - notEqual(b1, null); - equal(b1.version, "1.0"); - ok(!b1.appDisabled); - ok(!b1.userDisabled); - ok(b1.isActive); - ok(!b1.isSystem); - - await checkBootstrappedPref(); -}); - -// Tests that installing an upgrade doesn't require a restart -add_task(async function test_6() { - prepare_test({}, [ - "onNewInstall", - ]); - - let install = await AddonManager.getInstallForFile(XPIS.test_bootstrap1_2); - ensure_test_completed(); - - notEqual(install, null); - equal(install.type, "extension"); - equal(install.version, "2.0"); - equal(install.name, "Test Bootstrap 1"); - equal(install.state, AddonManager.STATE_DOWNLOADED); - - await Promise.all([ - BootstrapMonitor.promiseAddonStartup(ID1), - new Promise(resolve => { - prepare_test({ - [ID1]: [ - ["onInstalling", false], - "onInstalled", - ], - }, [ - "onInstallStarted", - "onInstallEnded", - ], resolve); - install.install(); - }), - ]); - - let b1 = await AddonManager.getAddonByID(ID1); - notEqual(b1, null); - equal(b1.version, "2.0"); - ok(!b1.appDisabled); - ok(!b1.userDisabled); - ok(b1.isActive); - ok(!b1.isSystem); - BootstrapMonitor.checkAddonInstalled(ID1, "2.0"); - BootstrapMonitor.checkAddonStarted(ID1, "2.0"); - equal(getStartupReason(), ADDON_UPGRADE); - equal(getInstallOldVersion(), 1); - equal(getStartupOldVersion(), 1); - equal(getShutdownReason(), ADDON_UPGRADE); - equal(getShutdownNewVersion(), 2); - equal(getUninstallNewVersion(), 2); - do_check_not_in_crash_annotation(ID1, "1.0"); - do_check_in_crash_annotation(ID1, "2.0"); - - await checkBootstrappedPref(); -}); - -// Tests that uninstalling doesn't require a restart -add_task(async function test_7() { - let b1 = await AddonManager.getAddonByID(ID1); - prepare_test({ - [ID1]: [ - ["onUninstalling", false], - "onUninstalled", - ], - }); - - equal(b1.operationsRequiringRestart & - AddonManager.OP_NEEDS_RESTART_UNINSTALL, 0); - await b1.uninstall(); - - await checkBootstrappedPref(); - - ensure_test_completed(); - BootstrapMonitor.checkAddonNotInstalled(ID1); - BootstrapMonitor.checkAddonNotStarted(ID1); - equal(getShutdownReason(), ADDON_UNINSTALL); - equal(getShutdownNewVersion(), undefined); - do_check_not_in_crash_annotation(ID1, "2.0"); - - b1 = await AddonManager.getAddonByID(ID1); - equal(b1, null); - - await promiseRestartManager(); - - let newb1 = await AddonManager.getAddonByID(ID1); - equal(newb1, null); - - await checkBootstrappedPref(); -}); - -// Test that a bootstrapped extension dropped into the profile loads properly -// on startup and doesn't cause an EM restart -add_task(async function test_8() { - await promiseShutdownManager(); - - await manuallyInstall(XPIS.test_bootstrap1_1, profileDir, ID1); - - await promiseStartupManager(); - - let b1 = await AddonManager.getAddonByID(ID1); - notEqual(b1, null); - equal(b1.version, "1.0"); - ok(!b1.appDisabled); - ok(!b1.userDisabled); - ok(b1.isActive); - ok(!b1.isSystem); - BootstrapMonitor.checkAddonInstalled(ID1, "1.0"); - BootstrapMonitor.checkAddonStarted(ID1, "1.0"); - equal(getStartupReason(), ADDON_INSTALL); - equal(getStartupOldVersion(), undefined); - do_check_in_crash_annotation(ID1, "1.0"); - - await checkBootstrappedPref(); -}); - -// Test that items detected as removed during startup get removed properly -add_task(async function test_9() { - await promiseShutdownManager(); - - manuallyUninstall(profileDir, ID1); - BootstrapMonitor.clear(ID1); - - await promiseStartupManager(); - - let b1 = await AddonManager.getAddonByID(ID1); - equal(b1, null); - do_check_not_in_crash_annotation(ID1, "1.0"); - - await checkBootstrappedPref(); -}); - - -// Tests that installing a downgrade sends the right reason -add_task(async function test_10() { - prepare_test({}, [ - "onNewInstall", - ]); - - let install = await AddonManager.getInstallForFile(XPIS.test_bootstrap1_2); - ensure_test_completed(); - - notEqual(install, null); - equal(install.type, "extension"); - equal(install.version, "2.0"); - equal(install.name, "Test Bootstrap 1"); - equal(install.state, AddonManager.STATE_DOWNLOADED); - do_check_not_in_crash_annotation(ID1, "2.0"); - - await Promise.all([ - BootstrapMonitor.promiseAddonStartup(ID1), - new Promise(resolve => { - prepare_test({ - [ID1]: [ - ["onInstalling", false], - "onInstalled", - ], - }, [ - "onInstallStarted", - "onInstallEnded", - ], resolve); - install.install(); - }), - ]); - - - let b1 = await AddonManager.getAddonByID(ID1); - notEqual(b1, null); - equal(b1.version, "2.0"); - ok(!b1.appDisabled); - ok(!b1.userDisabled); - ok(b1.isActive); - ok(!b1.isSystem); - BootstrapMonitor.checkAddonInstalled(ID1, "2.0"); - BootstrapMonitor.checkAddonStarted(ID1, "2.0"); - equal(getStartupReason(), ADDON_INSTALL); - equal(getStartupOldVersion(), undefined); - do_check_in_crash_annotation(ID1, "2.0"); - - prepare_test({}, [ - "onNewInstall", - ]); - - install = await AddonManager.getInstallForFile(XPIS.test_bootstrap1_1); - ensure_test_completed(); - - notEqual(install, null); - equal(install.type, "extension"); - equal(install.version, "1.0"); - equal(install.name, "Test Bootstrap 1"); - equal(install.state, AddonManager.STATE_DOWNLOADED); - - await Promise.all([ - BootstrapMonitor.promiseAddonStartup(ID1), - new Promise(resolve => { - prepare_test({ - [ID1]: [ - ["onInstalling", false], - "onInstalled", - ], - }, [ - "onInstallStarted", - "onInstallEnded", - ], resolve); - install.install(); - }), - ]); - - b1 = await AddonManager.getAddonByID(ID1); - notEqual(b1, null); - equal(b1.version, "1.0"); - ok(!b1.appDisabled); - ok(!b1.userDisabled); - ok(b1.isActive); - ok(!b1.isSystem); - BootstrapMonitor.checkAddonInstalled(ID1, "1.0"); - BootstrapMonitor.checkAddonStarted(ID1, "1.0"); - equal(getStartupReason(), ADDON_DOWNGRADE); - equal(getInstallOldVersion(), 2); - equal(getStartupOldVersion(), 2); - equal(getShutdownReason(), ADDON_DOWNGRADE); - equal(getShutdownNewVersion(), 1); - equal(getUninstallNewVersion(), 1); - do_check_in_crash_annotation(ID1, "1.0"); - do_check_not_in_crash_annotation(ID1, "2.0"); - - await checkBootstrappedPref(); -}); - -// Tests that uninstalling a disabled add-on still calls the uninstall method -add_task(async function test_11() { - let b1 = await AddonManager.getAddonByID(ID1); - prepare_test({ - [ID1]: [ - ["onDisabling", false], - "onDisabled", - ["onUninstalling", false], - "onUninstalled", - ], - }); - - await b1.disable(); - - BootstrapMonitor.checkAddonInstalled(ID1, "1.0"); - BootstrapMonitor.checkAddonNotStarted(ID1); - equal(getShutdownReason(), ADDON_DISABLE); - equal(getShutdownNewVersion(), undefined); - do_check_not_in_crash_annotation(ID1, "1.0"); - - await b1.uninstall(); - - ensure_test_completed(); - BootstrapMonitor.checkAddonNotInstalled(ID1); - BootstrapMonitor.checkAddonNotStarted(ID1); - do_check_not_in_crash_annotation(ID1, "1.0"); - - await checkBootstrappedPref(); -}); - -// Tests that bootstrapped extensions are correctly loaded even if the app is -// upgraded at the same time -add_task(async function test_12() { - await promiseShutdownManager(); - - await manuallyInstall(XPIS.test_bootstrap1_1, profileDir, ID1); - - await promiseStartupManager(); - - let b1 = await AddonManager.getAddonByID(ID1); - notEqual(b1, null); - equal(b1.version, "1.0"); - ok(!b1.appDisabled); - ok(!b1.userDisabled); - ok(b1.isActive); - ok(!b1.isSystem); - BootstrapMonitor.checkAddonInstalled(ID1, "1.0"); - BootstrapMonitor.checkAddonStarted(ID1, "1.0"); - equal(getStartupReason(), ADDON_INSTALL); - equal(getStartupOldVersion(), undefined); - do_check_in_crash_annotation(ID1, "1.0"); - - await b1.uninstall(); - - await promiseRestartManager(); - await checkBootstrappedPref(); -}); - - -// Tests that installing a bootstrapped extension with an invalid application -// entry doesn't call it's startup method -add_task(async function test_13() { - prepare_test({}, [ - "onNewInstall", - ]); - - let install = await AddonManager.getInstallForFile(XPIS.test_bootstrap1_3); - ensure_test_completed(); - - notEqual(install, null); - equal(install.type, "extension"); - equal(install.version, "3.0"); - equal(install.name, "Test Bootstrap 1"); - equal(install.state, AddonManager.STATE_DOWNLOADED); - do_check_not_in_crash_annotation(ID1, "3.0"); - - await new Promise(resolve => { - prepare_test({ - [ID1]: [ - ["onInstalling", false], - "onInstalled", - ], - }, [ - "onInstallStarted", - "onInstallEnded", - ], resolve); - install.install(); - }); - - let installs = await AddonManager.getAllInstalls(); - - // There should be no active installs now since the install completed and - // doesn't require a restart. - equal(installs.length, 0); - - let b1 = await AddonManager.getAddonByID(ID1); - notEqual(b1, null); - equal(b1.version, "3.0"); - ok(b1.appDisabled); - ok(!b1.userDisabled); - ok(!b1.isActive); - BootstrapMonitor.checkAddonInstalled(ID1, "3.0"); // We call install even for disabled add-ons - BootstrapMonitor.checkAddonNotStarted(ID1); // Should not have called startup though - do_check_not_in_crash_annotation(ID1, "3.0"); - - await promiseRestartManager(); - - b1 = await AddonManager.getAddonByID(ID1); - notEqual(b1, null); - equal(b1.version, "3.0"); - ok(b1.appDisabled); - ok(!b1.userDisabled); - ok(!b1.isActive); - BootstrapMonitor.checkAddonInstalled(ID1, "3.0"); // We call install even for disabled add-ons - BootstrapMonitor.checkAddonNotStarted(ID1); // Should not have called startup though - do_check_not_in_crash_annotation(ID1, "3.0"); - - await checkBootstrappedPref(); - await b1.uninstall(); -}); - -// Tests that a bootstrapped extension with an invalid target application entry -// does not get loaded when detected during startup -add_task(async function test_14() { - await promiseRestartManager(); - - await promiseShutdownManager(); - - await manuallyInstall(XPIS.test_bootstrap1_3, profileDir, ID1); - - await promiseStartupManager(); - - let b1 = await AddonManager.getAddonByID(ID1); - notEqual(b1, null); - equal(b1.version, "3.0"); - ok(b1.appDisabled); - ok(!b1.userDisabled); - ok(!b1.isActive); - BootstrapMonitor.checkAddonInstalled(ID1, "3.0"); // We call install even for disabled add-ons - BootstrapMonitor.checkAddonNotStarted(ID1); // Should not have called startup though - do_check_not_in_crash_annotation(ID1, "3.0"); - - await checkBootstrappedPref(); - await b1.uninstall(); -}); - -// Tests that upgrading a disabled bootstrapped extension still calls uninstall -// and install but doesn't startup the new version -add_task(async function test_15() { - await Promise.all([ - BootstrapMonitor.promiseAddonStartup(ID1), - promiseInstallFile(XPIS.test_bootstrap1_1), - ]); - - let b1 = await AddonManager.getAddonByID(ID1); - notEqual(b1, null); - equal(b1.version, "1.0"); - ok(!b1.appDisabled); - ok(!b1.userDisabled); - ok(b1.isActive); - ok(!b1.isSystem); - BootstrapMonitor.checkAddonInstalled(ID1, "1.0"); - BootstrapMonitor.checkAddonStarted(ID1, "1.0"); - - await b1.disable(); - ok(!b1.isActive); - BootstrapMonitor.checkAddonInstalled(ID1, "1.0"); - BootstrapMonitor.checkAddonNotStarted(ID1); - - prepare_test({}, [ - "onNewInstall", - ]); - - let install = await AddonManager.getInstallForFile(XPIS.test_bootstrap1_2); - ensure_test_completed(); - - notEqual(install, null); - ok(install.addon.userDisabled); - - await new Promise(resolve => { - prepare_test({ - [ID1]: [ - ["onInstalling", false], - "onInstalled", - ], - }, [ - "onInstallStarted", - "onInstallEnded", - ], resolve); - install.install(); - }); - - b1 = await AddonManager.getAddonByID(ID1); - notEqual(b1, null); - equal(b1.version, "2.0"); - ok(!b1.appDisabled); - ok(b1.userDisabled); - ok(!b1.isActive); - BootstrapMonitor.checkAddonInstalled(ID1, "2.0"); - BootstrapMonitor.checkAddonNotStarted(ID1); - - await checkBootstrappedPref(); - await promiseRestartManager(); - - let b1_2 = await AddonManager.getAddonByID(ID1); - notEqual(b1_2, null); - equal(b1_2.version, "2.0"); - ok(!b1_2.appDisabled); - ok(b1_2.userDisabled); - ok(!b1_2.isActive); - BootstrapMonitor.checkAddonInstalled(ID1, "2.0"); - BootstrapMonitor.checkAddonNotStarted(ID1); - - await b1_2.uninstall(); -}); - -// Tests that bootstrapped extensions don't get loaded when in safe mode -add_task(async function test_16() { - await Promise.all([ - BootstrapMonitor.promiseAddonStartup(ID1), - promiseInstallFile(XPIS.test_bootstrap1_1), - ]); - - let b1 = await AddonManager.getAddonByID(ID1); - // Should have installed and started - BootstrapMonitor.checkAddonInstalled(ID1, "1.0"); - BootstrapMonitor.checkAddonStarted(ID1, "1.0"); - ok(b1.isActive); - ok(!b1.isSystem); - equal(b1.iconURL, "chrome://foo/skin/icon.png"); - equal(b1.aboutURL, "chrome://foo/content/about.xul"); - equal(b1.optionsURL, "chrome://foo/content/options.xul"); - - await promiseShutdownManager(); - - // Should have stopped - BootstrapMonitor.checkAddonInstalled(ID1, "1.0"); - BootstrapMonitor.checkAddonNotStarted(ID1); - - gAppInfo.inSafeMode = true; - await promiseStartupManager(); - - let b1_2 = await AddonManager.getAddonByID(ID1); - // Should still be stopped - BootstrapMonitor.checkAddonInstalled(ID1, "1.0"); - BootstrapMonitor.checkAddonNotStarted(ID1); - ok(!b1_2.isActive); - equal(b1_2.iconURL, null); - equal(b1_2.aboutURL, null); - equal(b1_2.optionsURL, null); - - await promiseShutdownManager(); - gAppInfo.inSafeMode = false; - await promiseStartupManager(); - - // Should have started - BootstrapMonitor.checkAddonInstalled(ID1, "1.0"); - BootstrapMonitor.checkAddonStarted(ID1, "1.0"); - - let b1_3 = await AddonManager.getAddonByID(ID1); - await b1_3.uninstall(); -}); - -// Check that a bootstrapped extension in a non-profile location is loaded -add_task(async function test_17() { - await promiseShutdownManager(); - - await manuallyInstall(XPIS.test_bootstrap1_1, userExtDir, ID1); - - await promiseStartupManager(); - - let b1 = await AddonManager.getAddonByID(ID1); - // Should have installed and started - BootstrapMonitor.checkAddonInstalled(ID1, "1.0"); - BootstrapMonitor.checkAddonStarted(ID1, "1.0"); - notEqual(b1, null); - equal(b1.version, "1.0"); - ok(b1.isActive); - ok(!b1.isSystem); - - await checkBootstrappedPref(); -}); - -// Check that installing a new bootstrapped extension in the profile replaces -// the existing one -add_task(async function test_18() { - await Promise.all([ - BootstrapMonitor.promiseAddonStartup(ID1), - promiseInstallFile(XPIS.test_bootstrap1_2), - ]); - - let b1 = await AddonManager.getAddonByID(ID1); - // Should have installed and started - BootstrapMonitor.checkAddonInstalled(ID1, "2.0"); - BootstrapMonitor.checkAddonStarted(ID1, "2.0"); - notEqual(b1, null); - equal(b1.version, "2.0"); - ok(b1.isActive); - ok(!b1.isSystem); - - equal(getShutdownReason(), ADDON_UPGRADE); - equal(getUninstallReason(), ADDON_UPGRADE); - equal(getInstallReason(), ADDON_UPGRADE); - equal(getStartupReason(), ADDON_UPGRADE); - - equal(getShutdownNewVersion(), 2); - equal(getUninstallNewVersion(), 2); - equal(getInstallOldVersion(), 1); - equal(getStartupOldVersion(), 1); - - await checkBootstrappedPref(); -}); - -// Check that uninstalling the profile version reveals the non-profile one -add_task(async function test_19() { - let b1 = await AddonManager.getAddonByID(ID1); - // The revealed add-on gets activated asynchronously - await new Promise(resolve => { - prepare_test({ - [ID1]: [ - ["onUninstalling", false], - "onUninstalled", - ["onInstalling", false], - "onInstalled", - ], - }, [], resolve); - - b1.uninstall(); - }); - - b1 = await AddonManager.getAddonByID(ID1); - // Should have reverted to the older version - BootstrapMonitor.checkAddonInstalled(ID1, "1.0"); - BootstrapMonitor.checkAddonStarted(ID1, "1.0"); - notEqual(b1, null); - equal(b1.version, "1.0"); - ok(b1.isActive); - ok(!b1.isSystem); - - equal(getShutdownReason(), ADDON_DOWNGRADE); - equal(getUninstallReason(), ADDON_DOWNGRADE); - equal(getInstallReason(), ADDON_DOWNGRADE); - equal(getStartupReason(), ADDON_DOWNGRADE); - - equal(getShutdownNewVersion(), "1.0"); - equal(getUninstallNewVersion(), "1.0"); - equal(getInstallOldVersion(), "2.0"); - equal(getStartupOldVersion(), "2.0"); - - await checkBootstrappedPref(); -}); - -// Check that a new profile extension detected at startup replaces the non-profile -// one -add_task(async function test_20() { - await promiseShutdownManager(); - - await manuallyInstall(XPIS.test_bootstrap1_2, profileDir, ID1); - - await promiseStartupManager(); - - let b1 = await AddonManager.getAddonByID(ID1); - // Should have installed and started - BootstrapMonitor.checkAddonInstalled(ID1, "2.0"); - BootstrapMonitor.checkAddonStarted(ID1, "2.0"); - notEqual(b1, null); - equal(b1.version, "2.0"); - ok(b1.isActive); - ok(!b1.isSystem); - - equal(getShutdownReason(), APP_SHUTDOWN); - equal(getUninstallReason(), ADDON_UPGRADE); - equal(getInstallReason(), ADDON_UPGRADE); - equal(getStartupReason(), APP_STARTUP); - - equal(getShutdownNewVersion(), undefined); - equal(getUninstallNewVersion(), 2); - equal(getInstallOldVersion(), 1); - equal(getStartupOldVersion(), undefined); -}); - -// Check that a detected removal reveals the non-profile one -add_task(async function test_21() { - await promiseShutdownManager(); - - equal(getShutdownReason(), APP_SHUTDOWN); - equal(getShutdownNewVersion(), undefined); - - manuallyUninstall(profileDir, ID1); - BootstrapMonitor.clear(ID1); - - await promiseStartupManager(); - - let b1 = await AddonManager.getAddonByID(ID1); - // Should have installed and started - BootstrapMonitor.checkAddonInstalled(ID1, "1.0"); - BootstrapMonitor.checkAddonStarted(ID1, "1.0"); - notEqual(b1, null); - equal(b1.version, "1.0"); - ok(b1.isActive); - ok(!b1.isSystem); - - // This won't be set as the bootstrap script was gone so we couldn't - // uninstall it properly - equal(getUninstallReason(), undefined); - equal(getUninstallNewVersion(), undefined); - - equal(getInstallReason(), ADDON_DOWNGRADE); - equal(getInstallOldVersion(), 2); - - equal(getStartupReason(), APP_STARTUP); - equal(getStartupOldVersion(), undefined); - - await checkBootstrappedPref(); - await promiseShutdownManager(); - - manuallyUninstall(userExtDir, ID1); - BootstrapMonitor.clear(ID1); - - await promiseStartupManager(); -}); - -// Check that an upgrade from the filesystem is detected and applied correctly -add_task(async function test_22() { - await promiseShutdownManager(); - - let file = await manuallyInstall(XPIS.test_bootstrap1_1, profileDir, ID1); - if (file.isDirectory()) - file.append("install.rdf"); - - // Make it look old so changes are detected - setExtensionModifiedTime(file, file.lastModifiedTime - 5000); - - await promiseStartupManager(); - - let b1 = await AddonManager.getAddonByID(ID1); - // Should have installed and started - BootstrapMonitor.checkAddonInstalled(ID1, "1.0"); - BootstrapMonitor.checkAddonStarted(ID1, "1.0"); - notEqual(b1, null); - equal(b1.version, "1.0"); - ok(b1.isActive); - ok(!b1.isSystem); - - await promiseShutdownManager(); - - equal(getShutdownReason(), APP_SHUTDOWN); - equal(getShutdownNewVersion(), undefined); - - manuallyUninstall(profileDir, ID1); - BootstrapMonitor.clear(ID1); - await manuallyInstall(XPIS.test_bootstrap1_2, profileDir, ID1); - - await promiseStartupManager(); - - let b1_2 = await AddonManager.getAddonByID(ID1); - // Should have installed and started - BootstrapMonitor.checkAddonInstalled(ID1, "2.0"); - BootstrapMonitor.checkAddonStarted(ID1, "2.0"); - notEqual(b1_2, null); - equal(b1_2.version, "2.0"); - ok(b1_2.isActive); - ok(!b1_2.isSystem); - - // This won't be set as the bootstrap script was gone so we couldn't - // uninstall it properly - equal(getUninstallReason(), undefined); - equal(getUninstallNewVersion(), undefined); - - equal(getInstallReason(), ADDON_UPGRADE); - equal(getInstallOldVersion(), 1); - equal(getStartupReason(), APP_STARTUP); - equal(getStartupOldVersion(), undefined); - - await checkBootstrappedPref(); - await b1_2.uninstall(); -}); - - -// Tests that installing from a URL doesn't require a restart -add_task(async function test_23() { - prepare_test({}, [ - "onNewInstall", - ]); - - let url = "http://example.com/addons/test_bootstrap1_1.xpi"; - let install = await AddonManager.getInstallForURL(url, "application/x-xpinstall"); - - ensure_test_completed(); - - notEqual(install, null); - - await new Promise(resolve => { - prepare_test({}, [ - "onDownloadStarted", - "onDownloadEnded", - ], function() { - equal(install.type, "extension"); - equal(install.version, "1.0"); - equal(install.name, "Test Bootstrap 1"); - equal(install.state, AddonManager.STATE_DOWNLOADED); - equal(install.addon.operationsRequiringRestart & - AddonManager.OP_NEEDS_RESTART_INSTALL, 0); - do_check_not_in_crash_annotation(ID1, "1.0"); - - prepare_test({ - [ID1]: [ - ["onInstalling", false], - "onInstalled", - ], - }, [ - "onInstallStarted", - "onInstallEnded", - ], resolve); - }); - install.install(); - }); - - await checkBootstrappedPref(); - - let installs = await AddonManager.getAllInstalls(); - - // There should be no active installs now since the install completed and - // doesn't require a restart. - equal(installs.length, 0); - - let b1 = await AddonManager.getAddonByID(ID1); - - notEqual(b1, null); - equal(b1.version, "1.0"); - ok(!b1.appDisabled); - ok(!b1.userDisabled); - ok(b1.isActive); - ok(!b1.isSystem); - BootstrapMonitor.checkAddonInstalled(ID1, "1.0"); - BootstrapMonitor.checkAddonStarted(ID1, "1.0"); - equal(getStartupReason(), ADDON_INSTALL); - equal(getStartupOldVersion(), undefined); - do_check_in_crash_annotation(ID1, "1.0"); - - let dir = do_get_addon_root_uri(profileDir, ID1); - equal(b1.getResourceURI("bootstrap.js").spec, dir + "bootstrap.js"); - - await promiseRestartManager(); - - let b1_2 = await AddonManager.getAddonByID(ID1); - await b1_2.uninstall(); -}); - -// Tests that we recover from a broken preference -add_task(async function test_24() { - info("starting 24"); - - await Promise.all([ - BootstrapMonitor.promiseAddonStartup(ID2), - promiseInstallAllFiles([XPIS.test_bootstrap1_1, XPIS.test_bootstrap2_1]), - ]); - - info("test 24 got prefs"); - BootstrapMonitor.checkAddonInstalled(ID1, "1.0"); - BootstrapMonitor.checkAddonStarted(ID1, "1.0"); - BootstrapMonitor.checkAddonInstalled(ID2, "1.0"); - BootstrapMonitor.checkAddonStarted(ID2, "1.0"); - - await promiseRestartManager(); - - BootstrapMonitor.checkAddonInstalled(ID1, "1.0"); - BootstrapMonitor.checkAddonStarted(ID1, "1.0"); - BootstrapMonitor.checkAddonInstalled(ID2, "1.0"); - BootstrapMonitor.checkAddonStarted(ID2, "1.0"); - - await promiseShutdownManager(); - - BootstrapMonitor.checkAddonInstalled(ID1, "1.0"); - BootstrapMonitor.checkAddonNotStarted(ID1); - BootstrapMonitor.checkAddonInstalled(ID2, "1.0"); - BootstrapMonitor.checkAddonNotStarted(ID2); - - // Break the JSON. - let data = aomStartup.readStartupData(); - data["app-profile"].addons[ID1].path += "foo"; - - await OS.File.writeAtomic(gAddonStartup.path, - new TextEncoder().encode(JSON.stringify(data)), - {compression: "lz4"}); - - await promiseStartupManager(); - - BootstrapMonitor.checkAddonInstalled(ID1, "1.0"); - BootstrapMonitor.checkAddonStarted(ID1, "1.0"); - BootstrapMonitor.checkAddonInstalled(ID2, "1.0"); - BootstrapMonitor.checkAddonStarted(ID2, "1.0"); -}); diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_bootstrap_const.js b/toolkit/mozapps/extensions/test/xpcshell/test_bootstrap_const.js deleted file mode 100644 index 50d35baae97b..000000000000 --- a/toolkit/mozapps/extensions/test/xpcshell/test_bootstrap_const.js +++ /dev/null @@ -1,27 +0,0 @@ -/* Any copyright is dedicated to the Public Domain. - * http://creativecommons.org/publicdomain/zero/1.0/ - */ - -createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1"); - -const ADDONS = { - test_bootstrap_const: { - "install.rdf": { - "id": "bootstrap@tests.mozilla.org", - }, - "bootstrap.js": "ChromeUtils.import(\"resource://gre/modules/Services.jsm\");\n\nconst install = function() {\n Services.obs.notifyObservers(null, \"addon-install\");\n};\n", - }, -}; - -add_task(async function() { - await promiseStartupManager(); - - let sawInstall = false; - Services.obs.addObserver(function() { - sawInstall = true; - }, "addon-install"); - - await AddonTestUtils.promiseInstallXPI(ADDONS.test_bootstrap_const); - - ok(sawInstall); -}); diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_bootstrap_globals.js b/toolkit/mozapps/extensions/test/xpcshell/test_bootstrap_globals.js deleted file mode 100644 index d1658b4782db..000000000000 --- a/toolkit/mozapps/extensions/test/xpcshell/test_bootstrap_globals.js +++ /dev/null @@ -1,66 +0,0 @@ -/* Any copyright is dedicated to the Public Domain. - * http://creativecommons.org/publicdomain/zero/1.0/ - */ - -// This verifies that bootstrap.js has the expected globals defined -ChromeUtils.import("resource://gre/modules/Services.jsm"); - -createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1"); - -const ADDONS = { - bootstrap_globals: { - "install.rdf": { - "id": "bootstrap_globals@tests.mozilla.org", - }, - "bootstrap.js": String.raw`ChromeUtils.import("resource://gre/modules/Services.jsm"); - -var seenGlobals = new Set(); -var scope = this; -function checkGlobal(name, type) { - if (scope[name] && typeof(scope[name]) == type) - seenGlobals.add(name); -} - -var wrapped = {}; -Services.obs.notifyObservers({ wrappedJSObject: wrapped }, "bootstrap-request-globals"); -for (let [name, type] of wrapped.expectedGlobals) { - checkGlobal(name, type); -} - -function startup(data, reason) { - Services.obs.notifyObservers({ wrappedJSObject: seenGlobals }, "bootstrap-seen-globals"); -} - -function install(data, reason) {} -function shutdown(data, reason) {} -function uninstall(data, reason) {} -`, - }, -}; - - -const EXPECTED_GLOBALS = [ - ["console", "object"], -]; - -async function run_test() { - do_test_pending(); - await promiseStartupManager(); - let sawGlobals = false; - - Services.obs.addObserver(function(subject) { - subject.wrappedJSObject.expectedGlobals = EXPECTED_GLOBALS; - }, "bootstrap-request-globals"); - - Services.obs.addObserver(function({ wrappedJSObject: seenGlobals }) { - for (let [name ] of EXPECTED_GLOBALS) - Assert.ok(seenGlobals.has(name)); - - sawGlobals = true; - }, "bootstrap-seen-globals"); - - await AddonTestUtils.promiseInstallXPI(ADDONS.bootstrap_globals); - Assert.ok(sawGlobals); - await promiseShutdownManager(); - do_test_finished(); -} diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_bootstrapped_chrome_manifest.js b/toolkit/mozapps/extensions/test/xpcshell/test_bootstrapped_chrome_manifest.js deleted file mode 100644 index b0d46faad82a..000000000000 --- a/toolkit/mozapps/extensions/test/xpcshell/test_bootstrapped_chrome_manifest.js +++ /dev/null @@ -1,50 +0,0 @@ -/* Any copyright is dedicated to the Public Domain. - * http://creativecommons.org/publicdomain/zero/1.0/ - */ - -const ADDON = { - "install.rdf": { - "id": "bug675371@tests.mozilla.org", - }, - "chrome.manifest": `content bug675371 .`, - "test.js": `var active = true;`, -}; - -add_task(async function run_test() { - createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9.2"); - await promiseStartupManager(); -}); - -function checkActive(expected) { - let target = { active: false }; - let load = () => { - Services.scriptloader.loadSubScript("chrome://bug675371/content/test.js", target); - }; - - if (expected) { - load(); - } else { - Assert.throws(load, /Error opening input stream/); - } - equal(target.active, expected, "Manifest is active?"); -} - -add_task(async function test() { - let {addon} = await AddonTestUtils.promiseInstallXPI(ADDON); - - Assert.ok(addon.isActive); - - // Tests that chrome.manifest is registered when the addon is installed. - checkActive(true); - - await addon.disable(); - checkActive(false); - - await addon.enable(); - checkActive(true); - - await promiseShutdownManager(); - - // Tests that chrome.manifest remains registered at app shutdown. - checkActive(true); -}); diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_compatoverrides.js b/toolkit/mozapps/extensions/test/xpcshell/test_compatoverrides.js index 36cf82b576e2..1e9ec9eae9ca 100644 --- a/toolkit/mozapps/extensions/test/xpcshell/test_compatoverrides.js +++ b/toolkit/mozapps/extensions/test/xpcshell/test_compatoverrides.js @@ -7,23 +7,15 @@ const PREF_GETADDONS_CACHE_ENABLED = "extensions.getAddons.cache.enabled"; -ChromeUtils.import("resource://testing-common/httpd.js"); -var gServer = new HttpServer(); -gServer.start(-1); -gPort = gServer.identity.primaryPort; - -const PORT = gPort; -const BASE_URL = "http://localhost:" + PORT; +let testserver = createHttpServer({hosts: ["example.com"]}); const GETADDONS_RESPONSE = { next: null, previous: null, results: [], }; -gServer.registerPathHandler("/addons.json", (request, response) => { - response.setHeader("content-type", "application/json"); - response.write(JSON.stringify(GETADDONS_RESPONSE)); -}); + +AddonTestUtils.registerJSON(testserver, "/addons.json", GETADDONS_RESPONSE); const COMPAT_RESPONSE = { next: null, @@ -180,237 +172,179 @@ const COMPAT_RESPONSE = { }, ], }; -gServer.registerPathHandler("/compat.json", (request, response) => { - response.setHeader("content-type", "application/json"); - response.write(JSON.stringify(COMPAT_RESPONSE)); -}); + +AddonTestUtils.registerJSON(testserver, "/compat.json", COMPAT_RESPONSE); Services.prefs.setBoolPref(PREF_EM_STRICT_COMPATIBILITY, false); Services.prefs.setBoolPref(PREF_GETADDONS_CACHE_ENABLED, true); -Services.prefs.setCharPref(PREF_GETADDONS_BYIDS, `${BASE_URL}/addons.json`); -Services.prefs.setCharPref(PREF_COMPAT_OVERRIDES, `${BASE_URL}/compat.json`); +Services.prefs.setCharPref(PREF_GETADDONS_BYIDS, "http://example.com/addons.json"); +Services.prefs.setCharPref(PREF_COMPAT_OVERRIDES, "http://example.com/compat.json"); +const ADDONS = [ + // Not hosted, no overrides + { + manifest: { + id: "addon1@tests.mozilla.org", + targetApplications: [{ + id: "xpcshell@tests.mozilla.org", + minVersion: "1", + maxVersion: "1", + }], + }, + compatible: true, + overrides: 0, + }, -// Not hosted, no overrides -var addon1 = { - id: "addon1@tests.mozilla.org", - version: "1.0", - name: "Test addon 1", - bootstrap: true, - targetApplications: [{ - id: "xpcshell@tests.mozilla.org", - minVersion: "1", - maxVersion: "1", - }], -}; + // Hosted, no overrides + { + manifest: { + id: "addon2@tests.mozilla.org", + targetApplications: [{ + id: "xpcshell@tests.mozilla.org", + minVersion: "1", + maxVersion: "1", + }], + }, + compatible: true, + overrides: 0, + }, -// Hosted, no overrides -var addon2 = { - id: "addon2@tests.mozilla.org", - version: "1.0", - name: "Test addon 2", - bootstrap: true, - targetApplications: [{ - id: "xpcshell@tests.mozilla.org", - minVersion: "1", - maxVersion: "1", - }], -}; + // Hosted, matching override + { + manifest: { + id: "addon3@tests.mozilla.org", + targetApplications: [{ + id: "xpcshell@tests.mozilla.org", + minVersion: "1", + maxVersion: "1", + }], + }, + compatible: false, + overrides: 1, + }, -// Hosted, matching override -var addon3 = { - id: "addon3@tests.mozilla.org", - version: "1.0", - name: "Test addon 3", - bootstrap: true, - targetApplications: [{ - id: "xpcshell@tests.mozilla.org", - minVersion: "1", - maxVersion: "1", - }], -}; + // Hosted, matching override, + // wouldn't be compatible if strict checking is enabled + { + manifest: { + id: "addon4@tests.mozilla.org", + targetApplications: [{ + id: "xpcshell@tests.mozilla.org", + minVersion: "0.1", + maxVersion: "0.2", + }], + }, + compatible: false, + overrides: 1, + }, -// Hosted, matching override, wouldn't be compatible if strict checking is enabled -var addon4 = { - id: "addon4@tests.mozilla.org", - version: "1.0", - name: "Test addon 4", - bootstrap: true, - targetApplications: [{ - id: "xpcshell@tests.mozilla.org", - minVersion: "0.1", - maxVersion: "0.2", - }], -}; + // Hosted, app ID doesn't match in override + { + manifest: { + id: "addon5@tests.mozilla.org", + version: "1.0", + targetApplications: [{ + id: "xpcshell@tests.mozilla.org", + minVersion: "1", + maxVersion: "1", + }], + }, + compatible: true, + overrides: 0, + }, -// Hosted, app ID doesn't match in override -var addon5 = { - id: "addon5@tests.mozilla.org", - version: "1.0", - name: "Test addon 5", - bootstrap: true, - targetApplications: [{ - id: "xpcshell@tests.mozilla.org", - minVersion: "1", - maxVersion: "1", - }], -}; + // Hosted, addon version range doesn't match in override + { + manifest: { + id: "addon6@tests.mozilla.org", + targetApplications: [{ + id: "xpcshell@tests.mozilla.org", + minVersion: "1", + maxVersion: "1", + }], + }, + compatible: true, + overrides: 1, + }, -// Hosted, addon version range doesn't match in override -var addon6 = { - id: "addon6@tests.mozilla.org", - version: "1.0", - name: "Test addon 6", - bootstrap: true, - targetApplications: [{ - id: "xpcshell@tests.mozilla.org", - minVersion: "1", - maxVersion: "1", - }], -}; + // Hosted, app version range doesn't match in override + { + manifest: { + id: "addon7@tests.mozilla.org", + targetApplications: [{ + id: "xpcshell@tests.mozilla.org", + minVersion: "1", + maxVersion: "1", + }], + }, + compatible: true, + overrides: 1, + }, -// Hosted, app version range doesn't match in override -var addon7 = { - id: "addon7@tests.mozilla.org", - version: "1.0", - name: "Test addon 7", - bootstrap: true, - targetApplications: [{ - id: "xpcshell@tests.mozilla.org", - minVersion: "1", - maxVersion: "1", - }], -}; + // Hosted, multiple overrides + { + manifest: { + id: "addon8@tests.mozilla.org", + targetApplications: [{ + id: "xpcshell@tests.mozilla.org", + minVersion: "1", + maxVersion: "1", + }], + }, + compatible: false, + overrides: 3, + }, -// Hosted, multiple overrides -var addon8 = { - id: "addon8@tests.mozilla.org", - version: "1.0", - name: "Test addon 8", - bootstrap: true, - targetApplications: [{ - id: "xpcshell@tests.mozilla.org", - minVersion: "1", - maxVersion: "1", - }], -}; + // Not hosted, matching override + { + manifest: { + id: "addon9@tests.mozilla.org", + targetApplications: [{ + id: "xpcshell@tests.mozilla.org", + minVersion: "1", + maxVersion: "1", + }], + }, + compatible: false, + overrides: 1, + }, +]; -// Not hosted, matching override -var addon9 = { - id: "addon9@tests.mozilla.org", - version: "1.0", - name: "Test addon 9", - bootstrap: true, - targetApplications: [{ - id: "xpcshell@tests.mozilla.org", - minVersion: "1", - maxVersion: "1", - }], -}; - -const profileDir = gProfD.clone(); -profileDir.append("extensions"); - -async function run_test() { - do_test_pending(); +add_task(async function run_tests() { createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "2"); - await promiseWriteInstallRDFForExtension(addon1, profileDir); - await promiseWriteInstallRDFForExtension(addon2, profileDir); - await promiseWriteInstallRDFForExtension(addon3, profileDir); - await promiseWriteInstallRDFForExtension(addon4, profileDir); - await promiseWriteInstallRDFForExtension(addon5, profileDir); - await promiseWriteInstallRDFForExtension(addon6, profileDir); - await promiseWriteInstallRDFForExtension(addon7, profileDir); - await promiseWriteInstallRDFForExtension(addon8, profileDir); - await promiseWriteInstallRDFForExtension(addon9, profileDir); - + for (let addon of ADDONS) { + let xpi = await createAddon(addon.manifest); + await manuallyInstall(xpi, AddonTestUtils.profileExtensions, + addon.manifest.id); + } await promiseStartupManager(); - AddonManagerInternal.backgroundUpdateCheck().then(run_test_1); -} + await AddonManagerInternal.backgroundUpdateCheck(); -function end_test() { - gServer.stop(do_test_finished); -} + async function check() { + for (let info of ADDONS) { + let {id} = info.manifest; + let addon = await promiseAddonByID(id); + Assert.notEqual(addon, null, `Found ${id}`); + let overrides = AddonRepository.getCompatibilityOverridesSync(id); + if (info.overrides === 0) { + Assert.equal(overrides, null, `Got no overrides for ${id}`); + } else { + Assert.notEqual(overrides, null, `Got overrides for ${id}`); + Assert.equal(overrides.length, info.overrides, + `Got right number of overrides for ${id}`); + } + Assert.equal(addon.isCompatible, info.compatible, + `Got expected compatibility for ${id}`); + Assert.equal(addon.appDisabled, !info.compatible, + `Got expected appDisabled for ${id}`); + } + } -async function check_compat_status(aCallback) { - let [a1, a2, a3, a4, a5, a6, a7, a8, a9] = await AddonManager.getAddonsByIDs(["addon1@tests.mozilla.org", - "addon2@tests.mozilla.org", - "addon3@tests.mozilla.org", - "addon4@tests.mozilla.org", - "addon5@tests.mozilla.org", - "addon6@tests.mozilla.org", - "addon7@tests.mozilla.org", - "addon8@tests.mozilla.org", - "addon9@tests.mozilla.org"]); - Assert.notEqual(a1, null); - Assert.equal(AddonRepository.getCompatibilityOverridesSync(a1.id), null); - Assert.ok(a1.isCompatible); - Assert.ok(!a1.appDisabled); + await check(); - Assert.notEqual(a2, null); - Assert.equal(AddonRepository.getCompatibilityOverridesSync(a2.id), null); - Assert.ok(a2.isCompatible); - Assert.ok(!a2.appDisabled); - - Assert.notEqual(a3, null); - let overrides = AddonRepository.getCompatibilityOverridesSync(a3.id); - Assert.notEqual(overrides, null); - Assert.equal(overrides.length, 1); - Assert.ok(!a3.isCompatible); - Assert.ok(a3.appDisabled); - - Assert.notEqual(a4, null); - overrides = AddonRepository.getCompatibilityOverridesSync(a4.id); - Assert.notEqual(overrides, null); - Assert.equal(overrides.length, 1); - Assert.ok(!a4.isCompatible); - Assert.ok(a4.appDisabled); - - Assert.notEqual(a5, null); - Assert.equal(AddonRepository.getCompatibilityOverridesSync(a5.id), null); - Assert.ok(a5.isCompatible); - Assert.ok(!a5.appDisabled); - - Assert.notEqual(a6, null); - overrides = AddonRepository.getCompatibilityOverridesSync(a6.id); - Assert.notEqual(overrides, null); - Assert.equal(overrides.length, 1); - Assert.ok(a6.isCompatible); - Assert.ok(!a6.appDisabled); - - Assert.notEqual(a7, null); - overrides = AddonRepository.getCompatibilityOverridesSync(a7.id); - Assert.notEqual(overrides, null); - Assert.equal(overrides.length, 1); - Assert.ok(a7.isCompatible); - Assert.ok(!a7.appDisabled); - - Assert.notEqual(a8, null); - overrides = AddonRepository.getCompatibilityOverridesSync(a8.id); - Assert.notEqual(overrides, null); - Assert.equal(overrides.length, 3); - Assert.ok(!a8.isCompatible); - Assert.ok(a8.appDisabled); - - Assert.notEqual(a9, null); - overrides = AddonRepository.getCompatibilityOverridesSync(a9.id); - Assert.notEqual(overrides, null); - Assert.equal(overrides.length, 1); - Assert.ok(!a9.isCompatible); - Assert.ok(a9.appDisabled); - - executeSoon(aCallback); -} - -function run_test_1() { - info("Run test 1"); - check_compat_status(run_test_2); -} - -async function run_test_2() { - info("Run test 2"); await promiseRestartManager(); - check_compat_status(end_test); -} + + await check(); +}); diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_invalid_install_rdf.js b/toolkit/mozapps/extensions/test/xpcshell/test_invalid_install_rdf.js deleted file mode 100644 index 879ad5d0c7b3..000000000000 --- a/toolkit/mozapps/extensions/test/xpcshell/test_invalid_install_rdf.js +++ /dev/null @@ -1,113 +0,0 @@ -/* Any copyright is dedicated to the Public Domain. - * http://creativecommons.org/publicdomain/zero/1.0/ - */ - -// Test that side-loaded extensions with invalid install.rdf files are -// not initialized at startup. - -const APP_ID = "xpcshell@tests.mozilla.org"; - -Services.prefs.setIntPref("extensions.enabledScopes", AddonManager.SCOPE_USER); - -createAppInfo(APP_ID, "XPCShell", "1", "1.9.2"); - -const userAppDir = AddonTestUtils.profileDir.clone(); -userAppDir.append("app-extensions"); -userAppDir.create(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY); -AddonTestUtils.registerDirectory("XREUSysExt", userAppDir); - -const userExtensions = userAppDir.clone(); -userExtensions.append(APP_ID); -userExtensions.create(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY); - -XPCOMUtils.defineLazyServiceGetters(this, { - ChromeRegistry: ["@mozilla.org/chrome/chrome-registry;1", "nsIChromeRegistry"], -}); - -function hasChromeEntry(package) { - try { - void ChromeRegistry.convertChromeURL(Services.io.newURI(`chrome://${package}/content/`)); - return true; - } catch (e) { - return false; - } -} - -add_task(async function() { - await promiseWriteInstallRDFToXPI({ - id: "langpack-foo@addons.mozilla.org", - version: "1.0", - type: 8, - targetApplications: [{ - id: "xpcshell@tests.mozilla.org", - minVersion: "1", - maxVersion: "1", - }], - name: "Invalid install.rdf extension", - }, userExtensions, undefined, { - "chrome.manifest": ` - content foo-langpack ./ - `, - }); - - await promiseWriteInstallRDFToXPI({ - id: "foo@addons.mozilla.org", - version: "1.0", - bootstrap: true, - targetApplications: [{ - id: "xpcshell@tests.mozilla.org", - minVersion: "1", - maxVersion: "1", - }], - name: "Invalid install.rdf extension", - }, userExtensions, undefined, { - "chrome.manifest": ` - content foo ./ - `, - }); - - await promiseWriteInstallRDFToXPI({ - id: "foo-legacy-legacy@addons.mozilla.org", - version: "1.0", - bootstrap: false, - targetApplications: [{ - id: "xpcshell@tests.mozilla.org", - minVersion: "1", - maxVersion: "1", - }], - name: "Invalid install.rdf extension", - }, userExtensions, undefined, { - "chrome.manifest": ` - content foo-legacy-legacy ./ - `, - }); - - equal(hasChromeEntry("foo-langpack"), false, - "Should not have registered foo-langpack resource before AOM startup"); - equal(hasChromeEntry("foo-legacy-legacy"), false, - "Should not have registered foo-legacy-legacy resource before AOM startup"); - equal(hasChromeEntry("foo"), false, - "Should not have registered foo resource before AOM startup"); - - await promiseStartupManager(); - - equal(hasChromeEntry("foo-langpack"), false, - "Should not have registered chrome manifest for invalid extension"); - equal(hasChromeEntry("foo-legacy-legacy"), false, - "Should not have registered chrome manifest for non-restartless extension"); - equal(hasChromeEntry("foo"), true, - "Should have registered chrome manifest for valid extension"); - - await promiseRestartManager(); - - equal(hasChromeEntry("foo-langpack"), false, - "Should still not have registered chrome manifest for invalid extension after restart"); - equal(hasChromeEntry("foo-legacy-legacy"), false, - "Should still not have registered chrome manifest for non-restartless extension"); - equal(hasChromeEntry("foo"), true, - "Should still have registered chrome manifest for valid extension after restart"); - - await promiseShutdownManager(); - - userAppDir.remove(true); -}); diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_legacy.js b/toolkit/mozapps/extensions/test/xpcshell/test_legacy.js deleted file mode 100644 index 270e18a1bd5d..000000000000 --- a/toolkit/mozapps/extensions/test/xpcshell/test_legacy.js +++ /dev/null @@ -1,118 +0,0 @@ - -const LEGACY_PREF = "extensions.legacy.enabled"; - -createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1"); - -add_task(async function test_disable() { - await promiseStartupManager(); - - let legacy = [ - { - id: "bootstrap@tests.mozilla.org", - name: "Bootstrap add-on", - version: "1.0", - bootstrap: true, - }, - ]; - - let nonLegacy = [ - { - id: "webextension@tests.mozilla.org", - manifest: { - applications: {gecko: {id: "webextension@tests.mozilla.org"}}, - }, - }, - { - id: "privileged@tests.mozilla.org", - name: "Privileged Bootstrap add-on", - version: "1.0", - bootstrap: true, - }, - { - id: "dictionary@tests.mozilla.org", - name: "Test Dictionary", - version: "1.0", - type: "64", - }, - ]; - - function makeXPI(info) { - if (info.manifest) { - return createTempWebExtensionFile(info); - } - - return createTempXPIFile(Object.assign({}, info, { - targetApplications: [{ - id: "xpcshell@tests.mozilla.org", - minVersion: "1", - maxVersion: "1", - }], - })); - } - - AddonTestUtils.usePrivilegedSignatures = id => id.startsWith("privileged"); - - // Start out with legacy extensions disabled, installing non-legacy - // extensions should succeed. - Services.prefs.setBoolPref(LEGACY_PREF, false); - let installs = await Promise.all(nonLegacy.map(info => { - let xpi = makeXPI(info); - return AddonManager.getInstallForFile(xpi); - })); - await promiseCompleteAllInstalls(installs); - for (let install of installs) { - Assert.equal(install.state, AddonManager.STATE_INSTALLED); - Assert.equal(install.error, 0); - } - let addons = await AddonManager.getAddonsByIDs(nonLegacy.map(a => a.id)); - for (let addon of addons) { - Assert.equal(addon.appDisabled, false); - } - - // And installing legacy extensions should fail - let legacyXPIs = legacy.map(makeXPI); - installs = await Promise.all(legacyXPIs.map(xpi => AddonManager.getInstallForFile(xpi))); - - // Yuck, the AddonInstall API is atrocious. Installs of incompatible - // extensions are detected when the install reaches the DOWNLOADED state - // and the install is abandoned at that point. Since this is a local file - // install we just start out in the DOWNLOADED state. - for (let install of installs) { - Assert.equal(install.state, AddonManager.STATE_DOWNLOADED); - Assert.equal(install.addon.appDisabled, true); - } - - // Now enable legacy extensions, and we should be able to install - // the legacy extensions. - Services.prefs.setBoolPref(LEGACY_PREF, true); - installs = await Promise.all(legacyXPIs.map(xpi => AddonManager.getInstallForFile(xpi))); - for (let install of installs) { - Assert.equal(install.state, AddonManager.STATE_DOWNLOADED); - Assert.equal(install.addon.appDisabled, false); - } - await promiseCompleteAllInstalls(installs); - for (let install of installs) { - Assert.equal(install.state, AddonManager.STATE_INSTALLED); - Assert.equal(install.error, 0); - } - addons = await AddonManager.getAddonsByIDs(legacy.map(a => a.id)); - for (let addon of addons) { - Assert.equal(addon.appDisabled, false); - } - - // Flip the preference back, the legacy extensions should become disabled - // but non-legacy extensions should remain enabled. - Services.prefs.setBoolPref(LEGACY_PREF, false); - addons = await AddonManager.getAddonsByIDs(nonLegacy.map(a => a.id)); - for (let addon of addons) { - Assert.equal(addon.appDisabled, false); - await addon.uninstall(); - } - addons = await AddonManager.getAddonsByIDs(legacy.map(a => a.id)); - for (let addon of addons) { - Assert.equal(addon.appDisabled, true); - await addon.uninstall(); - } - - Services.prefs.clearUserPref(LEGACY_PREF); -}); diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_manifest.js b/toolkit/mozapps/extensions/test/xpcshell/test_manifest.js deleted file mode 100644 index 48aa26790d09..000000000000 --- a/toolkit/mozapps/extensions/test/xpcshell/test_manifest.js +++ /dev/null @@ -1,752 +0,0 @@ -/* Any copyright is dedicated to the Public Domain. - * http://creativecommons.org/publicdomain/zero/1.0/ - */ - -// This tests that all properties are read from the install manifests and that -// items are correctly enabled/disabled based on them (blocklist tests are -// elsewhere) - -const ADDONS = [ - { - "install.rdf": { - id: "addon1@tests.mozilla.org", - version: "1.0", - bootstrap: true, - aboutURL: "chrome://test/content/about.xul", - iconURL: "chrome://test/skin/icon.png", - targetApplications: [{ - id: "xpcshell@tests.mozilla.org", - minVersion: "1", - maxVersion: "1", - }], - name: "Test Addon 1", - description: "Test Description", - creator: "Test Creator", - homepageURL: "http://www.example.com", - developer: [ - "Test Developer 1", - "Test Developer 2", - ], - translator: [ - "Test Translator 1", - "Test Translator 2", - ], - contributor: [ - "Test Contributor 1", - "Test Contributor 2", - ], - }, - - expected: { - id: "addon1@tests.mozilla.org", - type: "extension", - version: "1.0", - optionsType: null, - aboutURL: "chrome://test/content/about.xul", - iconURL: "chrome://test/skin/icon.png", - icons: {32: "chrome://test/skin/icon.png", 48: "chrome://test/skin/icon.png"}, - name: "Test Addon 1", - description: "Test Description", - creator: "Test Creator", - homepageURL: "http://www.example.com", - developers: ["Test Developer 1", "Test Developer 2"], - translators: ["Test Translator 1", "Test Translator 2"], - contributors: ["Test Contributor 1", "Test Contributor 2"], - isActive: true, - userDisabled: false, - appDisabled: false, - isCompatible: true, - providesUpdatesSecurely: true, - blocklistState: Ci.nsIBlocklistService.STATE_NOT_BLOCKED, - }, - }, - - { - "install.rdf": { - id: "addon2@tests.mozilla.org", - version: "1.0", - bootstrap: true, - updateURL: "https://www.foo.com", - targetApplications: [{ - id: "xpcshell@tests.mozilla.org", - minVersion: "1", - maxVersion: "1", - }], - name: "Test Addon 2", - }, - - expected: { - id: "addon2@tests.mozilla.org", - isActive: true, - userDisabled: false, - appDisabled: false, - providesUpdatesSecurely: true, - }, - }, - - { - "install.rdf": { - id: "addon3@tests.mozilla.org", - version: "1.0", - bootstrap: true, - updateURL: "http://www.foo.com", - targetApplications: [{ - id: "xpcshell@tests.mozilla.org", - minVersion: "1", - maxVersion: "1", - }], - name: "Test Addon 3", - }, - - expected: { - id: "addon3@tests.mozilla.org", - isActive: false, - userDisabled: false, - appDisabled: true, - providesUpdatesSecurely: false, - }, - }, - - { - "install.rdf": { - id: "addon4@tests.mozilla.org", - version: "1.0", - bootstrap: true, - updateURL: "http://www.foo.com", - updateKey: "foo", - targetApplications: [{ - id: "xpcshell@tests.mozilla.org", - minVersion: "1", - maxVersion: "1", - }], - name: "Test Addon 4", - }, - - expected: { - id: "addon4@tests.mozilla.org", - isActive: false, - userDisabled: false, - appDisabled: true, - providesUpdatesSecurely: false, - }, - }, - - { - "install.rdf": { - id: "addon5@tests.mozilla.org", - version: "1.0", - bootstrap: true, - targetApplications: [{ - id: "xpcshell@tests.mozilla.org", - minVersion: "1", - maxVersion: "*", - }], - name: "Test Addon 5", - }, - - expected: { - isActive: true, - userDisabled: false, - appDisabled: false, - isCompatible: true, - }, - }, - - { - "install.rdf": { - id: "addon6@tests.mozilla.org", - version: "1.0", - bootstrap: true, - targetApplications: [{ - id: "xpcshell@tests.mozilla.org", - minVersion: "0", - maxVersion: "1", - }], - name: "Test Addon 6", - }, - - expected: { - isActive: true, - userDisabled: false, - appDisabled: false, - isCompatible: true, - }, - }, - - { - "install.rdf": { - id: "addon7@tests.mozilla.org", - version: "1.0", - bootstrap: true, - targetApplications: [{ - id: "xpcshell@tests.mozilla.org", - minVersion: "0", - maxVersion: "0", - }], - name: "Test Addon 7", - }, - - expected: { - isActive: false, - userDisabled: false, - appDisabled: true, - isCompatible: false, - }, - }, - - { - "install.rdf": { - id: "addon8@tests.mozilla.org", - version: "1.0", - bootstrap: true, - targetApplications: [{ - id: "xpcshell@tests.mozilla.org", - minVersion: "1.1", - maxVersion: "*", - }], - name: "Test Addon 8", - }, - - expected: { - isActive: false, - userDisabled: false, - appDisabled: true, - isCompatible: false, - }, - }, - - { - "install.rdf": { - id: "addon9@tests.mozilla.org", - version: "1.0", - bootstrap: true, - targetApplications: [{ - id: "toolkit@mozilla.org", - minVersion: "1.9.2", - maxVersion: "1.9.*", - }], - name: "Test Addon 9", - }, - - expected: { - isActive: true, - userDisabled: false, - appDisabled: false, - isCompatible: true, - }, - }, - - { - "install.rdf": { - id: "addon10@tests.mozilla.org", - version: "1.0", - bootstrap: true, - targetApplications: [{ - id: "toolkit@mozilla.org", - minVersion: "1.9.2.1", - maxVersion: "1.9.*", - }], - name: "Test Addon 10", - }, - - expected: { - isActive: false, - userDisabled: false, - appDisabled: true, - isCompatible: false, - }, - }, - - { - "install.rdf": { - id: "addon11@tests.mozilla.org", - version: "1.0", - bootstrap: true, - targetApplications: [{ - id: "toolkit@mozilla.org", - minVersion: "1.9", - maxVersion: "1.9.2", - }], - name: "Test Addon 11", - }, - - expected: { - isActive: true, - userDisabled: false, - appDisabled: false, - isCompatible: true, - }, - }, - - { - "install.rdf": { - id: "addon12@tests.mozilla.org", - version: "1.0", - bootstrap: true, - targetApplications: [{ - id: "toolkit@mozilla.org", - minVersion: "1.9", - maxVersion: "1.9.1.*", - }], - name: "Test Addon 12", - }, - - expected: { - isActive: false, - userDisabled: false, - appDisabled: true, - isCompatible: false, - }, - }, - - { - "install.rdf": { - id: "addon13@tests.mozilla.org", - version: "1.0", - bootstrap: true, - targetApplications: [{ - id: "toolkit@mozilla.org", - minVersion: "1.9", - maxVersion: "1.9.*", - }, { - id: "xpcshell@tests.mozilla.org", - minVersion: "0", - maxVersion: "0.5", - }], - name: "Test Addon 13", - }, - - expected: { - isActive: false, - userDisabled: false, - appDisabled: true, - isCompatible: false, - }, - }, - - { - "install.rdf": { - id: "addon14@tests.mozilla.org", - version: "1.0", - bootstrap: true, - targetApplications: [{ - id: "toolkit@mozilla.org", - minVersion: "1.9", - maxVersion: "1.9.1", - }, { - id: "xpcshell@tests.mozilla.org", - minVersion: "1", - maxVersion: "1", - }], - name: "Test Addon 14", - }, - - expected: { - isActive: true, - userDisabled: false, - appDisabled: false, - isCompatible: true, - }, - }, - - { - "install.rdf": { - id: "addon15@tests.mozilla.org", - version: "1.0", - bootstrap: true, - updateKey: "foo", - targetApplications: [{ - id: "xpcshell@tests.mozilla.org", - minVersion: "1", - maxVersion: "1", - }], - name: "Test Addon 15", - }, - - expected: { - isActive: true, - userDisabled: false, - appDisabled: false, - isCompatible: true, - providesUpdatesSecurely: true, - }, - }, - - { - "install.rdf": { - id: "addon16@tests.mozilla.org", - version: "1.0", - bootstrap: true, - updateKey: "foo", - updateURL: "https://www.foo.com", - targetApplications: [{ - id: "xpcshell@tests.mozilla.org", - minVersion: "1", - maxVersion: "1", - }], - name: "Test Addon 16", - }, - - expected: { - isActive: true, - userDisabled: false, - appDisabled: false, - isCompatible: true, - providesUpdatesSecurely: true, - }, - }, - - { - "install.rdf": { - id: "addon17@tests.mozilla.org", - version: "1.0", - bootstrap: true, - optionsURL: "chrome://test/content/options.xul", - optionsType: "2", - targetApplications: [{ - id: "xpcshell@tests.mozilla.org", - minVersion: "1", - maxVersion: "1", - }], - name: "Test Addon 17", - }, - - // An obsolete optionsType means the add-on isn't registered. - expected: null, - }, - - { - "install.rdf": { - id: "addon18@tests.mozilla.org", - version: "1.0", - bootstrap: true, - targetApplications: [{ - id: "xpcshell@tests.mozilla.org", - minVersion: "1", - maxVersion: "1", - }], - name: "Test Addon 18", - }, - extraFiles: {"options.xul": ""}, - - expected: { - isActive: true, - userDisabled: false, - appDisabled: false, - isCompatible: true, - optionsURL: null, - optionsType: null, - }, - }, - - { - "install.rdf": { - id: "addon19@tests.mozilla.org", - version: "1.0", - bootstrap: true, - optionsType: "99", - targetApplications: [{ - id: "xpcshell@tests.mozilla.org", - minVersion: "1", - maxVersion: "1", - }], - name: "Test Addon 19", - }, - - expected: null, - }, - - { - "install.rdf": { - id: "addon20@tests.mozilla.org", - version: "1.0", - bootstrap: true, - optionsURL: "chrome://test/content/options.xul", - targetApplications: [{ - id: "xpcshell@tests.mozilla.org", - minVersion: "1", - maxVersion: "1", - }], - name: "Test Addon 20", - }, - - // Even with a defined optionsURL optionsType is null by default. - expected: { - isActive: true, - userDisabled: false, - appDisabled: false, - isCompatible: true, - optionsURL: "chrome://test/content/options.xul", - optionsType: null, - }, - }, - - { - "install.rdf": { - id: "addon21@tests.mozilla.org", - version: "1.0", - bootstrap: true, - optionsType: "3", - optionsURL: "chrome://test/content/options.xul", - targetApplications: [{ - id: "xpcshell@tests.mozilla.org", - minVersion: "1", - maxVersion: "1", - }], - name: "Test Addon 21", - }, - - expected: { - isActive: true, - userDisabled: false, - appDisabled: false, - isCompatible: true, - optionsURL: "chrome://test/content/options.xul", - optionsType: AddonManager.OPTIONS_TYPE_TAB, - }, - }, - - { - "install.rdf": { - id: "addon22@tests.mozilla.org", - version: "1.0", - bootstrap: true, - optionsType: "2", - targetApplications: [{ - id: "xpcshell@tests.mozilla.org", - minVersion: "1", - maxVersion: "1", - }], - name: "Test Addon 22", - }, - - // An obsolete optionsType means the add-on isn't registered. - expected: null, - }, - - { - "install.rdf": { - id: "addon23@tests.mozilla.org", - version: "1.0", - bootstrap: true, - optionsType: "2", - targetApplications: [{ - id: "xpcshell@tests.mozilla.org", - minVersion: "1", - maxVersion: "1", - }], - name: "Test Addon 23", - }, - extraFiles: {"options.xul": ""}, - - // An obsolete optionsType means the add-on isn't registered. - expected: null, - }, - - { - "install.rdf": { - id: "addon24@tests.mozilla.org", - version: "1.0", - bootstrap: true, - targetApplications: [{ - id: "xpcshell@tests.mozilla.org", - minVersion: "1", - maxVersion: "1", - }], - name: "Test Addon 24", - }, - extraFiles: {"options.xul": ""}, - - expected: { - optionsType: null, - optionsURL: null, - }, - }, - - { - "install.rdf": { - id: "addon25@tests.mozilla.org", - version: "1.0", - bootstrap: true, - optionsType: "3", - targetApplications: [{ - id: "xpcshell@tests.mozilla.org", - minVersion: "1", - maxVersion: "1", - }], - name: "Test Addon 25", - }, - - expected: { - optionsType: null, - optionsURL: null, - }, - }, - - { - "install.rdf": { - id: "addon26@tests.mozilla.org", - version: "1.0", - bootstrap: true, - optionsType: "4", - targetApplications: [{ - id: "xpcshell@tests.mozilla.org", - minVersion: "1", - maxVersion: "1", - }], - name: "Test Addon 26", - }, - extraFiles: {"options.xul": ""}, - expected: null, - }, - - // Tests compatibility based on target platforms. - - // No targetPlatforms so should be compatible - { - "install.rdf": { - id: "tp-addon1@tests.mozilla.org", - version: "1.0", - bootstrap: true, - name: "Test 1", - targetApplications: [{ - id: "xpcshell@tests.mozilla.org", - minVersion: "1", - maxVersion: "1", - }], - }, - - expected: { - appDisabled: false, - isPlatformCompatible: true, - isActive: true, - }, - }, - - // Matches the OS - { - "install.rdf": { - id: "tp-addon2@tests.mozilla.org", - version: "1.0", - bootstrap: true, - name: "Test 2", - targetPlatforms: [ - "XPCShell", - "WINNT_x86", - "XPCShell", - ], - targetApplications: [{ - id: "xpcshell@tests.mozilla.org", - minVersion: "1", - maxVersion: "1", - }], - }, - - expected: { - appDisabled: false, - isPlatformCompatible: true, - isActive: true, - }, - }, - - // Matches the OS and ABI - { - "install.rdf": { - id: "tp-addon3@tests.mozilla.org", - version: "1.0", - bootstrap: true, - name: "Test 3", - targetPlatforms: [ - "WINNT", - "XPCShell_noarch-spidermonkey", - ], - targetApplications: [{ - id: "xpcshell@tests.mozilla.org", - minVersion: "1", - maxVersion: "1", - }], - }, - - expected: { - appDisabled: false, - isPlatformCompatible: true, - isActive: true, - }, - }, - - // Doesn't match - { - "install.rdf": { - id: "tp-addon4@tests.mozilla.org", - version: "1.0", - bootstrap: true, - name: "Test 4", - targetPlatforms: [ - "WINNT_noarch-spidermonkey", - "Darwin", - "WINNT_noarch-spidermonkey", - ], - targetApplications: [{ - id: "xpcshell@tests.mozilla.org", - minVersion: "1", - maxVersion: "1", - }], - }, - - expected: { - appDisabled: true, - isPlatformCompatible: false, - isActive: false, - }, - }, - - // Matches the OS but since a different entry specifies ABI this doesn't match. - { - "install.rdf": { - id: "tp-addon5@tests.mozilla.org", - version: "1.0", - bootstrap: true, - name: "Test 5", - targetPlatforms: [ - "XPCShell", - "XPCShell_foo", - ], - targetApplications: [{ - id: "xpcshell@tests.mozilla.org", - minVersion: "1", - maxVersion: "1", - }], - }, - - expected: { - appDisabled: true, - isPlatformCompatible: false, - isActive: false, - }, - }, -]; - -const IDS = ADDONS.map(a => a["install.rdf"].id); - -add_task(async function setup() { - createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9.2"); - const profileDir = gProfD.clone(); - profileDir.append("extensions"); - - for (let addon of ADDONS) { - await promiseWriteInstallRDFForExtension(addon["install.rdf"], profileDir, undefined, addon.extraFiles); - } -}); - -add_task(async function test_values() { - await promiseStartupManager(); - - let addons = await getAddons(IDS); - - for (let addon of ADDONS) { - let {id} = addon["install.rdf"]; - checkAddon(id, addons.get(id), addon.expected); - } - - await promiseShutdownManager(); -}); diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_manifest_locales.js b/toolkit/mozapps/extensions/test/xpcshell/test_manifest_locales.js deleted file mode 100644 index 8c366eda17a0..000000000000 --- a/toolkit/mozapps/extensions/test/xpcshell/test_manifest_locales.js +++ /dev/null @@ -1,134 +0,0 @@ -/* 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/. - */ - -const ID = "bug397778@tests.mozilla.org"; - -const ADDON = { - id: "bug397778@tests.mozilla.org", - version: "1.0", - name: "Fallback Name", - description: "Fallback Description", - bootstrap: true, - - targetApplications: [{ - id: "xpcshell@tests.mozilla.org", - minVersion: "1", - maxVersion: "1"}], - - localized: [ - { - locale: ["fr"], - name: "fr Name", - description: "fr Description", - }, - { - locale: ["de-DE"], - name: "Deutsches W\u00f6rterbuch", - }, - { - locale: ["es-ES"], - name: "es-ES Name", - description: "es-ES Description", - }, - { - locale: ["zh-TW"], - name: "zh-TW Name", - description: "zh-TW Description", - }, - { - locale: ["zh-CN"], - name: "zh-CN Name", - description: "zh-CN Description", - }, - { - locale: ["en-GB"], - name: "en-GB Name", - description: "en-GB Description", - }, - { - locale: ["en"], - name: "en Name", - description: "en Description", - }, - { - locale: ["en-CA"], - name: "en-CA Name", - description: "en-CA Description", - }, - ], -}; - -add_task(async function setup() { - createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1"); - Services.locale.requestedLocales = ["fr-FR"]; - - await promiseStartupManager(); - await promiseInstallXPI(ADDON); -}); - -add_task(async function test_1() { - let addon = await AddonManager.getAddonByID(ID); - Assert.notEqual(addon, null); - Assert.equal(addon.name, "fr Name"); - Assert.equal(addon.description, "fr Description"); - - await addon.disable(); - await promiseRestartManager(); - - let newAddon = await AddonManager.getAddonByID(ID); - Assert.notEqual(newAddon, null); - Assert.equal(newAddon.name, "fr Name"); -}); - -add_task(async function test_2() { - // Change locale. The more specific de-DE is the best match - await restartWithLocales(["de"]); - - let addon = await AddonManager.getAddonByID(ID); - Assert.notEqual(addon, null); - Assert.equal(addon.name, "Deutsches W\u00f6rterbuch"); - Assert.equal(addon.description, null); -}); - -add_task(async function test_3() { - // Change locale. Locale case should have no effect - await restartWithLocales(["DE-de"]); - - let addon = await AddonManager.getAddonByID(ID); - Assert.notEqual(addon, null); - Assert.equal(addon.name, "Deutsches W\u00f6rterbuch"); - Assert.equal(addon.description, null); -}); - -add_task(async function test_4() { - // Change locale. es-ES should closely match - await restartWithLocales(["es-AR"]); - - let addon = await AddonManager.getAddonByID(ID); - Assert.notEqual(addon, null); - Assert.equal(addon.name, "es-ES Name"); - Assert.equal(addon.description, "es-ES Description"); -}); - -add_task(async function test_5() { - // Change locale. Either zh-CN or zh-TW could match - await restartWithLocales(["zh"]); - - let addon = await AddonManager.getAddonByID(ID); - Assert.notEqual(addon, null); - ok(addon.name == "zh-TW Name" || addon.name == "zh-CN Name", - `Add-on name mismatch: ${addon.name}`); -}); - -add_task(async function test_6() { - // Unknown locale should try to match against en-US as well. Of en,en-GB - // en should match as being less specific - await restartWithLocales(["nl-NL"]); - - let addon = await AddonManager.getAddonByID(ID); - Assert.notEqual(addon, null); - Assert.equal(addon.name, "en Name"); - Assert.equal(addon.description, "en Description"); -}); diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_onPropertyChanged_appDisabled.js b/toolkit/mozapps/extensions/test/xpcshell/test_onPropertyChanged_appDisabled.js index d000aaf0290e..bf80f9727333 100644 --- a/toolkit/mozapps/extensions/test/xpcshell/test_onPropertyChanged_appDisabled.js +++ b/toolkit/mozapps/extensions/test/xpcshell/test_onPropertyChanged_appDisabled.js @@ -1,64 +1,45 @@ /* Any copyright is dedicated to the Public Domain. http://creativecommons.org/publicdomain/zero/1.0/ */ -const profileDir = gProfD.clone(); -profileDir.append("extensions"); - -async function run_test() { - do_test_pending(); +const ID = "addon1@tests.mozilla.org"; +add_task(async function run_test() { createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9.2"); - await promiseWriteInstallRDFForExtension({ - id: "addon1@tests.mozilla.org", - version: "1.0", - name: "Test 1", - bootstrap: true, + let xpi = createAddon({ + id: ID, targetApplications: [{ id: "xpcshell@tests.mozilla.org", minVersion: "0.1", maxVersion: "0.2", }], - }, profileDir); + }); + await manuallyInstall(xpi, AddonTestUtils.profileExtensions, ID); + AddonManager.strictCompatibility = false; await promiseStartupManager(); - AddonManager.strictCompatibility = false; + let addon = await AddonManager.getAddonByID(ID); + Assert.notEqual(addon, null); + await addon.disable(); - let aAddon = await AddonManager.getAddonByID("addon1@tests.mozilla.org"); - Assert.notEqual(aAddon, null); - await aAddon.disable(); - executeSoon(run_test_1); -} - -async function run_test_1() { - await promiseRestartManager(); - let aAddon = await AddonManager.getAddonByID("addon1@tests.mozilla.org"); - Assert.notEqual(aAddon, null); - Assert.ok(aAddon.userDisabled); - Assert.ok(!aAddon.isActive); - Assert.ok(!aAddon.appDisabled); - - prepare_test({ - "addon1@tests.mozilla.org": [ - ["onPropertyChanged", ["appDisabled"]], - ], - }, [], run_test_2); + Assert.ok(addon.userDisabled); + Assert.ok(!addon.isActive); + Assert.ok(!addon.appDisabled); + let promise = promiseAddonEvent("onPropertyChanged"); AddonManager.strictCompatibility = true; -} + let [, properties] = await promise; -async function run_test_2() { - let aAddon = await AddonManager.getAddonByID("addon1@tests.mozilla.org"); - Assert.notEqual(aAddon, null); - Assert.ok(aAddon.userDisabled); - Assert.ok(!aAddon.isActive); - Assert.ok(aAddon.appDisabled); - - prepare_test({ - "addon1@tests.mozilla.org": [ - ["onPropertyChanged", ["appDisabled"]], - ], - }, [], callback_soon(do_test_finished)); + Assert.deepEqual(properties, ["appDisabled"], + "Got onPropertyChanged for appDisabled"); + Assert.ok(addon.appDisabled); + promise = promiseAddonEvent("onPropertyChanged"); AddonManager.strictCompatibility = false; -} + [, properties] = await promise; + + Assert.deepEqual(properties, ["appDisabled"], + "Got onPropertyChanged for appDisabled"); + Assert.ok(!addon.appDisabled); +}); + diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_registry.js b/toolkit/mozapps/extensions/test/xpcshell/test_registry.js index 7068dab6b071..8e580293f389 100644 --- a/toolkit/mozapps/extensions/test/xpcshell/test_registry.js +++ b/toolkit/mozapps/extensions/test/xpcshell/test_registry.js @@ -10,45 +10,20 @@ Services.prefs.setIntPref("extensions.enabledScopes", AddonManager.SCOPE_PROFILE + AddonManager.SCOPE_USER + AddonManager.SCOPE_SYSTEM); -var addon1 = { - id: "addon1@tests.mozilla.org", - version: "1.0", - name: "Test 1", - bootstrap: true, - targetApplications: [{ - id: "xpcshell@tests.mozilla.org", - minVersion: "1", - maxVersion: "1", - }], -}; - -var addon2 = { - id: "addon2@tests.mozilla.org", - version: "2.0", - name: "Test 2", - bootstrap: true, - targetApplications: [{ - id: "xpcshell@tests.mozilla.org", - minVersion: "1", - maxVersion: "2", - }], -}; - -const IDS = ["addon1@tests.mozilla.org", - "addon2@tests.mozilla.org", - "addon3@tests.mozilla.org"]; - - -var addon1Dir; -var addon2Dir; -const addon3Dir = gProfD.clone(); -addon3Dir.append("addon3@tests.mozilla.org"); +const ID1 = "addon1@tests.mozilla.org"; +const ID2 = "addon2@tests.mozilla.org"; +let xpi1, xpi2; let registry; add_task(async function setup() { - addon1Dir = await promiseWriteInstallRDFForExtension(addon1, gProfD, "addon1"); - addon2Dir = await promiseWriteInstallRDFForExtension(addon2, gProfD, "addon2"); + xpi1 = await createTempWebExtensionFile({ + manifest: {applications: {gecko: {id: ID1}}}, + }); + + xpi2 = await createTempWebExtensionFile({ + manifest: {applications: {gecko: {id: ID2}}}, + }); registry = new MockRegistry(); registerCleanupFunction(() => { @@ -59,18 +34,15 @@ add_task(async function setup() { // Tests whether basic registry install works add_task(async function test_1() { registry.setValue(Ci.nsIWindowsRegKey.ROOT_KEY_LOCAL_MACHINE, - "SOFTWARE\\Mozilla\\XPCShell\\Extensions", - "addon1@tests.mozilla.org", addon1Dir.path); + "SOFTWARE\\Mozilla\\XPCShell\\Extensions", + ID1, xpi1.path); registry.setValue(Ci.nsIWindowsRegKey.ROOT_KEY_CURRENT_USER, "SOFTWARE\\Mozilla\\XPCShell\\Extensions", - "addon2@tests.mozilla.org", addon2Dir.path); - registry.setValue(Ci.nsIWindowsRegKey.ROOT_KEY_CURRENT_USER, - "SOFTWARE\\Mozilla\\XPCShell\\Extensions", - "addon3@tests.mozilla.org", addon3Dir.path); + ID2, xpi2.path); await promiseStartupManager(); - let [a1, a2, a3] = await AddonManager.getAddonsByIDs(IDS); + let [a1, a2] = await AddonManager.getAddonsByIDs([ID1, ID2]); notEqual(a1, null); ok(a1.isActive); ok(!hasFlag(a1.permissions, AddonManager.PERM_CAN_UNINSTALL)); @@ -80,48 +52,38 @@ add_task(async function test_1() { ok(a2.isActive); ok(!hasFlag(a2.permissions, AddonManager.PERM_CAN_UNINSTALL)); equal(a2.scope, AddonManager.SCOPE_USER); - - equal(a3, null); }); // Tests whether uninstalling from the registry works add_task(async function test_2() { registry.setValue(Ci.nsIWindowsRegKey.ROOT_KEY_LOCAL_MACHINE, "SOFTWARE\\Mozilla\\XPCShell\\Extensions", - "addon1@tests.mozilla.org", null); + ID1, null); registry.setValue(Ci.nsIWindowsRegKey.ROOT_KEY_CURRENT_USER, "SOFTWARE\\Mozilla\\XPCShell\\Extensions", - "addon2@tests.mozilla.org", null); - registry.setValue(Ci.nsIWindowsRegKey.ROOT_KEY_CURRENT_USER, - "SOFTWARE\\Mozilla\\XPCShell\\Extensions", - "addon3@tests.mozilla.org", null); + ID2, null); await promiseRestartManager(); - let [a1, a2, a3] = await AddonManager.getAddonsByIDs(IDS); + let [a1, a2] = await AddonManager.getAddonsByIDs([ID1, ID2]); equal(a1, null); equal(a2, null); - equal(a3, null); }); // Checks that the ID in the registry must match that in the install manifest add_task(async function test_3() { registry.setValue(Ci.nsIWindowsRegKey.ROOT_KEY_LOCAL_MACHINE, "SOFTWARE\\Mozilla\\XPCShell\\Extensions", - "addon1@tests.mozilla.org", addon2Dir.path); + ID1, xpi2.path); registry.setValue(Ci.nsIWindowsRegKey.ROOT_KEY_CURRENT_USER, "SOFTWARE\\Mozilla\\XPCShell\\Extensions", - "addon2@tests.mozilla.org", addon1Dir.path); - registry.setValue(Ci.nsIWindowsRegKey.ROOT_KEY_CURRENT_USER, - "SOFTWARE\\Mozilla\\XPCShell\\Extensions", - "addon3@tests.mozilla.org", addon3Dir.path); + ID2, xpi1.path); await promiseRestartManager(); - let [a1, a2, a3] = await AddonManager.getAddonsByIDs(IDS); + let [a1, a2] = await AddonManager.getAddonsByIDs([ID1, ID2]); equal(a1, null); equal(a2, null); - equal(a3, null); }); // Tests whether an extension's ID can change without its directory changing @@ -131,34 +93,30 @@ add_task(async function test_4() { registry.setValue(Ci.nsIWindowsRegKey.ROOT_KEY_LOCAL_MACHINE, "SOFTWARE\\Mozilla\\XPCShell\\Extensions", - "addon1@tests.mozilla.org", null); + ID1, null); registry.setValue(Ci.nsIWindowsRegKey.ROOT_KEY_CURRENT_USER, "SOFTWARE\\Mozilla\\XPCShell\\Extensions", - "addon2@tests.mozilla.org", null); - registry.setValue(Ci.nsIWindowsRegKey.ROOT_KEY_CURRENT_USER, - "SOFTWARE\\Mozilla\\XPCShell\\Extensions", - "addon3@tests.mozilla.org", null); + ID2, null); await promiseRestartManager(); registry.setValue(Ci.nsIWindowsRegKey.ROOT_KEY_LOCAL_MACHINE, "SOFTWARE\\Mozilla\\XPCShell\\Extensions", - "addon1@tests.mozilla.org", addon1Dir.path); + ID1, xpi1.path); await promiseShutdownManager(); registry.setValue(Ci.nsIWindowsRegKey.ROOT_KEY_LOCAL_MACHINE, "SOFTWARE\\Mozilla\\XPCShell\\Extensions", - "addon1@tests.mozilla.org", null); + ID1, null); registry.setValue(Ci.nsIWindowsRegKey.ROOT_KEY_CURRENT_USER, "SOFTWARE\\Mozilla\\XPCShell\\Extensions", - "addon2@tests.mozilla.org", addon1Dir.path); - await promiseWriteInstallRDFForExtension(addon2, gProfD, "addon1"); + ID2, xpi1.path); + xpi2.copyTo(xpi1.parent, xpi1.leafName); await promiseStartupManager(); - let [a1, a2, a3] = await AddonManager.getAddonsByIDs(IDS); + let [a1, a2] = await AddonManager.getAddonsByIDs([ID1, ID2]); equal(a1, null); notEqual(a2, null); - equal(a3, null); }); diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_shutdown.js b/toolkit/mozapps/extensions/test/xpcshell/test_shutdown.js index df614d5b202f..609b304a9f00 100644 --- a/toolkit/mozapps/extensions/test/xpcshell/test_shutdown.js +++ b/toolkit/mozapps/extensions/test/xpcshell/test_shutdown.js @@ -10,6 +10,7 @@ const IGNORE = ["getPreferredIconURL", "escapeAddonURI", "addAddonListener", "removeAddonListener", "addInstallListener", "removeInstallListener", "addManagerListener", "removeManagerListener", + "addExternalExtensionLoader", "shutdown", "init", "stateToString", "errorToString", "getUpgradeListener", "addUpgradeListener", "removeUpgradeListener", diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_strictcompatibility.js b/toolkit/mozapps/extensions/test/xpcshell/test_strictcompatibility.js index 8abe753216ad..8d6b53a07740 100644 --- a/toolkit/mozapps/extensions/test/xpcshell/test_strictcompatibility.js +++ b/toolkit/mozapps/extensions/test/xpcshell/test_strictcompatibility.js @@ -8,23 +8,17 @@ // The `compatbile` array defines which of the tests below the add-on // should be compatible in. It's pretty gross. -const ADDONS = { +const ADDONS = [ // Always compatible - "addon1@tests.mozilla.org": { - "install.rdf": { + { + manifest: { id: "addon1@tests.mozilla.org", - version: "1.0", - name: "Test 1", - bootstrap: true, targetApplications: [{ id: "xpcshell@tests.mozilla.org", minVersion: "1", maxVersion: "1", }], }, - expected: { - strictCompatibility: false, - }, compatible: { nonStrict: true, strict: true, @@ -32,21 +26,15 @@ const ADDONS = { }, // Incompatible in strict compatibility mode - "addon2@tests.mozilla.org": { - "install.rdf": { + { + manifest: { id: "addon2@tests.mozilla.org", - version: "1.0", - name: "Test 2", - bootstrap: true, targetApplications: [{ id: "xpcshell@tests.mozilla.org", minVersion: "0.7", maxVersion: "0.8", }], }, - expected: { - strictCompatibility: false, - }, compatible: { nonStrict: true, strict: false, @@ -54,12 +42,9 @@ const ADDONS = { }, // Opt-in to strict compatibility - always incompatible - "addon4@tests.mozilla.org": { - "install.rdf": { - id: "addon4@tests.mozilla.org", - version: "1.0", - name: "Test 4", - bootstrap: true, + { + manifest: { + id: "addon3@tests.mozilla.org", strictCompatibility: true, targetApplications: [{ id: "xpcshell@tests.mozilla.org", @@ -67,9 +52,6 @@ const ADDONS = { maxVersion: "0.9", }], }, - expected: { - strictCompatibility: true, - }, compatible: { nonStrict: false, strict: false, @@ -78,89 +60,50 @@ const ADDONS = { // Addon from the future - would be marked as compatibile-by-default, // but minVersion is higher than the app version - "addon5@tests.mozilla.org": { - "install.rdf": { - id: "addon5@tests.mozilla.org", - version: "1.0", - name: "Test 5", - bootstrap: true, + { + manifest: { + id: "addon4@tests.mozilla.org", targetApplications: [{ id: "xpcshell@tests.mozilla.org", minVersion: "3", maxVersion: "5", }], }, - expected: { - strictCompatibility: false, - }, compatible: { nonStrict: false, strict: false, }, }, - // Extremely old addon - maxVersion is less than the minimum compat version - // set in extensions.minCompatibleVersion - "addon6@tests.mozilla.org": { - "install.rdf": { - id: "addon6@tests.mozilla.org", - version: "1.0", - name: "Test 6", - bootstrap: true, - targetApplications: [{ - id: "xpcshell@tests.mozilla.org", - minVersion: "0.1", - maxVersion: "0.2", - }], - }, - expected: { - strictCompatibility: false, - }, - compatible: { - nonStrict: true, - strict: false, - }, - }, - // Dictionary - compatible even in strict compatibility mode - "addon7@tests.mozilla.org": { - "install.rdf": { - id: "addon7@tests.mozilla.org", - version: "1.0", - name: "Test 7", - type: "64", + { + manifest: { + id: "addon5@tests.mozilla.org", + type: "dictionary", targetApplications: [{ id: "xpcshell@tests.mozilla.org", minVersion: "0.8", maxVersion: "0.9", }], }, - expected: { - strictCompatibility: false, - }, compatible: { nonStrict: true, strict: true, }, }, -}; - -const IDS = Object.keys(ADDONS); - -const profileDir = gProfD.clone(); -profileDir.append("extensions"); +]; async function checkCompatStatus(strict, index) { info(`Checking compat status for test ${index}\n`); equal(AddonManager.strictCompatibility, strict); - let addons = await getAddons(IDS); - for (let [id, addon] of Object.entries(ADDONS)) { - checkAddon(id, addons.get(id), { - ...addon.expected, - isCompatible: addon.compatible[index], - appDisabled: !addon.compatible[index], + for (let test of ADDONS) { + let {id} = test.manifest; + let addon = await promiseAddonByID(id); + checkAddon(id, addon, { + isCompatible: test.compatible[index], + appDisabled: !test.compatible[index], }); } } @@ -168,8 +111,9 @@ async function checkCompatStatus(strict, index) { add_task(async function setup() { createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9.2"); - for (let addon of Object.values(ADDONS)) { - await promiseWriteInstallRDFForExtension(addon["install.rdf"], profileDir); + for (let addon of ADDONS) { + let xpi = await createAddon(addon.manifest); + await manuallyInstall(xpi, AddonTestUtils.profileExtensions, addon.manifest.id); } await promiseStartupManager(); @@ -191,14 +135,11 @@ add_task(async function test_2() { await checkCompatStatus(true, "strict"); }); -const CHECK_COMPAT_ADDONS = { - "cc-addon1@tests.mozilla.org": { - "install.rdf": { - // Cannot be enabled as it has no target app info for the applciation +const CHECK_COMPAT_ADDONS = [ + // Cannot be enabled as it has no target app info for the applciation + { + manifest: { id: "cc-addon1@tests.mozilla.org", - version: "1.0", - name: "Test 1", - bootstrap: true, targetApplications: [{ id: "unknown@tests.mozilla.org", minVersion: "1", @@ -209,14 +150,12 @@ const CHECK_COMPAT_ADDONS = { canOverride: false, }, - "cc-addon2@tests.mozilla.org": { - "install.rdf": { - // Always appears incompatible but can be enabled if compatibility checking is - // disabled + + // Always appears incompatible but can be enabled if compatibility checking is + // disabled + { + manifest: { id: "cc-addon2@tests.mozilla.org", - version: "1.0", - name: "Test 2", - bootstrap: true, targetApplications: [{ id: "toolkit@mozilla.org", minVersion: "1", @@ -227,13 +166,10 @@ const CHECK_COMPAT_ADDONS = { canOverride: true, }, - "cc-addon4@tests.mozilla.org": { - "install.rdf": { - // Always compatible and enabled - id: "cc-addon4@tests.mozilla.org", - version: "1.0", - name: "Test 4", - bootstrap: true, + // Always compatible and enabled + { + manifest: { + id: "cc-addon3@tests.mozilla.org", targetApplications: [{ id: "toolkit@mozilla.org", minVersion: "1", @@ -243,13 +179,10 @@ const CHECK_COMPAT_ADDONS = { compatible: true, }, - "cc-addon5@tests.mozilla.org": { - "install.rdf": { - // Always compatible and enabled - id: "cc-addon5@tests.mozilla.org", - version: "1.0", - name: "Test 5", - bootstrap: true, + // Always compatible and enabled + { + manifest: { + id: "cc-addon4@tests.mozilla.org", targetApplications: [{ id: "xpcshell@tests.mozilla.org", minVersion: "1", @@ -258,17 +191,15 @@ const CHECK_COMPAT_ADDONS = { }, compatible: true, }, -}; - -const CHECK_COMPAT_IDS = Object.keys(CHECK_COMPAT_ADDONS); +]; async function checkCompatOverrides(overridden) { - let addons = await getAddons(CHECK_COMPAT_IDS); - - for (let [id, addon] of Object.entries(CHECK_COMPAT_ADDONS)) { - checkAddon(id, addons.get(id), { - isCompatible: addon.compatible, - isActive: addon.compatible || (overridden && addon.canOverride), + for (let test of CHECK_COMPAT_ADDONS) { + let {id} = test.manifest; + let addon = await promiseAddonByID(id); + checkAddon(id, addon, { + isCompatible: test.compatible, + isActive: test.compatible || (overridden && test.canOverride), }); } } @@ -283,8 +214,10 @@ add_task(async function setupCheckCompat() { Object.assign(AddonTestUtils.appInfo, {version: "2.2.3", platformVersion: "2"}); - for (let addon of Object.values(CHECK_COMPAT_ADDONS)) { - await promiseWriteInstallRDFForExtension(addon["install.rdf"], profileDir); + for (let addon of CHECK_COMPAT_ADDONS) { + let {manifest} = addon; + let xpi = await createAddon(manifest); + await manuallyInstall(xpi, AddonTestUtils.profileExtensions, manifest.id); } await promiseRestartManager("2.2.3"); }); @@ -329,3 +262,4 @@ add_task(async function test_compat_overrides_4() { await checkCompatOverrides(false); }); + diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_trash_directory.js b/toolkit/mozapps/extensions/test/xpcshell/test_trash_directory.js index ecaf8f501809..42bccfd055ba 100644 --- a/toolkit/mozapps/extensions/test/xpcshell/test_trash_directory.js +++ b/toolkit/mozapps/extensions/test/xpcshell/test_trash_directory.js @@ -2,22 +2,12 @@ * http://creativecommons.org/publicdomain/zero/1.0/ */ -const ADDONS = { - test_bootstrap1_1: { - "install.rdf": { - "id": "bootstrap1@tests.mozilla.org", - "name": "Test Bootstrap 1", - }, - "bootstrap.js": BOOTSTRAP_MONITOR_BOOTSTRAP_JS, - }, -}; - -add_task(async function setup() { +// Test that an open file inside the trash directory does not cause +// unrelated installs to break (see bug 1180901 for more background). +add_task(async function test() { createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9.2"); await promiseStartupManager(); -}); -add_task(async function() { let profileDir = OS.Constants.Path.profileDir; let trashDir = OS.Path.join(profileDir, "extensions", "trash"); let testFile = OS.Path.join(trashDir, "test.txt"); @@ -34,33 +24,13 @@ add_task(async function() { let fileExists = await OS.File.exists(testFile); ok(fileExists, "test.txt should have been created in " + trashDir); - let promiseInstallStatus = new Promise((resolve, reject) => { - let listener = { - onInstallFailed() { - AddonManager.removeInstallListener(listener); - reject("extension installation should not have failed"); - }, - onInstallEnded() { - AddonManager.removeInstallListener(listener); - ok(true, "extension installation should not have failed"); - resolve(); - }, - }; - - AddonManager.addInstallListener(listener); - }); - - await AddonTestUtils.promiseInstallXPI(ADDONS.test_bootstrap1_1); + await promiseInstallWebExtension({}); // The testFile should still exist at this point because we have not // yet closed the file handle and as a result, Windows cannot remove it. fileExists = await OS.File.exists(testFile); ok(fileExists, "test.txt should still exist"); - // Wait for the AddonManager to tell us if the installation of the extension - // succeeded or not. - await promiseInstallStatus; - // Cleanup await promiseShutdownManager(); await file.close(); diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_update_compatmode.js b/toolkit/mozapps/extensions/test/xpcshell/test_update_compatmode.js index 07ba07830d4a..34e3024ae328 100644 --- a/toolkit/mozapps/extensions/test/xpcshell/test_update_compatmode.js +++ b/toolkit/mozapps/extensions/test/xpcshell/test_update_compatmode.js @@ -5,174 +5,90 @@ // This verifies that add-on update check correctly fills in the // %COMPATIBILITY_MODE% token in the update URL. +Cu.importGlobalProperties(["URLSearchParams"]); // The test extension uses an insecure update url. Services.prefs.setBoolPref(PREF_EM_CHECK_UPDATE_SECURITY, false); -var testserver = AddonTestUtils.createHttpServer({hosts: ["example.com"]}); -testserver.registerDirectory("/data/", do_get_file("data")); +let testserver = createHttpServer({hosts: ["example.com"]}); -const profileDir = gProfD.clone(); -profileDir.append("extensions"); +let lastMode; +testserver.registerPathHandler("/update.json", (request, response) => { + let params = new URLSearchParams(request.queryString); + lastMode = params.get("mode"); -async function run_test() { - do_test_pending(); + response.setHeader("content-type", "application/json", true); + response.write(JSON.stringify({addons: {}})); +}); + +const ID_NORMAL = "compatmode@tests.mozilla.org"; +const ID_STRICT = "compatmode-strict@tests.mozilla.org"; + +add_task(async function setup() { createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9.2"); - await promiseWriteInstallRDFForExtension({ - id: "compatmode-normal@tests.mozilla.org", - version: "1.0", - bootstrap: true, - updateURL: "http://example.com/data/test_updatecompatmode_%COMPATIBILITY_MODE%.json", + let xpi = await createAddon({ + id: ID_NORMAL, + updateURL: "http://example.com/update.json?mode=%COMPATIBILITY_MODE%", targetApplications: [{ id: "xpcshell@tests.mozilla.org", minVersion: "1", maxVersion: "1", }], - name: "Test Addon - normal", - }, profileDir); + }); + await manuallyInstall(xpi, AddonTestUtils.profileExtensions, ID_NORMAL); - await promiseWriteInstallRDFForExtension({ - id: "compatmode-strict@tests.mozilla.org", - version: "1.0", - bootstrap: true, - updateURL: "http://example.com/data/test_updatecompatmode_%COMPATIBILITY_MODE%.json", - targetApplications: [{ - id: "xpcshell@tests.mozilla.org", - minVersion: "1", - maxVersion: "1", - }], - name: "Test Addon - strict", - }, profileDir); - - await promiseWriteInstallRDFForExtension({ - id: "compatmode-strict-optin@tests.mozilla.org", - version: "1.0", - bootstrap: true, - updateURL: "http://example.com/data/test_updatecompatmode_%COMPATIBILITY_MODE%.json", - targetApplications: [{ - id: "xpcshell@tests.mozilla.org", - minVersion: "1", - maxVersion: "1", - }], - name: "Test Addon - strict opt-in", + xpi = await createAddon({ + id: ID_STRICT, + updateURL: "http://example.com/update.json?mode=%COMPATIBILITY_MODE%", strictCompatibility: true, - }, profileDir); - - await promiseWriteInstallRDFForExtension({ - id: "compatmode-ignore@tests.mozilla.org", - version: "1.0", - bootstrap: true, - updateURL: "http://example.com/data/test_updatecompatmode_%COMPATIBILITY_MODE%.json", targetApplications: [{ id: "xpcshell@tests.mozilla.org", minVersion: "1", maxVersion: "1", }], - name: "Test Addon - ignore", - }, profileDir); + }); + await manuallyInstall(xpi, AddonTestUtils.profileExtensions, ID_STRICT); await promiseStartupManager(); - run_test_1(); -} - -function end_test() { - do_test_finished(); -} - +}); // Strict compatibility checking disabled. -async function run_test_1() { - info("Testing with strict compatibility checking disabled"); +add_task(async function test_strict_disabled() { Services.prefs.setBoolPref(PREF_EM_STRICT_COMPATIBILITY, false); - let addon = await AddonManager.getAddonByID("compatmode-normal@tests.mozilla.org"); + let addon = await AddonManager.getAddonByID(ID_NORMAL); Assert.notEqual(addon, null); - addon.findUpdates({ - onCompatibilityUpdateAvailable() { - do_throw("Should have not have seen compatibility information"); - }, - onNoUpdateAvailable() { - do_throw("Should have seen an available update"); - }, - - onUpdateAvailable(unused, install) { - Assert.equal(install.version, "2.0"); - }, - - onUpdateFinished() { - run_test_2(); - }, - }, AddonManager.UPDATE_WHEN_USER_REQUESTED); -} + await promiseFindAddonUpdates(addon, AddonManager.UPDATE_WHEN_USER_REQUESTED); + Assert.equal(lastMode, "normal", "COMPATIBIILITY_MODE normal was set correctly"); +}); // Strict compatibility checking enabled. -async function run_test_2() { - info("Testing with strict compatibility checking enabled"); +add_task(async function test_strict_enabled() { Services.prefs.setBoolPref(PREF_EM_STRICT_COMPATIBILITY, true); - let addon = await AddonManager.getAddonByID("compatmode-strict@tests.mozilla.org"); + let addon = await AddonManager.getAddonByID(ID_NORMAL); Assert.notEqual(addon, null); - addon.findUpdates({ - onCompatibilityUpdateAvailable() { - do_throw("Should have not have seen compatibility information"); - }, - onNoUpdateAvailable() { - do_throw("Should have seen an available update"); - }, - - onUpdateAvailable(unused, install) { - Assert.equal(install.version, "2.0"); - }, - - onUpdateFinished() { - run_test_3(); - }, - }, AddonManager.UPDATE_WHEN_USER_REQUESTED); -} + await promiseFindAddonUpdates(addon, AddonManager.UPDATE_WHEN_USER_REQUESTED); + Assert.equal(lastMode, "strict", "COMPATIBILITY_MODE strict was set correctly"); +}); // Strict compatibility checking opt-in. -async function run_test_3() { - info("Testing with strict compatibility disabled, but addon opt-in"); +add_task(async function test_strict_optin() { Services.prefs.setBoolPref(PREF_EM_STRICT_COMPATIBILITY, false); - let addon = await AddonManager.getAddonByID("compatmode-strict-optin@tests.mozilla.org"); + let addon = await AddonManager.getAddonByID(ID_STRICT); Assert.notEqual(addon, null); - addon.findUpdates({ - onCompatibilityUpdateAvailable() { - do_throw("Should have not have seen compatibility information"); - }, - onUpdateAvailable() { - do_throw("Should not have seen an available update"); - }, - - onUpdateFinished() { - run_test_4(); - }, - }, AddonManager.UPDATE_WHEN_USER_REQUESTED); -} + await promiseFindAddonUpdates(addon, AddonManager.UPDATE_WHEN_USER_REQUESTED); + Assert.equal(lastMode, "normal", "COMPATIBILITY_MODE is normal even for an addon with strictCompatibility"); +}); // Compatibility checking disabled. -async function run_test_4() { - info("Testing with all compatibility checking disabled"); +add_task(async function test_compat_disabled() { AddonManager.checkCompatibility = false; - let addon = await AddonManager.getAddonByID("compatmode-ignore@tests.mozilla.org"); + let addon = await AddonManager.getAddonByID(ID_NORMAL); Assert.notEqual(addon, null); - addon.findUpdates({ - onCompatibilityUpdateAvailable() { - do_throw("Should have not have seen compatibility information"); - }, - onNoUpdateAvailable() { - do_throw("Should have seen an available update"); - }, - - onUpdateAvailable(unused, install) { - Assert.equal(install.version, "2.0"); - }, - - onUpdateFinished() { - end_test(); - }, - }, AddonManager.UPDATE_WHEN_USER_REQUESTED); -} + await promiseFindAddonUpdates(addon, AddonManager.UPDATE_WHEN_USER_REQUESTED); + Assert.equal(lastMode, "ignore", "COMPATIBILITY_MODE ignore was set correctly"); +}); diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_update_strictcompat.js b/toolkit/mozapps/extensions/test/xpcshell/test_update_strictcompat.js index c1714ba6b29e..e0b087ae890c 100644 --- a/toolkit/mozapps/extensions/test/xpcshell/test_update_strictcompat.js +++ b/toolkit/mozapps/extensions/test/xpcshell/test_update_strictcompat.js @@ -2,9 +2,8 @@ * http://creativecommons.org/publicdomain/zero/1.0/ */ -// This verifies that add-on update checks work -// This file is a placeholder for now, it holds test cases related to -// strict compatibility to be moved or removed shortly. +// This verifies that add-on update checks work in conjunction with +// strict compatibility settings. const PREF_GETADDONS_CACHE_ENABLED = "extensions.getAddons.cache.enabled"; @@ -12,160 +11,205 @@ const PREF_GETADDONS_CACHE_ENABLED = "extensions.getAddons.cache.enabled"; Services.prefs.setBoolPref(PREF_EM_CHECK_UPDATE_SECURITY, false); Services.prefs.setBoolPref(PREF_EM_STRICT_COMPATIBILITY, false); -const updateFile = "test_update.json"; const appId = "toolkit@mozilla.org"; -const profileDir = gProfD.clone(); -profileDir.append("extensions"); - -const ADDONS = { - test_update: { - id: "addon1@tests.mozilla.org", - version: "2.0", - name: "Test 1", - }, - test_update8: { - id: "addon8@tests.mozilla.org", - version: "2.0", - name: "Test 8", - }, - test_update12: { - id: "addon12@tests.mozilla.org", - version: "2.0", - name: "Test 12", - }, - test_install2_1: { - id: "addon2@tests.mozilla.org", - version: "2.0", - name: "Real Test 2", - }, - test_install2_2: { - id: "addon2@tests.mozilla.org", - version: "3.0", - name: "Real Test 3", - }, -}; - -var testserver = createHttpServer({hosts: ["example.com"]}); +testserver = createHttpServer({hosts: ["example.com"]}); testserver.registerDirectory("/data/", do_get_file("data")); -const XPIS = {}; - add_task(async function setup() { createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1"); - - Services.locale.requestedLocales = ["fr-FR"]; - - for (let [name, info] of Object.entries(ADDONS)) { - XPIS[name] = createTempWebExtensionFile({ - manifest: { - name: info.name, - version: info.version, - applications: {gecko: {id: info.id}}, - }, - }); - testserver.registerFile(`/addons/${name}.xpi`, XPIS[name]); - } - AddonTestUtils.updateReason = AddonManager.UPDATE_WHEN_USER_REQUESTED; - await promiseStartupManager(); + Services.prefs.setCharPref(PREF_GETADDONS_BYIDS, + "http://example.com/data/test_update_addons.json"); + Services.prefs.setCharPref(PREF_COMPAT_OVERRIDES, + "http://example.com/data/test_update_compat.json"); + Services.prefs.setBoolPref(PREF_GETADDONS_CACHE_ENABLED, true); }); // Test that the update check correctly observes the // extensions.strictCompatibility pref and compatibility overrides. -add_task(async function test_17() { - await promiseInstallWebExtension({ - manifest: { - name: "Test Addon 9", - version: "1.0", - applications: { - gecko: { - id: "addon9@tests.mozilla.org", - update_url: "http://example.com/data/" + updateFile, - }, +add_task(async function test_update_strict() { + const ID = "addon9@tests.mozilla.org"; + let xpi = await createAddon({ + id: ID, + updateURL: "http://example.com/update.json", + targetApplications: [{ + id: "xpcshell@tests.mozilla.org", + minVersion: "0.1", + maxVersion: "0.2", + }], + }); + await manuallyInstall(xpi, AddonTestUtils.profileExtensions, ID); + + await promiseStartupManager(); + + await AddonRepository.backgroundUpdateCheck(); + + let UPDATE = { + addons: { + [ID]: { + updates: [ + { + version: "2.0", + update_link: "http://example.com/addons/test_update9_2.xpi", + applications: { + gecko: { + strict_min_version: "1", + advisory_max_version: "1", + }, + }, + }, + + // Incompatible when strict compatibility is enabled + { + version: "3.0", + update_link: "http://example.com/addons/test_update9_3.xpi", + applications: { + gecko: { + strict_min_version: "0.9", + advisory_max_version: "0.9", + }, + }, + }, + + // Incompatible due to compatibility override + { + version: "4.0", + update_link: "http://example.com/addons/test_update9_4.xpi", + applications: { + gecko: { + strict_min_version: "0.9", + advisory_max_version: "0.9", + }, + }, + }, + + // Addon for future version of app + { + version: "4.0", + update_link: "http://example.com/addons/test_update9_5.xpi", + applications: { + gecko: { + strict_min_version: "5", + advisory_max_version: "6", + }, + }, + }, + ], }, }, - }); + }; - let listener; - await new Promise(resolve => { - listener = { - onNewInstall(aInstall) { - equal(aInstall.existingAddon.id, "addon9@tests.mozilla.org", - "Saw unexpected onNewInstall for " + aInstall.existingAddon.id); - equal(aInstall.version, "3.0"); - }, - onDownloadFailed(aInstall) { - resolve(); - }, - }; - AddonManager.addInstallListener(listener); + AddonTestUtils.registerJSON(testserver, "/update.json", UPDATE); - Services.prefs.setCharPref(PREF_GETADDONS_BYIDS, - `http://example.com/data/test_update_addons.json`); - Services.prefs.setCharPref(PREF_COMPAT_OVERRIDES, - `http://example.com/data/test_update_compat.json`); - Services.prefs.setBoolPref(PREF_GETADDONS_CACHE_ENABLED, true); + let addon = await AddonManager.getAddonByID(ID); + let {updateAvailable} = await promiseFindAddonUpdates(addon); - AddonManagerInternal.backgroundUpdateCheck(); - }); + Assert.notEqual(updateAvailable, null, "Got update"); + Assert.equal(updateAvailable.version, "3.0", "The correct update was selected"); + await addon.uninstall(); - AddonManager.removeInstallListener(listener); - - let a9 = await AddonManager.getAddonByID("addon9@tests.mozilla.org"); - await a9.uninstall(); + await promiseShutdownManager(); }); // Tests that compatibility updates are applied to addons when the updated // compatibility data wouldn't match with strict compatibility enabled. -add_task(async function test_18() { - await promiseInstallXPI({ - id: "addon10@tests.mozilla.org", - version: "1.0", - bootstrap: true, - updateURL: "http://example.com/data/" + updateFile, +add_task(async function test_update_strict2() { + const ID = "addon10@tests.mozilla.org"; + let xpi = createAddon({ + id: ID, + updateURL: "http://example.com/update.json", targetApplications: [{ id: appId, minVersion: "0.1", maxVersion: "0.2", }], - name: "Test Addon 10", }); + await manuallyInstall(xpi, AddonTestUtils.profileExtensions, ID); - let a10 = await AddonManager.getAddonByID("addon10@tests.mozilla.org"); - notEqual(a10, null); + await promiseStartupManager(); + await AddonRepository.backgroundUpdateCheck(); - let result = await AddonTestUtils.promiseFindAddonUpdates(a10); + const UPDATE = { + addons: { + [ID]: { + updates: [ + { + version: "1.0", + update_link: "http://example.com/addons/test_update10.xpi", + applications: { + gecko: { + strict_min_version: "0.1", + advisory_max_version: "0.4", + }, + }, + }, + ], + }, + }, + }; + + AddonTestUtils.registerJSON(testserver, "/update.json", UPDATE); + + let addon = await AddonManager.getAddonByID(ID); + notEqual(addon, null); + + let result = await promiseFindAddonUpdates(addon); ok(result.compatibilityUpdate, "Should have seen a compatibility update"); ok(!result.updateAvailable, "Should not have seen a version update"); - await a10.uninstall(); + await addon.uninstall(); + await promiseShutdownManager(); }); // Test that the update check correctly observes when an addon opts-in to // strict compatibility checking. -add_task(async function test_19() { - await promiseInstallXPI({ - id: "addon11@tests.mozilla.org", - version: "1.0", - bootstrap: true, - updateURL: "http://example.com/data/" + updateFile, +add_task(async function test_update_strict_optin() { + const ID = "addon11@tests.mozilla.org"; + let xpi = await createAddon({ + id: ID, + updateURL: "http://example.com/update.json", targetApplications: [{ id: appId, minVersion: "0.1", maxVersion: "0.2", }], - name: "Test Addon 11", }); + await manuallyInstall(xpi, AddonTestUtils.profileExtensions, ID); - let a11 = await AddonManager.getAddonByID("addon11@tests.mozilla.org"); - notEqual(a11, null); + await promiseStartupManager(); - let result = await AddonTestUtils.promiseFindAddonUpdates(a11); + await AddonRepository.backgroundUpdateCheck(); + + const UPDATE = { + addons: { + [ID]: { + updates: [ + { + version: "2.0", + update_link: "http://example.com/addons/test_update11.xpi", + applications: { + gecko: { + strict_min_version: "0.1", + strict_max_version: "0.2", + }, + }, + }, + ], + }, + }, + }; + + AddonTestUtils.registerJSON(testserver, "/update.json", UPDATE); + + let addon = await AddonManager.getAddonByID(ID); + notEqual(addon, null); + + let result = await AddonTestUtils.promiseFindAddonUpdates(addon); ok(!result.compatibilityUpdate, "Should not have seen a compatibility update"); ok(!result.updateAvailable, "Should not have seen a version update"); - await a11.uninstall(); + await addon.uninstall(); + await promiseShutdownManager(); }); - diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_upgrade.js b/toolkit/mozapps/extensions/test/xpcshell/test_upgrade.js index df74ef1452b3..728d48ab0d67 100644 --- a/toolkit/mozapps/extensions/test/xpcshell/test_upgrade.js +++ b/toolkit/mozapps/extensions/test/xpcshell/test_upgrade.js @@ -21,75 +21,78 @@ globalDir.append("extensions"); var gGlobalExisted = globalDir.exists(); var gInstallTime = Date.now(); +const ID1 = "addon1@tests.mozilla.org"; +const ID2 = "addon2@tests.mozilla.org"; +const ID3 = "addon3@tests.mozilla.org"; +const ID4 = "addon4@tests.mozilla.org"; +const PATH4 = OS.Path.join(globalDir.path, `${ID4}.xpi`); + add_task(async function setup() { createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9.2"); // Will be compatible in the first version and incompatible in subsequent versions - await promiseWriteInstallRDFForExtension({ - id: "addon1@tests.mozilla.org", - version: "1.0", - bootstrap: true, + let xpi = await createAddon({ + id: ID1, targetApplications: [{ id: "xpcshell@tests.mozilla.org", minVersion: "1", maxVersion: "1", }], - name: "Test Addon 1", targetPlatforms: [ - "XPCShell", - "WINNT_x86", + {os: "XPCShell"}, + {os: "WINNT_x86"}, ], - }, profileDir); + }); + await manuallyInstall(xpi, profileDir, ID1); + // Works in all tested versions - await promiseWriteInstallRDFForExtension({ - id: "addon2@tests.mozilla.org", - version: "1.0", - bootstrap: true, + xpi = await createAddon({ + id: ID2, targetApplications: [{ id: "xpcshell@tests.mozilla.org", minVersion: "1", maxVersion: "2", }], - name: "Test Addon 2", targetPlatforms: [ - "XPCShell_noarch-spidermonkey", + { + os: "XPCShell", + abi: "noarch-spidermonkey", + }, ], - }, profileDir); + }); + await manuallyInstall(xpi, profileDir, ID2); // Will be disabled in the first version and enabled in the second. - await promiseWriteInstallRDFForExtension({ - id: "addon3@tests.mozilla.org", - version: "1.0", - bootstrap: true, + xpi = createAddon({ + id: ID3, targetApplications: [{ id: "xpcshell@tests.mozilla.org", minVersion: "2", maxVersion: "2", }], - name: "Test Addon 3", - }, profileDir); + }); + await manuallyInstall(xpi, profileDir, ID3); // Will be compatible in both versions but will change version in between - var dest = await promiseWriteInstallRDFForExtension({ - id: "addon4@tests.mozilla.org", + xpi = await createAddon({ + id: ID4, version: "1.0", - bootstrap: true, targetApplications: [{ id: "xpcshell@tests.mozilla.org", minVersion: "1", maxVersion: "1", }], - name: "Test Addon 4", - }, globalDir); - setExtensionModifiedTime(dest, gInstallTime); + }); + await manuallyInstall(xpi, globalDir, ID4); + await promiseSetExtensionModifiedTime(PATH4, gInstallTime); }); registerCleanupFunction(function end_test() { if (!gGlobalExisted) { globalDir.remove(true); } else { - globalDir.append(do_get_expected_addon_name("addon4@tests.mozilla.org")); + globalDir.append(do_get_expected_addon_name(ID4)); globalDir.remove(true); } }); @@ -98,22 +101,20 @@ registerCleanupFunction(function end_test() { add_task(async function test_1() { await promiseStartupManager(); - let [a1, a2, a3, a4] = await AddonManager.getAddonsByIDs(["addon1@tests.mozilla.org", - "addon2@tests.mozilla.org", - "addon3@tests.mozilla.org", - "addon4@tests.mozilla.org"]); - Assert.notEqual(a1, null); - Assert.ok(isExtensionInBootstrappedList(profileDir, a1.id)); + let [a1, a2, a3, a4] = await promiseAddonsByIDs([ID1, ID2, ID3, ID4]); + Assert.notEqual(a1, null, "Found extension 1"); + Assert.equal(a1.isActive, true, "Extension 1 is active"); - Assert.notEqual(a2, null); - Assert.ok(isExtensionInBootstrappedList(profileDir, a2.id)); + Assert.notEqual(a2, null, "Found extension 2"); + Assert.equal(a2.isActive, true, "Extension 2 is active"); - Assert.notEqual(a3, null); - Assert.ok(!isExtensionInBootstrappedList(profileDir, a3.id)); + Assert.notEqual(a3, null, "Found extension 3"); + Assert.equal(a3.isActive, false, "Extension 3 is not active"); Assert.notEqual(a4, null); - Assert.ok(isExtensionInBootstrappedList(globalDir, a4.id)); + Assert.equal(a4.isActive, true); Assert.equal(a4.version, "1.0"); + }); // Test that upgrading the application doesn't disable now incompatible add-ons @@ -121,24 +122,20 @@ add_task(async function test_2() { await promiseShutdownManager(); // Upgrade the extension - var dest = await promiseWriteInstallRDFForExtension({ - id: "addon4@tests.mozilla.org", + let xpi = createAddon({ + id: ID4, version: "2.0", - bootstrap: true, targetApplications: [{ id: "xpcshell@tests.mozilla.org", minVersion: "2", maxVersion: "2", }], - name: "Test Addon 4", - }, globalDir); - setExtensionModifiedTime(dest, gInstallTime); + }); + await manuallyInstall(xpi, globalDir, ID4); + await promiseSetExtensionModifiedTime(PATH4, gInstallTime); await promiseStartupManager("2"); - let [a1, a2, a3, a4] = await AddonManager.getAddonsByIDs(["addon1@tests.mozilla.org", - "addon2@tests.mozilla.org", - "addon3@tests.mozilla.org", - "addon4@tests.mozilla.org"]); + let [a1, a2, a3, a4] = await promiseAddonsByIDs([ID1, ID2, ID3, ID4]); Assert.notEqual(a1, null); Assert.ok(isExtensionInBootstrappedList(profileDir, a1.id)); @@ -158,28 +155,24 @@ add_task(async function test_3() { await promiseShutdownManager(); // Upgrade the extension - var dest = await promiseWriteInstallRDFForExtension({ - id: "addon4@tests.mozilla.org", + let xpi = createAddon({ + id: ID4, version: "3.0", - bootstrap: true, targetApplications: [{ id: "xpcshell@tests.mozilla.org", minVersion: "3", maxVersion: "3", }], - name: "Test Addon 4", - }, globalDir); - setExtensionModifiedTime(dest, gInstallTime); + }); + await manuallyInstall(xpi, globalDir, ID4); + await promiseSetExtensionModifiedTime(PATH4, gInstallTime); - // Simulates a simple Build ID change, the platform deletes extensions.ini - // whenever the application is changed. + // Simulates a simple Build ID change gAddonStartup.remove(true); await promiseStartupManager(); - let [a1, a2, a3, a4] = await AddonManager.getAddonsByIDs(["addon1@tests.mozilla.org", - "addon2@tests.mozilla.org", - "addon3@tests.mozilla.org", - "addon4@tests.mozilla.org"]); + let [a1, a2, a3, a4] = await promiseAddonsByIDs([ID1, ID2, ID3, ID4]); + Assert.notEqual(a1, null); Assert.ok(isExtensionInBootstrappedList(profileDir, a1.id)); @@ -195,3 +188,4 @@ add_task(async function test_3() { await promiseShutdownManager(); }); + diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_webextension_install_syntax_error.js b/toolkit/mozapps/extensions/test/xpcshell/test_webextension_install_syntax_error.js index 369324bf057a..306b35739507 100644 --- a/toolkit/mozapps/extensions/test/xpcshell/test_webextension_install_syntax_error.js +++ b/toolkit/mozapps/extensions/test/xpcshell/test_webextension_install_syntax_error.js @@ -7,11 +7,10 @@ add_task(async function setup() { add_task(async function install_xpi() { - // Data for WebExtension with syntax error + // WebExtension with a JSON syntax error in manifest.json let xpi1 = Extension.generateXPI({ files: { "manifest.json": String.raw`{ - // This is a manifest. Intentional syntax error in next line. "manifest_version: 2, "applications": {"gecko": {"id": "${ADDON_ID}"}}, "name": "Temp WebExt with Error", @@ -20,11 +19,10 @@ add_task(async function install_xpi() { }, }); - // Data for WebExtension without syntax error + // Valid WebExtension let xpi2 = Extension.generateXPI({ files: { "manifest.json": String.raw`{ - // This is a manifest. "manifest_version": 2, "applications": {"gecko": {"id": "${ADDON_ID}"}}, "name": "Temp WebExt without Error", @@ -41,7 +39,7 @@ add_task(async function install_xpi() { xpi2.moveTo(xpi1.parent, xpi1.leafName); let install2 = await AddonManager.getInstallForFile(xpi2); - Assert.notEqual(install2.error, AddonManager.ERROR_CORRUPT_FILE); + Assert.equal(install2.error, 0); xpi1.remove(false); }); diff --git a/toolkit/mozapps/extensions/test/xpcshell/xpcshell.ini b/toolkit/mozapps/extensions/test/xpcshell/xpcshell.ini index 8a6067de0391..39606f4924b4 100644 --- a/toolkit/mozapps/extensions/test/xpcshell/xpcshell.ini +++ b/toolkit/mozapps/extensions/test/xpcshell/xpcshell.ini @@ -63,14 +63,11 @@ tags = blocklist # Times out during parallel runs on desktop requesttimeoutfactor = 2 tags = blocklist -[test_bootstrap.js] -[test_bootstrap_const.js] -[test_bootstrap_globals.js] -[test_bootstrapped_chrome_manifest.js] [test_cache_certdb.js] [test_cacheflush.js] [test_childprocess.js] [test_compatoverrides.js] +head = head_addons.js head_compat.js [test_corrupt.js] [test_crash_annotation_quoting.js] [test_db_path.js] @@ -133,22 +130,16 @@ skip-if = appname != "firefox" [test_install_icons.js] # Bug 676992: test consistently hangs on Android skip-if = os == "android" -[test_invalid_install_rdf.js] [test_isDebuggable.js] [test_isReady.js] [test_json_updatecheck.js] -[test_legacy.js] -skip-if = !allow_legacy_extensions || appname == "thunderbird" [test_locale.js] -[test_manifest.js] -[test_manifest_locales.js] -# Bug 676992: test consistently hangs on Android -skip-if = os == "android" [test_moved_extension_metadata.js] skip-if = true [test_no_addons.js] [test_nodisable_hidden.js] [test_onPropertyChanged_appDisabled.js] +head = head_addons.js head_compat.js [test_overrideblocklist.js] run-sequentially = Uses global XCurProcD dir. tags = blocklist @@ -200,6 +191,7 @@ tags = blocklist # Bug 676992: test consistently fails on Android fail-if = os == "android" [test_strictcompatibility.js] +head = head_addons.js head_compat.js [test_syncGUID.js] [test_system_allowed.js] head = head_addons.js head_system_addons.js @@ -245,9 +237,12 @@ skip-if = os == "win" # Bug 1358846 skip-if = os == "android" [test_updateCancel.js] [test_update_compatmode.js] +head = head_addons.js head_compat.js [test_update_ignorecompat.js] skip-if = true # Bug 676922 Bug 1437697 [test_update_rdf.js] +[test_update_strictcompat.js] +head = head_addons.js head_compat.js [test_update_webextensions.js] tags = webextensions [test_updatecheck.js] @@ -258,6 +253,7 @@ skip-if = os == "android" # Bug 676992: test consistently hangs on Android skip-if = os == "android" [test_upgrade.js] +head = head_addons.js head_compat.js # Bug 676992: test consistently hangs on Android skip-if = os == "android" run-sequentially = Uses global XCurProcD dir. diff --git a/tools/tryselect/selectors/coverage.py b/tools/tryselect/selectors/coverage.py index ae17a38707d3..5b2c1b3a8895 100644 --- a/tools/tryselect/selectors/coverage.py +++ b/tools/tryselect/selectors/coverage.py @@ -74,7 +74,7 @@ def read_test_manifests(): tests = set() for test in test_resolver.resolve_tests(build.topsrcdir): - tests.add(test['file_relpath']) + tests.add(test['srcdir_relpath']) if 'support-files' not in test: continue @@ -91,11 +91,11 @@ def read_test_manifests(): # If it doesn't have a glob, then it's a single file. if '*' not in support_file_pattern: # Simple case: single support file, just add it here. - support_files_map[support_file_pattern].append(test['file_relpath']) + support_files_map[support_file_pattern].append(test['srcdir_relpath']) continue for support_file, _ in file_finder.find(support_file_pattern): - support_files_map[support_file].append(test['file_relpath']) + support_files_map[support_file].append(test['srcdir_relpath']) return tests, support_files_map @@ -330,8 +330,8 @@ def filter_tasks_by_chunks(tasks, chunks): if selected_task is None: print('Warning: no task found for chunk', platform, chunk) - - selected_tasks.add(selected_task) + else: + selected_tasks.add(selected_task) return list(selected_tasks)