зеркало из https://github.com/mozilla/gecko-dev.git
Merge m-c to graphics
MozReview-Commit-ID: CAFMHywf3lQ
This commit is contained in:
Коммит
0839dfda73
|
@ -2,6 +2,9 @@
|
|||
^config/gcc-stl-wrapper.template.h
|
||||
^config/msvc-stl-wrapper.template.h
|
||||
^js/src/jsapi-tests/.*
|
||||
^widget/android/GeneratedJNINatives.h
|
||||
^widget/android/GeneratedJNIWrappers.cpp
|
||||
^widget/android/GeneratedJNIWrappers.h
|
||||
|
||||
# Generated from ./tools/rewriting/ThirdPartyPaths.txt
|
||||
# awk '{print "^"$1".*"}' ./tools/rewriting/ThirdPartyPaths.txt
|
||||
|
|
|
@ -115,6 +115,20 @@ function formatValue (type, data) {
|
|||
}
|
||||
|
||||
var historyObserver = createObserverInstance(HISTORY_EVENTS, HISTORY_ARGS);
|
||||
// Hack alert: we sometimes need to send extra title-changed notifications
|
||||
// ourselves for backwards compat. Replace the original onVisit callback to
|
||||
// add on that logic:
|
||||
historyObserver.realOnVisit = historyObserver.onVisit;
|
||||
historyObserver.onVisit = function(url, visitId, time, sessionId,
|
||||
referringId, transitionType, guid, hidden,
|
||||
visitCount, typed, lastKnownTitle) {
|
||||
// If this is the first visit we're adding, fire title-changed
|
||||
// in case anyone cares.
|
||||
if (visitCount == 1) {
|
||||
historyObserver.onTitleChanged(url, lastKnownTitle);
|
||||
}
|
||||
this.realOnVisit(url, visitId, time, sessionId, referringId, transitionType);
|
||||
};
|
||||
historyService.addObserver(historyObserver, false);
|
||||
|
||||
var bookmarkObserver = createObserverInstance(BOOKMARK_EVENTS, BOOKMARK_ARGS);
|
||||
|
|
|
@ -74,19 +74,6 @@ support-files =
|
|||
web_video1.ogv
|
||||
web_video1.ogv^headers^
|
||||
zoom_test.html
|
||||
file_install_extensions.html
|
||||
browser_legacy.xpi
|
||||
browser_legacy_webext.xpi
|
||||
browser_webext_permissions.xpi
|
||||
browser_webext_nopermissions.xpi
|
||||
browser_webext_update1.xpi
|
||||
browser_webext_update2.xpi
|
||||
browser_webext_update_icon1.xpi
|
||||
browser_webext_update_icon2.xpi
|
||||
browser_webext_update_perms1.xpi
|
||||
browser_webext_update_perms2.xpi
|
||||
browser_webext_update.json
|
||||
browser_webext_search.xml
|
||||
!/image/test/mochitest/blue.png
|
||||
!/toolkit/content/tests/browser/common/mockTransfer.js
|
||||
!/toolkit/modules/tests/browser/metadata_*.html
|
||||
|
@ -256,10 +243,6 @@ skip-if = os == "mac" # decoder doctor isn't implemented on osx
|
|||
[browser_duplicateIDs.js]
|
||||
[browser_drag.js]
|
||||
skip-if = true # browser_drag.js is disabled, as it needs to be updated for the new behavior from bug 320638.
|
||||
[browser_extension_permissions.js]
|
||||
[browser_extension_sideloading.js]
|
||||
[browser_extension_update_background.js]
|
||||
[browser_extension_update_interactive.js]
|
||||
[browser_favicon_change.js]
|
||||
[browser_favicon_change_not_in_document.js]
|
||||
[browser_findbarClose.js]
|
||||
|
@ -289,6 +272,7 @@ subsuite = clipboard
|
|||
[browser_misused_characters_in_strings.js]
|
||||
[browser_modifiedclick_inherit_principal.js]
|
||||
[browser_offlineQuotaNotification.js]
|
||||
skip-if = os == "linux" && !debug # bug 1304273
|
||||
[browser_feed_discovery.js]
|
||||
support-files = feed_discovery.html
|
||||
[browser_gZipOfflineChild.js]
|
||||
|
|
|
@ -1,243 +0,0 @@
|
|||
"use strict";
|
||||
|
||||
// See but 1340586 for proposal to reorganize permissions tests to
|
||||
// get rid of this...
|
||||
requestLongerTimeout(2);
|
||||
|
||||
const BASE = getRootDirectory(gTestPath)
|
||||
.replace("chrome://mochitests/content/", "https://example.com/");
|
||||
|
||||
const INSTALL_PAGE = `${BASE}/file_install_extensions.html`;
|
||||
const PERMS_XPI = "browser_webext_permissions.xpi";
|
||||
const NO_PERMS_XPI = "browser_webext_nopermissions.xpi";
|
||||
const ID = "permissions@test.mozilla.org";
|
||||
|
||||
Services.perms.add(makeURI("https://example.com/"), "install",
|
||||
Services.perms.ALLOW_ACTION);
|
||||
|
||||
registerCleanupFunction(async function() {
|
||||
let addon = await AddonManager.getAddonByID(ID);
|
||||
if (addon) {
|
||||
ok(false, `Addon ${ID} was still installed at the end of the test`);
|
||||
addon.uninstall();
|
||||
}
|
||||
});
|
||||
|
||||
function isDefaultIcon(icon) {
|
||||
// These are basically the same icon, but code within webextensions
|
||||
// generates references to the former and generic add-ons manager code
|
||||
// generates referces to the latter.
|
||||
return (icon == "chrome://browser/content/extension.svg" ||
|
||||
icon == "chrome://mozapps/skin/extensions/extensionGeneric.svg");
|
||||
}
|
||||
|
||||
function promisePopupNotificationShown(name) {
|
||||
return new Promise(resolve => {
|
||||
function popupshown() {
|
||||
let notification = PopupNotifications.getNotification(name);
|
||||
if (!notification) { return; }
|
||||
|
||||
ok(notification, `${name} notification shown`);
|
||||
ok(PopupNotifications.isPanelOpen, "notification panel open");
|
||||
|
||||
PopupNotifications.panel.removeEventListener("popupshown", popupshown);
|
||||
resolve(PopupNotifications.panel.firstChild);
|
||||
}
|
||||
|
||||
PopupNotifications.panel.addEventListener("popupshown", popupshown);
|
||||
});
|
||||
}
|
||||
|
||||
function checkNotification(panel, filename) {
|
||||
let icon = panel.getAttribute("icon");
|
||||
|
||||
let ul = document.getElementById("addon-webext-perm-list");
|
||||
let header = document.getElementById("addon-webext-perm-intro");
|
||||
|
||||
if (filename == PERMS_XPI) {
|
||||
// The icon should come from the extension, don't bother with the precise
|
||||
// path, just make sure we've got a jar url pointing to the right path
|
||||
// inside the jar.
|
||||
ok(icon.startsWith("jar:file://"), "Icon is a jar url");
|
||||
ok(icon.endsWith("/icon.png"), "Icon is icon.png inside a jar");
|
||||
|
||||
is(header.getAttribute("hidden"), "", "Permission list header is visible");
|
||||
is(ul.childElementCount, 5, "Permissions list has 5 entries");
|
||||
// Real checking of the contents here is deferred until bug 1316996 lands
|
||||
} else if (filename == NO_PERMS_XPI) {
|
||||
// This extension has no icon, it should have the default
|
||||
ok(isDefaultIcon(icon), "Icon is the default extension icon");
|
||||
|
||||
is(header.getAttribute("hidden"), "true", "Permission list header is hidden");
|
||||
is(ul.childElementCount, 0, "Permissions list has 0 entries");
|
||||
}
|
||||
}
|
||||
|
||||
// Navigate the current tab to the given url and return a Promise
|
||||
// that resolves when the page is loaded.
|
||||
function load(url) {
|
||||
gBrowser.selectedBrowser.loadURI(INSTALL_PAGE);
|
||||
return BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
|
||||
}
|
||||
|
||||
const INSTALL_FUNCTIONS = [
|
||||
async function installMozAM(filename) {
|
||||
await load(INSTALL_PAGE);
|
||||
|
||||
await ContentTask.spawn(gBrowser.selectedBrowser, `${BASE}/${filename}`, function*(url) {
|
||||
yield content.wrappedJSObject.installMozAM(url);
|
||||
});
|
||||
},
|
||||
|
||||
async function installTrigger(filename) {
|
||||
await load(INSTALL_PAGE);
|
||||
|
||||
ContentTask.spawn(gBrowser.selectedBrowser, `${BASE}/${filename}`, function*(url) {
|
||||
content.wrappedJSObject.installTrigger(url);
|
||||
});
|
||||
},
|
||||
|
||||
async function installFile(filename) {
|
||||
const ChromeRegistry = Cc["@mozilla.org/chrome/chrome-registry;1"]
|
||||
.getService(Ci.nsIChromeRegistry);
|
||||
let chromeUrl = Services.io.newURI(gTestPath);
|
||||
let fileUrl = ChromeRegistry.convertChromeURL(chromeUrl);
|
||||
let file = fileUrl.QueryInterface(Ci.nsIFileURL).file;
|
||||
file.leafName = filename;
|
||||
|
||||
let MockFilePicker = SpecialPowers.MockFilePicker;
|
||||
MockFilePicker.init(window);
|
||||
MockFilePicker.returnFiles = [file];
|
||||
|
||||
await BrowserOpenAddonsMgr("addons://list/extension");
|
||||
let contentWin = gBrowser.selectedTab.linkedBrowser.contentWindow;
|
||||
|
||||
// Do the install...
|
||||
contentWin.gViewController.doCommand("cmd_installFromFile");
|
||||
MockFilePicker.cleanup();
|
||||
},
|
||||
|
||||
async function installSearch(filename) {
|
||||
await SpecialPowers.pushPrefEnv({set: [
|
||||
["extensions.getAddons.maxResults", 10],
|
||||
["extensions.getAddons.search.url", `${BASE}/browser_webext_search.xml`],
|
||||
]});
|
||||
|
||||
let win = await BrowserOpenAddonsMgr("addons://list/extension");
|
||||
|
||||
let searchResultsPromise = new Promise(resolve => {
|
||||
win.document.addEventListener("ViewChanged", resolve, {once: true});
|
||||
});
|
||||
let search = win.document.getElementById("header-search");
|
||||
search.focus();
|
||||
search.value = "search text";
|
||||
EventUtils.synthesizeKey("VK_RETURN", {}, win);
|
||||
|
||||
await searchResultsPromise;
|
||||
ok(win.gViewController.currentViewId.startsWith("addons://search"),
|
||||
"about:addons is displaying search results");
|
||||
|
||||
let list = win.document.getElementById("search-list");
|
||||
let item = null;
|
||||
for (let child of list.childNodes) {
|
||||
if (child.nodeName == "richlistitem" &&
|
||||
child.mAddon.install.sourceURI.path.endsWith(filename)) {
|
||||
item = child;
|
||||
break;
|
||||
}
|
||||
}
|
||||
ok(item, `Found ${filename} in search results`);
|
||||
|
||||
// abracadabara XBL
|
||||
item.clientTop;
|
||||
|
||||
let install = win.document.getAnonymousElementByAttribute(item, "anonid", "install-status");
|
||||
let button = win.document.getAnonymousElementByAttribute(install, "anonid", "install-remote-btn");
|
||||
EventUtils.synthesizeMouseAtCenter(button, {}, win);
|
||||
},
|
||||
];
|
||||
|
||||
add_task(function* () {
|
||||
yield SpecialPowers.pushPrefEnv({set: [
|
||||
["extensions.webapi.testing", true],
|
||||
["extensions.install.requireBuiltInCerts", false],
|
||||
|
||||
// XXX remove this when prompts are enabled by default
|
||||
["extensions.webextPermissionPrompts", true],
|
||||
]});
|
||||
|
||||
function* runOnce(installFn, filename, cancel) {
|
||||
let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser);
|
||||
|
||||
let installPromise = new Promise(resolve => {
|
||||
let listener = {
|
||||
onDownloadCancelled() {
|
||||
AddonManager.removeInstallListener(listener);
|
||||
resolve(false);
|
||||
},
|
||||
|
||||
onDownloadFailed() {
|
||||
AddonManager.removeInstallListener(listener);
|
||||
resolve(false);
|
||||
},
|
||||
|
||||
onInstallCancelled() {
|
||||
AddonManager.removeInstallListener(listener);
|
||||
resolve(false);
|
||||
},
|
||||
|
||||
onInstallEnded() {
|
||||
AddonManager.removeInstallListener(listener);
|
||||
resolve(true);
|
||||
},
|
||||
|
||||
onInstallFailed() {
|
||||
AddonManager.removeInstallListener(listener);
|
||||
resolve(false);
|
||||
},
|
||||
};
|
||||
AddonManager.addInstallListener(listener);
|
||||
});
|
||||
|
||||
let installMethodPromise = installFn(filename);
|
||||
|
||||
let panel = yield promisePopupNotificationShown("addon-webext-permissions");
|
||||
checkNotification(panel, filename);
|
||||
|
||||
if (cancel) {
|
||||
panel.secondaryButton.click();
|
||||
try {
|
||||
yield installMethodPromise;
|
||||
} catch (err) {}
|
||||
} else {
|
||||
// Look for post-install notification
|
||||
let postInstallPromise = promisePopupNotificationShown("addon-installed");
|
||||
panel.button.click();
|
||||
|
||||
// Press OK on the post-install notification
|
||||
panel = yield postInstallPromise;
|
||||
panel.button.click();
|
||||
|
||||
yield installMethodPromise;
|
||||
}
|
||||
|
||||
let result = yield installPromise;
|
||||
let addon = yield AddonManager.getAddonByID(ID);
|
||||
if (cancel) {
|
||||
ok(!result, "Installation was cancelled");
|
||||
is(addon, null, "Extension is not installed");
|
||||
} else {
|
||||
ok(result, "Installation completed");
|
||||
isnot(addon, null, "Extension is installed");
|
||||
addon.uninstall();
|
||||
}
|
||||
|
||||
yield BrowserTestUtils.removeTab(tab);
|
||||
}
|
||||
|
||||
for (let installFn of INSTALL_FUNCTIONS) {
|
||||
yield runOnce(installFn, NO_PERMS_XPI, true);
|
||||
yield runOnce(installFn, PERMS_XPI, true);
|
||||
yield runOnce(installFn, PERMS_XPI, false);
|
||||
}
|
||||
});
|
|
@ -0,0 +1,24 @@
|
|||
[DEFAULT]
|
||||
support-files =
|
||||
head.js
|
||||
file_install_extensions.html
|
||||
browser_legacy.xpi
|
||||
browser_legacy_webext.xpi
|
||||
browser_webext_permissions.xpi
|
||||
browser_webext_nopermissions.xpi
|
||||
browser_webext_update1.xpi
|
||||
browser_webext_update2.xpi
|
||||
browser_webext_update_icon1.xpi
|
||||
browser_webext_update_icon2.xpi
|
||||
browser_webext_update_perms1.xpi
|
||||
browser_webext_update_perms2.xpi
|
||||
browser_webext_update.json
|
||||
browser_webext_search.xml
|
||||
|
||||
[browser_extension_sideloading.js]
|
||||
[browser_extension_update_background.js]
|
||||
[browser_extension_update_interactive.js]
|
||||
[browser_permissions_addons_search.js]
|
||||
[browser_permissions_installTrigger.js]
|
||||
[browser_permissions_local_file.js]
|
||||
[browser_permissions_mozAddonManager.js]
|
|
@ -72,31 +72,14 @@ class MockProvider {
|
|||
}
|
||||
}
|
||||
|
||||
function promisePopupNotificationShown(name) {
|
||||
return new Promise(resolve => {
|
||||
function popupshown() {
|
||||
let notification = PopupNotifications.getNotification(name);
|
||||
if (!notification) {
|
||||
return;
|
||||
}
|
||||
|
||||
ok(notification, `${name} notification shown`);
|
||||
ok(PopupNotifications.isPanelOpen, "notification panel open");
|
||||
|
||||
PopupNotifications.panel.removeEventListener("popupshown", popupshown);
|
||||
resolve(PopupNotifications.panel.firstChild);
|
||||
}
|
||||
|
||||
PopupNotifications.panel.addEventListener("popupshown", popupshown);
|
||||
});
|
||||
}
|
||||
|
||||
function promiseSetDisabled(addon) {
|
||||
return new Promise(resolve => {
|
||||
setCallbacks.set(addon, resolve);
|
||||
});
|
||||
}
|
||||
|
||||
let cleanup;
|
||||
|
||||
add_task(function* () {
|
||||
// XXX remove this when prompts are enabled by default
|
||||
yield SpecialPowers.pushPrefEnv({set: [
|
||||
|
@ -168,14 +151,15 @@ add_task(function* () {
|
|||
flags: AddonManager.TYPE_UI_VIEW_LIST |
|
||||
AddonManager.TYPE_SUPPORTS_UNDO_RESTARTLESS_UNINSTALL,
|
||||
}]);
|
||||
registerCleanupFunction(function*() {
|
||||
|
||||
testCleanup = async function() {
|
||||
AddonManagerPrivate.unregisterProvider(provider);
|
||||
|
||||
// clear out ExtensionsUI state about sideloaded extensions so
|
||||
// subsequent tests don't get confused.
|
||||
ExtensionsUI.sideloaded.clear();
|
||||
ExtensionsUI.emit("change");
|
||||
});
|
||||
};
|
||||
|
||||
// Navigate away from the starting page to force about:addons to load
|
||||
// in a new tab during the tests below.
|
|
@ -1,36 +1,10 @@
|
|||
const {AddonManagerPrivate} = Cu.import("resource://gre/modules/AddonManager.jsm", {});
|
||||
|
||||
const URL_BASE = "https://example.com/browser/browser/base/content/test/general";
|
||||
const ID = "update2@tests.mozilla.org";
|
||||
const ID_ICON = "update_icon2@tests.mozilla.org";
|
||||
const ID_PERMS = "update_perms@tests.mozilla.org";
|
||||
const ID_LEGACY = "legacy_update@tests.mozilla.org";
|
||||
|
||||
registerCleanupFunction(async function() {
|
||||
for (let id of [ID, ID_ICON, ID_PERMS, ID_LEGACY]) {
|
||||
let addon = await AddonManager.getAddonByID(id);
|
||||
if (addon) {
|
||||
ok(false, `Addon ${id} was still installed at the end of the test`);
|
||||
addon.uninstall();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
function promiseInstallAddon(url) {
|
||||
return AddonManager.getInstallForURL(url, null, "application/x-xpinstall")
|
||||
.then(install => {
|
||||
ok(install, "Created install");
|
||||
return new Promise(resolve => {
|
||||
install.addListener({
|
||||
onInstallEnded(_install, addon) {
|
||||
resolve(addon);
|
||||
},
|
||||
});
|
||||
install.install();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function promiseViewLoaded(tab, viewid) {
|
||||
let win = tab.linkedBrowser.contentWindow;
|
||||
if (win.gViewController && !win.gViewController.isLoading &&
|
||||
|
@ -50,41 +24,11 @@ function promiseViewLoaded(tab, viewid) {
|
|||
});
|
||||
}
|
||||
|
||||
function promisePopupNotificationShown(name) {
|
||||
return new Promise(resolve => {
|
||||
function popupshown() {
|
||||
let notification = PopupNotifications.getNotification(name);
|
||||
if (!notification) { return; }
|
||||
|
||||
ok(notification, `${name} notification shown`);
|
||||
ok(PopupNotifications.isPanelOpen, "notification panel open");
|
||||
|
||||
PopupNotifications.panel.removeEventListener("popupshown", popupshown);
|
||||
resolve(PopupNotifications.panel.firstChild);
|
||||
}
|
||||
|
||||
PopupNotifications.panel.addEventListener("popupshown", popupshown);
|
||||
});
|
||||
}
|
||||
|
||||
function getBadgeStatus() {
|
||||
let menuButton = document.getElementById("PanelUI-menu-button");
|
||||
return menuButton.getAttribute("badge-status");
|
||||
}
|
||||
|
||||
function promiseInstallEvent(addon, event) {
|
||||
return new Promise(resolve => {
|
||||
let listener = {};
|
||||
listener[event] = (install, ...args) => {
|
||||
if (install.addon.id == addon.id) {
|
||||
AddonManager.removeInstallListener(listener);
|
||||
resolve(...args);
|
||||
}
|
||||
};
|
||||
AddonManager.addInstallListener(listener);
|
||||
});
|
||||
}
|
||||
|
||||
// Set some prefs that apply to all the tests in this file
|
||||
add_task(function* setup() {
|
||||
yield SpecialPowers.pushPrefEnv({set: [
|
||||
|
@ -115,7 +59,7 @@ function* backgroundUpdateTest(url, id, checkIconFn) {
|
|||
["extensions.update.enabled", true],
|
||||
|
||||
// Point updates to the local mochitest server
|
||||
["extensions.update.background.url", `${URL_BASE}/browser_webext_update.json`],
|
||||
["extensions.update.background.url", `${BASE}/browser_webext_update.json`],
|
||||
]});
|
||||
|
||||
// Install version 1.0 of the test extension
|
||||
|
@ -228,7 +172,7 @@ function checkDefaultIcon(icon) {
|
|||
"Popup has the default extension icon");
|
||||
}
|
||||
|
||||
add_task(() => backgroundUpdateTest(`${URL_BASE}/browser_webext_update1.xpi`,
|
||||
add_task(() => backgroundUpdateTest(`${BASE}/browser_webext_update1.xpi`,
|
||||
ID, checkDefaultIcon));
|
||||
|
||||
function checkNonDefaultIcon(icon) {
|
||||
|
@ -239,7 +183,7 @@ function checkNonDefaultIcon(icon) {
|
|||
ok(icon.endsWith("/icon.png"), "Icon is icon.png inside a jar");
|
||||
}
|
||||
|
||||
add_task(() => backgroundUpdateTest(`${URL_BASE}/browser_webext_update_icon1.xpi`,
|
||||
add_task(() => backgroundUpdateTest(`${BASE}/browser_webext_update_icon1.xpi`,
|
||||
ID_ICON, checkNonDefaultIcon));
|
||||
|
||||
// Helper function to test an upgrade that should not show a prompt
|
||||
|
@ -249,7 +193,7 @@ async function testNoPrompt(origUrl, id) {
|
|||
["extensions.update.enabled", true],
|
||||
|
||||
// Point updates to the local mochitest server
|
||||
["extensions.update.background.url", `${URL_BASE}/browser_webext_update.json`],
|
||||
["extensions.update.background.url", `${BASE}/browser_webext_update.json`],
|
||||
]});
|
||||
|
||||
// Install version 1.0 of the test extension
|
||||
|
@ -286,11 +230,11 @@ async function testNoPrompt(origUrl, id) {
|
|||
|
||||
// Test that an update that adds new non-promptable permissions is just
|
||||
// applied without showing a notification dialog.
|
||||
add_task(() => testNoPrompt(`${URL_BASE}/browser_webext_update_perms1.xpi`,
|
||||
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(`${URL_BASE}/browser_legacy.xpi`, ID_LEGACY));
|
||||
add_task(() => testNoPrompt(`${BASE}/browser_legacy.xpi`, ID_LEGACY));
|
||||
|
|
@ -1,83 +1,8 @@
|
|||
const {AddonManagerPrivate} = Cu.import("resource://gre/modules/AddonManager.jsm", {});
|
||||
|
||||
const URL_BASE = "https://example.com/browser/browser/base/content/test/general";
|
||||
const ID = "update2@tests.mozilla.org";
|
||||
const ID_LEGACY = "legacy_update@tests.mozilla.org";
|
||||
|
||||
registerCleanupFunction(async function() {
|
||||
for (let id of [ID, ID_LEGACY]) {
|
||||
let addon = await AddonManager.getAddonByID(id);
|
||||
if (addon) {
|
||||
ok(false, `Addon ${id} was still installed at the end of the test`);
|
||||
addon.uninstall();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
function promiseInstallAddon(url) {
|
||||
return AddonManager.getInstallForURL(url, null, "application/x-xpinstall")
|
||||
.then(install => {
|
||||
ok(install, "Created install");
|
||||
return new Promise(resolve => {
|
||||
install.addListener({
|
||||
onInstallEnded(_install, addon) {
|
||||
resolve(addon);
|
||||
},
|
||||
});
|
||||
install.install();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function promiseViewLoaded(tab, viewid) {
|
||||
let win = tab.linkedBrowser.contentWindow;
|
||||
if (win.gViewController && !win.gViewController.isLoading &&
|
||||
win.gViewController.currentViewId == viewid) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
return new Promise(resolve => {
|
||||
function listener() {
|
||||
if (win.gViewController.currentViewId != viewid) {
|
||||
return;
|
||||
}
|
||||
win.document.removeEventListener("ViewChanged", listener);
|
||||
resolve();
|
||||
}
|
||||
win.document.addEventListener("ViewChanged", listener);
|
||||
});
|
||||
}
|
||||
|
||||
function promisePopupNotificationShown(name) {
|
||||
return new Promise(resolve => {
|
||||
function popupshown() {
|
||||
let notification = PopupNotifications.getNotification(name);
|
||||
if (!notification) { return; }
|
||||
|
||||
ok(notification, `${name} notification shown`);
|
||||
ok(PopupNotifications.isPanelOpen, "notification panel open");
|
||||
|
||||
PopupNotifications.panel.removeEventListener("popupshown", popupshown);
|
||||
resolve(PopupNotifications.panel.firstChild);
|
||||
}
|
||||
|
||||
PopupNotifications.panel.addEventListener("popupshown", popupshown);
|
||||
});
|
||||
}
|
||||
|
||||
function promiseInstallEvent(addon, event) {
|
||||
return new Promise(resolve => {
|
||||
let listener = {};
|
||||
listener[event] = (install, ...args) => {
|
||||
if (install.addon.id == addon.id) {
|
||||
AddonManager.removeInstallListener(listener);
|
||||
resolve(...args);
|
||||
}
|
||||
};
|
||||
AddonManager.addInstallListener(listener);
|
||||
});
|
||||
}
|
||||
|
||||
// Set some prefs that apply to all the tests in this file
|
||||
add_task(function* setup() {
|
||||
yield SpecialPowers.pushPrefEnv({set: [
|
||||
|
@ -99,7 +24,7 @@ function* interactiveUpdateTest(autoUpdate, checkFn) {
|
|||
["extensions.update.autoUpdateDefault", autoUpdate],
|
||||
|
||||
// Point updates to the local mochitest server
|
||||
["extensions.update.url", `${URL_BASE}/browser_webext_update.json`],
|
||||
["extensions.update.url", `${BASE}/browser_webext_update.json`],
|
||||
]});
|
||||
|
||||
// Trigger an update check, manually applying the update if we're testing
|
||||
|
@ -133,29 +58,17 @@ function* interactiveUpdateTest(autoUpdate, checkFn) {
|
|||
}
|
||||
}
|
||||
|
||||
// Navigate away from the starting page to force about:addons to load
|
||||
// in a new tab during the tests below.
|
||||
gBrowser.selectedBrowser.loadURI("about:robots");
|
||||
yield BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
|
||||
|
||||
// Install version 1.0 of the test extension
|
||||
let addon = yield promiseInstallAddon(`${URL_BASE}/browser_webext_update1.xpi`);
|
||||
let addon = yield promiseInstallAddon(`${BASE}/browser_webext_update1.xpi`);
|
||||
ok(addon, "Addon was installed");
|
||||
is(addon.version, "1.0", "Version 1 of the addon is installed");
|
||||
|
||||
// Open add-ons manager and navigate to extensions list
|
||||
let loadPromise = new Promise(resolve => {
|
||||
let listener = (subject, topic) => {
|
||||
if (subject.location.href == "about:addons") {
|
||||
Services.obs.removeObserver(listener, topic);
|
||||
resolve(subject);
|
||||
}
|
||||
};
|
||||
Services.obs.addObserver(listener, "EM-loaded", false);
|
||||
});
|
||||
let tab = gBrowser.addTab("about:addons");
|
||||
gBrowser.selectedTab = tab;
|
||||
let win = yield loadPromise;
|
||||
|
||||
const VIEW = "addons://list/extension";
|
||||
let viewPromise = promiseViewLoaded(tab, VIEW);
|
||||
win.loadView(VIEW);
|
||||
yield viewPromise;
|
||||
let win = yield BrowserOpenAddonsMgr("addons://list/extension");
|
||||
|
||||
// Trigger an update check
|
||||
let popupPromise = promisePopupNotificationShown("addon-webext-permissions");
|
||||
|
@ -182,7 +95,7 @@ function* interactiveUpdateTest(autoUpdate, checkFn) {
|
|||
addon = yield updatePromise;
|
||||
is(addon.version, "2.0", "Should have upgraded");
|
||||
|
||||
yield BrowserTestUtils.removeTab(tab);
|
||||
yield BrowserTestUtils.removeTab(gBrowser.selectedTab);
|
||||
addon.uninstall();
|
||||
yield SpecialPowers.popPrefEnv();
|
||||
}
|
||||
|
@ -211,7 +124,7 @@ add_task(() => interactiveUpdateTest(false, checkOne));
|
|||
add_task(async function() {
|
||||
await SpecialPowers.pushPrefEnv({set: [
|
||||
// Point updates to the local mochitest server
|
||||
["extensions.update.url", `${URL_BASE}/browser_webext_update.json`],
|
||||
["extensions.update.url", `${BASE}/browser_webext_update.json`],
|
||||
]});
|
||||
|
||||
// Navigate away to ensure that BrowserOpenAddonMgr() opens a new tab
|
||||
|
@ -219,7 +132,7 @@ add_task(async function() {
|
|||
await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
|
||||
|
||||
// Install initial version of the test extension
|
||||
let addon = await promiseInstallAddon(`${URL_BASE}/browser_legacy.xpi`);
|
||||
let addon = await promiseInstallAddon(`${BASE}/browser_legacy.xpi`);
|
||||
ok(addon, "Addon was installed");
|
||||
is(addon.version, "1.1", "Version 1 of the addon is installed");
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
"use strict";
|
||||
|
||||
async function installSearch(filename) {
|
||||
await SpecialPowers.pushPrefEnv({set: [
|
||||
["extensions.getAddons.maxResults", 10],
|
||||
["extensions.getAddons.search.url", `${BASE}/browser_webext_search.xml`],
|
||||
]});
|
||||
|
||||
let win = await BrowserOpenAddonsMgr("addons://list/extension");
|
||||
|
||||
let searchResultsPromise = new Promise(resolve => {
|
||||
win.document.addEventListener("ViewChanged", resolve, {once: true});
|
||||
});
|
||||
let search = win.document.getElementById("header-search");
|
||||
search.focus();
|
||||
search.value = "search text";
|
||||
EventUtils.synthesizeKey("VK_RETURN", {}, win);
|
||||
|
||||
await searchResultsPromise;
|
||||
ok(win.gViewController.currentViewId.startsWith("addons://search"),
|
||||
"about:addons is displaying search results");
|
||||
|
||||
let list = win.document.getElementById("search-list");
|
||||
let item = null;
|
||||
for (let child of list.childNodes) {
|
||||
if (child.nodeName == "richlistitem" &&
|
||||
child.mAddon.install.sourceURI.path.endsWith(filename)) {
|
||||
item = child;
|
||||
break;
|
||||
}
|
||||
}
|
||||
ok(item, `Found ${filename} in search results`);
|
||||
|
||||
// abracadabara XBL
|
||||
item.clientTop;
|
||||
|
||||
let install = win.document.getAnonymousElementByAttribute(item, "anonid", "install-status");
|
||||
let button = win.document.getAnonymousElementByAttribute(install, "anonid", "install-remote-btn");
|
||||
EventUtils.synthesizeMouseAtCenter(button, {}, win);
|
||||
}
|
||||
|
||||
add_task(() => testInstallMethod(installSearch));
|
|
@ -0,0 +1,14 @@
|
|||
"use strict";
|
||||
|
||||
const INSTALL_PAGE = `${BASE}/file_install_extensions.html`;
|
||||
|
||||
async function installTrigger(filename) {
|
||||
gBrowser.selectedBrowser.loadURI(INSTALL_PAGE);
|
||||
await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
|
||||
|
||||
ContentTask.spawn(gBrowser.selectedBrowser, `${BASE}/${filename}`, function*(url) {
|
||||
content.wrappedJSObject.installTrigger(url);
|
||||
});
|
||||
}
|
||||
|
||||
add_task(() => testInstallMethod(installTrigger));
|
|
@ -0,0 +1,23 @@
|
|||
"use strict";
|
||||
|
||||
async function installFile(filename) {
|
||||
const ChromeRegistry = Cc["@mozilla.org/chrome/chrome-registry;1"]
|
||||
.getService(Ci.nsIChromeRegistry);
|
||||
let chromeUrl = Services.io.newURI(gTestPath);
|
||||
let fileUrl = ChromeRegistry.convertChromeURL(chromeUrl);
|
||||
let file = fileUrl.QueryInterface(Ci.nsIFileURL).file;
|
||||
file.leafName = filename;
|
||||
|
||||
let MockFilePicker = SpecialPowers.MockFilePicker;
|
||||
MockFilePicker.init(window);
|
||||
MockFilePicker.returnFiles = [file];
|
||||
|
||||
await BrowserOpenAddonsMgr("addons://list/extension");
|
||||
let contentWin = gBrowser.selectedTab.linkedBrowser.contentWindow;
|
||||
|
||||
// Do the install...
|
||||
contentWin.gViewController.doCommand("cmd_installFromFile");
|
||||
MockFilePicker.cleanup();
|
||||
}
|
||||
|
||||
add_task(() => testInstallMethod(installFile));
|
|
@ -0,0 +1,14 @@
|
|||
"use strict";
|
||||
|
||||
const INSTALL_PAGE = `${BASE}/file_install_extensions.html`;
|
||||
|
||||
async function installMozAM(filename) {
|
||||
gBrowser.selectedBrowser.loadURI(INSTALL_PAGE);
|
||||
await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
|
||||
|
||||
await ContentTask.spawn(gBrowser.selectedBrowser, `${BASE}/${filename}`, function*(url) {
|
||||
yield content.wrappedJSObject.installMozAM(url);
|
||||
});
|
||||
}
|
||||
|
||||
add_task(() => testInstallMethod(installMozAM));
|
|
@ -21,7 +21,7 @@
|
|||
</application>
|
||||
</compatible_applications>
|
||||
<compatible_os>ALL</compatible_os>
|
||||
<install size="1">https://example.com/browser/browser/base/content/test/general/browser_webext_permissions.xpi</install>
|
||||
<install size="1">https://example.com/browser/browser/base/content/test/webextensions/browser_webext_permissions.xpi</install>
|
||||
</addon>
|
||||
|
||||
<addon>
|
||||
|
@ -45,7 +45,7 @@
|
|||
</application>
|
||||
</compatible_applications>
|
||||
<compatible_os>ALL</compatible_os>
|
||||
<install size="1">https://example.com/browser/browser/base/content/test/general/browser_webext_nopermissions.xpi</install>
|
||||
<install size="1">https://example.com/browser/browser/base/content/test/webextensions/browser_webext_nopermissions.xpi</install>
|
||||
</addon>
|
||||
</searchresults>
|
||||
|
|
@ -4,7 +4,7 @@
|
|||
"updates": [
|
||||
{
|
||||
"version": "2.0",
|
||||
"update_link": "https://example.com/browser/browser/base/content/test/general/browser_webext_update2.xpi",
|
||||
"update_link": "https://example.com/browser/browser/base/content/test/webextensions/browser_webext_update2.xpi",
|
||||
"applications": {
|
||||
"gecko": {
|
||||
"strict_min_version": "1",
|
||||
|
@ -18,7 +18,7 @@
|
|||
"updates": [
|
||||
{
|
||||
"version": "2.0",
|
||||
"update_link": "https://example.com/browser/browser/base/content/test/general/browser_webext_update_icon2.xpi",
|
||||
"update_link": "https://example.com/browser/browser/base/content/test/webextensions/browser_webext_update_icon2.xpi",
|
||||
"applications": {
|
||||
"gecko": {
|
||||
"strict_min_version": "1",
|
||||
|
@ -32,7 +32,7 @@
|
|||
"updates": [
|
||||
{
|
||||
"version": "2.0",
|
||||
"update_link": "https://example.com/browser/browser/base/content/test/general/browser_webext_update_perms2.xpi",
|
||||
"update_link": "https://example.com/browser/browser/base/content/test/webextensions/browser_webext_update_perms2.xpi",
|
||||
"applications": {
|
||||
"gecko": {
|
||||
"strict_min_version": "1",
|
||||
|
@ -46,7 +46,7 @@
|
|||
"updates": [
|
||||
{
|
||||
"version": "2.0",
|
||||
"update_link": "https://example.com/browser/browser/base/content/test/general/browser_legacy_webext.xpi",
|
||||
"update_link": "https://example.com/browser/browser/base/content/test/webextensions/browser_legacy_webext.xpi",
|
||||
"applications": {
|
||||
"gecko": {
|
||||
"strict_min_version": "1",
|
|
@ -0,0 +1,282 @@
|
|||
|
||||
const BASE = getRootDirectory(gTestPath)
|
||||
.replace("chrome://mochitests/content/", "https://example.com/");
|
||||
|
||||
/**
|
||||
* Wait for the given PopupNotification to display
|
||||
*
|
||||
* @param {string} name
|
||||
* The name of the notification to wait for.
|
||||
*
|
||||
* @returns {Promise}
|
||||
* Resolves with the notification window.
|
||||
*/
|
||||
function promisePopupNotificationShown(name) {
|
||||
return new Promise(resolve => {
|
||||
function popupshown() {
|
||||
let notification = PopupNotifications.getNotification(name);
|
||||
if (!notification) { return; }
|
||||
|
||||
ok(notification, `${name} notification shown`);
|
||||
ok(PopupNotifications.isPanelOpen, "notification panel open");
|
||||
|
||||
PopupNotifications.panel.removeEventListener("popupshown", popupshown);
|
||||
resolve(PopupNotifications.panel.firstChild);
|
||||
}
|
||||
|
||||
PopupNotifications.panel.addEventListener("popupshown", popupshown);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait for a specific install event to fire for a given addon
|
||||
*
|
||||
* @param {AddonWrapper} addon
|
||||
* The addon to watch for an event on
|
||||
* @param {string}
|
||||
* The name of the event to watch for (e.g., onInstallEnded)
|
||||
*
|
||||
* @returns {Promise}
|
||||
* Resolves when the event triggers with the first argument
|
||||
* to the event handler as the resolution value.
|
||||
*/
|
||||
function promiseInstallEvent(addon, event) {
|
||||
return new Promise(resolve => {
|
||||
let listener = {};
|
||||
listener[event] = (install, arg) => {
|
||||
if (install.addon.id == addon.id) {
|
||||
AddonManager.removeInstallListener(listener);
|
||||
resolve(arg);
|
||||
}
|
||||
};
|
||||
AddonManager.addInstallListener(listener);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Install an (xpi packaged) extension
|
||||
*
|
||||
* @param {string} url
|
||||
* URL of the .xpi file to install
|
||||
*
|
||||
* @returns {Promise}
|
||||
* Resolves when the extension has been installed with the Addon
|
||||
* object as the resolution value.
|
||||
*/
|
||||
function promiseInstallAddon(url) {
|
||||
return AddonManager.getInstallForURL(url, null, "application/x-xpinstall")
|
||||
.then(install => {
|
||||
ok(install, "Created install");
|
||||
return new Promise(resolve => {
|
||||
install.addListener({
|
||||
onInstallEnded(_install, addon) {
|
||||
resolve(addon);
|
||||
},
|
||||
});
|
||||
install.install();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function isDefaultIcon(icon) {
|
||||
// These are basically the same icon, but code within webextensions
|
||||
// generates references to the former and generic add-ons manager code
|
||||
// generates referces to the latter.
|
||||
return (icon == "chrome://browser/content/extension.svg" ||
|
||||
icon == "chrome://mozapps/skin/extensions/extensionGeneric.svg");
|
||||
}
|
||||
|
||||
function is_hidden(element) {
|
||||
var style = element.ownerGlobal.getComputedStyle(element);
|
||||
if (style.display == "none")
|
||||
return true;
|
||||
if (style.visibility != "visible")
|
||||
return true;
|
||||
if (style.display == "-moz-popup")
|
||||
return ["hiding", "closed"].indexOf(element.state) != -1;
|
||||
|
||||
// Hiding a parent element will hide all its children
|
||||
if (element.parentNode != element.ownerDocument)
|
||||
return is_hidden(element.parentNode);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function is_visible(element) {
|
||||
var style = element.ownerGlobal.getComputedStyle(element);
|
||||
if (style.display == "none")
|
||||
return false;
|
||||
if (style.visibility != "visible")
|
||||
return false;
|
||||
if (style.display == "-moz-popup" && element.state != "open")
|
||||
return false;
|
||||
|
||||
// Hiding a parent element will hide all its children
|
||||
if (element.parentNode != element.ownerDocument)
|
||||
return is_visible(element.parentNode);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that install-time permission prompts work for a given
|
||||
* installation method.
|
||||
*
|
||||
* @param {Function} installFn
|
||||
* Callable that takes the name of an xpi file to install and
|
||||
* starts to install it. Should return a Promise that resolves
|
||||
* when the install is finished or rejects if the install is canceled.
|
||||
*
|
||||
* @returns {Promise}
|
||||
*/
|
||||
async function testInstallMethod(installFn) {
|
||||
const PERMS_XPI = "browser_webext_permissions.xpi";
|
||||
const NO_PERMS_XPI = "browser_webext_nopermissions.xpi";
|
||||
const ID = "permissions@test.mozilla.org";
|
||||
|
||||
await SpecialPowers.pushPrefEnv({set: [
|
||||
["extensions.webapi.testing", true],
|
||||
["extensions.install.requireBuiltInCerts", false],
|
||||
|
||||
// XXX remove this when prompts are enabled by default
|
||||
["extensions.webextPermissionPrompts", true],
|
||||
]});
|
||||
|
||||
let testURI = makeURI("https://example.com/");
|
||||
Services.perms.add(testURI, "install", Services.perms.ALLOW_ACTION);
|
||||
registerCleanupFunction(() => Services.perms.remove(testURI, "install"));
|
||||
|
||||
async function runOnce(filename, cancel) {
|
||||
let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser);
|
||||
|
||||
let installPromise = new Promise(resolve => {
|
||||
let listener = {
|
||||
onDownloadCancelled() {
|
||||
AddonManager.removeInstallListener(listener);
|
||||
resolve(false);
|
||||
},
|
||||
|
||||
onDownloadFailed() {
|
||||
AddonManager.removeInstallListener(listener);
|
||||
resolve(false);
|
||||
},
|
||||
|
||||
onInstallCancelled() {
|
||||
AddonManager.removeInstallListener(listener);
|
||||
resolve(false);
|
||||
},
|
||||
|
||||
onInstallEnded() {
|
||||
AddonManager.removeInstallListener(listener);
|
||||
resolve(true);
|
||||
},
|
||||
|
||||
onInstallFailed() {
|
||||
AddonManager.removeInstallListener(listener);
|
||||
resolve(false);
|
||||
},
|
||||
};
|
||||
AddonManager.addInstallListener(listener);
|
||||
});
|
||||
|
||||
let installMethodPromise = installFn(filename);
|
||||
|
||||
let panel = await promisePopupNotificationShown("addon-webext-permissions");
|
||||
let icon = panel.getAttribute("icon");
|
||||
|
||||
let ul = document.getElementById("addon-webext-perm-list");
|
||||
let header = document.getElementById("addon-webext-perm-intro");
|
||||
|
||||
if (filename == PERMS_XPI) {
|
||||
// The icon should come from the extension, don't bother with the precise
|
||||
// path, just make sure we've got a jar url pointing to the right path
|
||||
// inside the jar.
|
||||
ok(icon.startsWith("jar:file://"), "Icon is a jar url");
|
||||
ok(icon.endsWith("/icon.png"), "Icon is icon.png inside a jar");
|
||||
|
||||
is(header.getAttribute("hidden"), "", "Permission list header is visible");
|
||||
is(ul.childElementCount, 5, "Permissions list has 5 entries");
|
||||
// Real checking of the contents here is deferred until bug 1316996 lands
|
||||
} else if (filename == NO_PERMS_XPI) {
|
||||
// This extension has no icon, it should have the default
|
||||
ok(isDefaultIcon(icon), "Icon is the default extension icon");
|
||||
|
||||
is(header.getAttribute("hidden"), "true", "Permission list header is hidden");
|
||||
is(ul.childElementCount, 0, "Permissions list has 0 entries");
|
||||
}
|
||||
|
||||
if (cancel) {
|
||||
panel.secondaryButton.click();
|
||||
try {
|
||||
await installMethodPromise;
|
||||
} catch (err) {}
|
||||
} else {
|
||||
// Look for post-install notification
|
||||
let postInstallPromise = promisePopupNotificationShown("addon-installed");
|
||||
panel.button.click();
|
||||
|
||||
// Press OK on the post-install notification
|
||||
panel = await postInstallPromise;
|
||||
panel.button.click();
|
||||
|
||||
await installMethodPromise;
|
||||
}
|
||||
|
||||
let result = await installPromise;
|
||||
let addon = await AddonManager.getAddonByID(ID);
|
||||
if (cancel) {
|
||||
ok(!result, "Installation was cancelled");
|
||||
is(addon, null, "Extension is not installed");
|
||||
} else {
|
||||
ok(result, "Installation completed");
|
||||
isnot(addon, null, "Extension is installed");
|
||||
addon.uninstall();
|
||||
}
|
||||
|
||||
await BrowserTestUtils.removeTab(tab);
|
||||
}
|
||||
|
||||
// A few different tests for each installation method:
|
||||
// 1. Start installation of an extension that requests no permissions,
|
||||
// verify the notification contents, then cancel the install
|
||||
await runOnce(NO_PERMS_XPI, true);
|
||||
|
||||
// 2. Same as #1 but with an extension that requests some permissions.
|
||||
await runOnce(PERMS_XPI, true);
|
||||
|
||||
// 3. Repeat with the same extension from step 2 but this time,
|
||||
// accept the permissions to install the extension. (Then uninstall
|
||||
// the extension to clean up.)
|
||||
await runOnce(PERMS_XPI, false);
|
||||
|
||||
await SpecialPowers.popPrefEnv();
|
||||
}
|
||||
|
||||
// The tests in this directory install a bunch of extensions but they
|
||||
// need to uninstall them before exiting, as a stray leftover extension
|
||||
// after one test can foul up subsequent tests.
|
||||
// So, add a task to run before any tests that grabs a list of all the
|
||||
// add-ons that are pre-installed in the test environment and then checks
|
||||
// the list of installed add-ons at the end of the test to make sure no
|
||||
// new add-ons have been added.
|
||||
// Individual tests can store a cleanup function in the testCleanup global
|
||||
// to ensure it gets called before the final check is performed.
|
||||
let testCleanup;
|
||||
add_task(async function() {
|
||||
let addons = await AddonManager.getAllAddons();
|
||||
let existingAddons = new Set(addons.map(a => a.id));
|
||||
|
||||
registerCleanupFunction(async function() {
|
||||
if (testCleanup) {
|
||||
await testCleanup();
|
||||
testCleanup = null;
|
||||
}
|
||||
|
||||
for (let addon of await AddonManager.getAllAddons()) {
|
||||
if (!existingAddons.has(addon.id)) {
|
||||
ok(false, `Addon ${addon.id} was left installed at the end of the test`);
|
||||
addon.uninstall();
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
|
@ -28,6 +28,7 @@ BROWSER_CHROME_MANIFESTS += [
|
|||
'content/test/tabPrompts/browser.ini',
|
||||
'content/test/tabs/browser.ini',
|
||||
'content/test/urlbar/browser.ini',
|
||||
'content/test/webextensions/browser.ini',
|
||||
'content/test/webrtc/browser.ini',
|
||||
]
|
||||
|
||||
|
|
|
@ -362,14 +362,10 @@ function GetHistoryResource(aProfileFolder) {
|
|||
if (places.length > 0) {
|
||||
yield new Promise((resolve, reject) => {
|
||||
MigrationUtils.insertVisitsWrapper(places, {
|
||||
_success: false,
|
||||
handleResult() {
|
||||
// Importing any entry is considered a successful import.
|
||||
this._success = true;
|
||||
},
|
||||
handleError() {},
|
||||
handleCompletion() {
|
||||
if (this._success) {
|
||||
ignoreErrors: true,
|
||||
ignoreResults: true,
|
||||
handleCompletion(updatedCount) {
|
||||
if (updatedCount > 0) {
|
||||
resolve();
|
||||
} else {
|
||||
reject(new Error("Couldn't add visits"));
|
||||
|
|
|
@ -139,14 +139,10 @@ EdgeTypedURLMigrator.prototype = {
|
|||
}
|
||||
|
||||
MigrationUtils.insertVisitsWrapper(places, {
|
||||
_success: false,
|
||||
handleResult() {
|
||||
// Importing any entry is considered a successful import.
|
||||
this._success = true;
|
||||
},
|
||||
handleError() {},
|
||||
handleCompletion() {
|
||||
aCallback(this._success);
|
||||
ignoreErrors: true,
|
||||
ignoreResults: true,
|
||||
handleCompletion(updatedCount) {
|
||||
aCallback(updatedCount > 0);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
|
|
@ -91,14 +91,10 @@ History.prototype = {
|
|||
}
|
||||
|
||||
MigrationUtils.insertVisitsWrapper(places, {
|
||||
_success: false,
|
||||
handleResult() {
|
||||
// Importing any entry is considered a successful import.
|
||||
this._success = true;
|
||||
},
|
||||
handleError() {},
|
||||
handleCompletion() {
|
||||
aCallback(this._success);
|
||||
ignoreErrors: true,
|
||||
ignoreResults: true,
|
||||
handleCompletion(updatedCount) {
|
||||
aCallback(updatedCount > 0);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -996,7 +996,7 @@ this.MigrationUtils = Object.freeze({
|
|||
if (gKeepUndoData) {
|
||||
this._updateHistoryUndo(places);
|
||||
}
|
||||
return PlacesUtils.asyncHistory.updatePlaces(places, options);
|
||||
return PlacesUtils.asyncHistory.updatePlaces(places, options, true);
|
||||
},
|
||||
|
||||
insertLoginWrapper(login) {
|
||||
|
@ -1054,8 +1054,14 @@ this.MigrationUtils = Object.freeze({
|
|||
let visitMap = new Map(visits.map(v => [v.url, v]));
|
||||
for (let place of places) {
|
||||
let visitCount = place.visits.length;
|
||||
let first = Math.min.apply(Math, place.visits.map(v => v.visitDate));
|
||||
let last = Math.max.apply(Math, place.visits.map(v => v.visitDate));
|
||||
let first, last;
|
||||
if (visitCount > 1) {
|
||||
let visitDates = place.visits.map(v => v.visitDate);
|
||||
first = Math.min.apply(Math, visitDates);
|
||||
last = Math.max.apply(Math, visitDates);
|
||||
} else {
|
||||
first = last = place.visits[0].visitDate;
|
||||
}
|
||||
let url = place.uri.spec;
|
||||
try {
|
||||
new URL(url);
|
||||
|
|
|
@ -227,14 +227,10 @@ History.prototype = {
|
|||
}
|
||||
if (places.length > 0) {
|
||||
MigrationUtils.insertVisitsWrapper(places, {
|
||||
_success: false,
|
||||
handleResult() {
|
||||
// Importing any entry is considered a successful import.
|
||||
this._success = true;
|
||||
},
|
||||
handleError() {},
|
||||
handleCompletion() {
|
||||
aCallback(this._success);
|
||||
ignoreErrors: true,
|
||||
ignoreResults: true,
|
||||
handleCompletion(updatedCount) {
|
||||
aCallback(updatedCount > 0);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
|
|
|
@ -250,13 +250,10 @@ add_task(function* checkUndoRemoval() {
|
|||
let now_uSec = Date.now() * 1000;
|
||||
let visitedURI = Services.io.newURI("http://www.example.com/");
|
||||
let frecencyUpdatePromise = new Promise(resolve => {
|
||||
let expectedChanges = 2;
|
||||
let observer = {
|
||||
onFrecencyChanged() {
|
||||
if (!--expectedChanges) {
|
||||
onManyFrecenciesChanged() {
|
||||
PlacesUtils.history.removeObserver(observer);
|
||||
resolve();
|
||||
}
|
||||
},
|
||||
};
|
||||
PlacesUtils.history.addObserver(observer, false);
|
||||
|
|
|
@ -52,6 +52,24 @@ Links.prototype = {
|
|||
* All history events are emitted from this object.
|
||||
*/
|
||||
historyObserver: {
|
||||
_batchProcessingDepth: 0,
|
||||
_batchCalledFrecencyChanged: false,
|
||||
|
||||
/**
|
||||
* Called by the history service.
|
||||
*/
|
||||
onBeginUpdateBatch() {
|
||||
this._batchProcessingDepth += 1;
|
||||
},
|
||||
|
||||
onEndUpdateBatch() {
|
||||
this._batchProcessingDepth -= 1;
|
||||
if (this._batchProcessingDepth == 0 && this._batchCalledFrecencyChanged) {
|
||||
this.onManyFrecenciesChanged();
|
||||
this._batchCalledFrecencyChanged = false;
|
||||
}
|
||||
},
|
||||
|
||||
onDeleteURI: function historyObserver_onDeleteURI(aURI) {
|
||||
// let observers remove sensetive data associated with deleted visit
|
||||
gLinks.emit("deleteURI", {
|
||||
|
@ -65,6 +83,15 @@ Links.prototype = {
|
|||
|
||||
onFrecencyChanged: function historyObserver_onFrecencyChanged(aURI,
|
||||
aNewFrecency, aGUID, aHidden, aLastVisitDate) { // jshint ignore:line
|
||||
|
||||
// If something is doing a batch update of history entries we don't want
|
||||
// to do lots of work for each record. So we just track the fact we need
|
||||
// to call onManyFrecenciesChanged() once the batch is complete.
|
||||
if (this._batchProcessingDepth > 0) {
|
||||
this._batchCalledFrecencyChanged = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// The implementation of the query in getLinks excludes hidden and
|
||||
// unvisited pages, so it's important to exclude them here, too.
|
||||
if (!aHidden && aLastVisitDate &&
|
||||
|
@ -84,6 +111,14 @@ Links.prototype = {
|
|||
gLinks.emit("manyLinksChanged");
|
||||
},
|
||||
|
||||
onVisit(aURI, aVisitId, aTime, aSessionId, aReferrerVisitId, aTransitionType,
|
||||
aGuid, aHidden, aVisitCount, aTyped, aLastKnownTitle) {
|
||||
// For new visits, if we're not batch processing, notify for a title update
|
||||
if (!this._batchProcessingDepth && aVisitCount == 1 && aLastKnownTitle) {
|
||||
this.onTitleChanged(aURI, aTitle, aGuid);
|
||||
}
|
||||
},
|
||||
|
||||
onTitleChanged: function historyObserver_onTitleChanged(aURI, aNewTitle) {
|
||||
if (NewTabUtils.linkChecker.checkLoadURI(aURI.spec)) {
|
||||
gLinks.emit("linkChanged", {
|
||||
|
|
|
@ -145,15 +145,14 @@ add_task(function* test_Links_onLinkChanged() {
|
|||
|
||||
let linkChangedPromise = new Promise(resolve => {
|
||||
let handler = (_, link) => { // jshint ignore:line
|
||||
/* There are 3 linkChanged events:
|
||||
/* There are 2 linkChanged events:
|
||||
* 1. visit insertion (-1 frecency by default)
|
||||
* 2. frecency score update (after transition type calculation etc)
|
||||
* 3. title change
|
||||
*/
|
||||
if (link.url === url) {
|
||||
equal(link.url, url, `expected url on linkChanged event`);
|
||||
linkChangedMsgCount += 1;
|
||||
if (linkChangedMsgCount === 3) {
|
||||
if (linkChangedMsgCount === 2) {
|
||||
ok(true, `all linkChanged events captured`);
|
||||
provider.off("linkChanged", this);
|
||||
resolve();
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
/* import-globals-from ../../../toolkit/content/treeUtils.js */
|
||||
|
||||
Components.utils.import("resource://gre/modules/Services.jsm");
|
||||
Components.utils.import("resource://gre/modules/AppConstants.jsm");
|
||||
|
||||
const nsIPermissionManager = Components.interfaces.nsIPermissionManager;
|
||||
const nsICookiePermission = Components.interfaces.nsICookiePermission;
|
||||
|
@ -334,6 +335,7 @@ var gPermissionManager = {
|
|||
} else if (AppConstants.platform == "macosx" &&
|
||||
aEvent.keyCode == KeyEvent.DOM_VK_BACK_SPACE) {
|
||||
this.onPermissionDeleted();
|
||||
aEvent.preventDefault();
|
||||
}
|
||||
},
|
||||
|
||||
|
|
|
@ -520,6 +520,9 @@
|
|||
@RESPATH@/components/remotebrowserutils.manifest
|
||||
@RESPATH@/components/RemoteWebNavigation.js
|
||||
|
||||
@RESPATH@/components/ProcessSelector.js
|
||||
@RESPATH@/components/ProcessSelector.manifest
|
||||
|
||||
@RESPATH@/components/SlowScriptDebug.manifest
|
||||
@RESPATH@/components/SlowScriptDebug.js
|
||||
|
||||
|
|
|
@ -292,6 +292,16 @@ window:not([chromehidden~="toolbar"]) #urlbar-wrapper {
|
|||
background-color: var(--tab-selection-background-color);
|
||||
}
|
||||
|
||||
.tab-throbber[selected][progress] {
|
||||
list-style-image: url("chrome://browser/skin/compacttheme/loading-inverted.png");
|
||||
}
|
||||
|
||||
@media (min-resolution: 1.1dppx) {
|
||||
.tab-throbber[selected][progress] {
|
||||
list-style-image: url("chrome://browser/skin/compacttheme/loading-inverted@2x.png");
|
||||
}
|
||||
}
|
||||
|
||||
.tab-icon-sound[soundplaying],
|
||||
.tab-icon-sound[muted] {
|
||||
filter: url(chrome://global/skin/filters.svg#fill) !important; /* removes drop-shadow filter */
|
||||
|
|
Двоичный файл не отображается.
После Ширина: | Высота: | Размер: 6.2 KiB |
Двоичный файл не отображается.
После Ширина: | Высота: | Размер: 16 KiB |
|
@ -491,6 +491,10 @@ description > html|a {
|
|||
margin-inline-end: 14px !important;
|
||||
}
|
||||
|
||||
#verifiedManage:visited {
|
||||
color: var(--in-content-link-color);
|
||||
}
|
||||
|
||||
.fxaSyncIllustration {
|
||||
width: 231px;
|
||||
}
|
||||
|
|
|
@ -139,6 +139,8 @@
|
|||
skin/classic/browser/privatebrowsing/private-browsing.svg (../shared/privatebrowsing/private-browsing.svg)
|
||||
skin/classic/browser/privatebrowsing/tracking-protection-off.svg (../shared/privatebrowsing/tracking-protection-off.svg)
|
||||
skin/classic/browser/privatebrowsing/tracking-protection.svg (../shared/privatebrowsing/tracking-protection.svg)
|
||||
skin/classic/browser/compacttheme/loading-inverted.png (../shared/compacttheme/loading-inverted.png)
|
||||
skin/classic/browser/compacttheme/loading-inverted@2x.png (../shared/compacttheme/loading-inverted@2x.png)
|
||||
skin/classic/browser/compacttheme/urlbar-history-dropmarker.svg (../shared/compacttheme/urlbar-history-dropmarker.svg)
|
||||
skin/classic/browser/urlbar-star.svg (../shared/urlbar-star.svg)
|
||||
skin/classic/browser/urlbar-tab.svg (../shared/urlbar-tab.svg)
|
||||
|
|
|
@ -42,6 +42,7 @@ SEARCH_PATHS = [
|
|||
'python/blessings',
|
||||
'python/compare-locales',
|
||||
'python/configobj',
|
||||
'python/dlmanager',
|
||||
'python/futures',
|
||||
'python/jsmin',
|
||||
'python/psutil',
|
||||
|
@ -94,6 +95,7 @@ SEARCH_PATHS = [
|
|||
'testing/web-platform',
|
||||
'testing/web-platform/harness',
|
||||
'testing/web-platform/tests/tools/wptserve',
|
||||
'testing/web-platform/tests/tools/six',
|
||||
'testing/xpcshell',
|
||||
'xpcom/idl-parser',
|
||||
]
|
||||
|
|
|
@ -116,6 +116,7 @@ skip-if = (os == 'linux' && bits == 32 && debug) # bug 1328915, disable linux32
|
|||
[browser_inspector_infobar_01.js]
|
||||
[browser_inspector_infobar_02.js]
|
||||
[browser_inspector_infobar_03.js]
|
||||
[browser_inspector_infobar_04.js]
|
||||
[browser_inspector_infobar_textnode.js]
|
||||
[browser_inspector_initialization.js]
|
||||
skip-if = (e10s && debug) # Bug 1250058 - Docshell leak on debug e10s
|
||||
|
|
|
@ -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/. */
|
||||
|
||||
"use strict";
|
||||
|
||||
// Check the position and text content of the highlighter nodeinfo bar under page zoom.
|
||||
|
||||
const TEST_URI = URL_ROOT + "doc_inspector_infobar_01.html";
|
||||
|
||||
add_task(function* () {
|
||||
let {inspector, testActor} = yield openInspectorForURL(TEST_URI);
|
||||
let testData = {
|
||||
selector: "#top",
|
||||
dims: "500" + " \u00D7 " + "100"
|
||||
};
|
||||
|
||||
yield testInfobar(testData, inspector, testActor);
|
||||
info("Change zoom page to level 2.");
|
||||
yield testActor.zoomPageTo(2);
|
||||
info("Testing again the infobar after zoom.");
|
||||
yield testInfobar(testData, inspector, testActor);
|
||||
});
|
||||
|
||||
function* testInfobar(test, inspector, testActor) {
|
||||
info(`Testing ${test.selector}`);
|
||||
|
||||
yield selectAndHighlightNode(test.selector, inspector);
|
||||
|
||||
// Ensure the node is the correct one.
|
||||
let id = yield testActor.getHighlighterNodeTextContent(
|
||||
"box-model-infobar-id");
|
||||
is(id, test.selector, `Node ${test.selector} selected.`);
|
||||
|
||||
let dims = yield testActor.getHighlighterNodeTextContent(
|
||||
"box-model-infobar-dimensions");
|
||||
is(dims, test.dims, "Node's infobar displays the right dimensions.");
|
||||
}
|
|
@ -469,17 +469,18 @@ html, body, #app, #memory-tool {
|
|||
}
|
||||
|
||||
.heap-tree-item-name {
|
||||
/**
|
||||
* Flex: contains an .arrow and some text, which need to be laid out
|
||||
* horizontally, vertically aligned in the middle of the container.
|
||||
*/
|
||||
display: flex;
|
||||
align-items: center;
|
||||
/**
|
||||
* Flexing to fill out remaining vertical space.
|
||||
* @see .header and .heap-tree-item */
|
||||
flex: 1;
|
||||
padding-inline-start: 5px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.heap-tree-item-name .arrow {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -520,19 +521,16 @@ html, body, #app, #memory-tool {
|
|||
}
|
||||
|
||||
.heap-tree-item-individuals {
|
||||
width: 38px;
|
||||
min-width: 20px;
|
||||
min-width: 38px;
|
||||
overflow: hidden;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.heap-tree-item-individuals > button {
|
||||
height: 10px;
|
||||
width: 32px;
|
||||
|
||||
/* Override default styles for toolbar buttons to fix entire row height. */
|
||||
margin: 0 auto !important;
|
||||
padding: 0;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -15,7 +15,10 @@ const {
|
|||
isNodeValid,
|
||||
moveInfobar,
|
||||
} = require("./utils/markup");
|
||||
const { setIgnoreLayoutChanges } = require("devtools/shared/layout/utils");
|
||||
const {
|
||||
setIgnoreLayoutChanges,
|
||||
getCurrentZoom,
|
||||
} = require("devtools/shared/layout/utils");
|
||||
const inspector = require("devtools/server/actors/inspector");
|
||||
const nodeConstants = require("devtools/shared/dom-node-constants");
|
||||
|
||||
|
@ -670,10 +673,14 @@ BoxModelHighlighter.prototype = extend(AutoRefreshHighlighter.prototype, {
|
|||
pseudos += ":" + pseudo;
|
||||
}
|
||||
|
||||
let rect = this._getOuterQuad("border").bounds;
|
||||
let dim = parseFloat(rect.width.toPrecision(6)) +
|
||||
// We want to display the original `width` and `height`, instead of the ones affected
|
||||
// by any zoom. Since the infobar can be displayed also for text nodes, we can't
|
||||
// access the computed style for that, and this is why we recalculate them here.
|
||||
let zoom = getCurrentZoom(this.win);
|
||||
let { width, height } = this._getOuterQuad("border").bounds;
|
||||
let dim = parseFloat((width / zoom).toPrecision(6)) +
|
||||
" \u00D7 " +
|
||||
parseFloat(rect.height.toPrecision(6));
|
||||
parseFloat((height / zoom).toPrecision(6));
|
||||
|
||||
this.getElement("infobar-tagname").setTextContent(displayName);
|
||||
this.getElement("infobar-id").setTextContent(id);
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
<html>
|
||||
<head>
|
||||
<script>
|
||||
o1 = document.createElement("script");
|
||||
o2 = document.implementation.createDocument('', '', null);
|
||||
o3 = document.createElement("iframe");
|
||||
document.documentElement.appendChild(o3);
|
||||
o4 = o3.contentWindow;
|
||||
o5 = document.createTextNode('o2.adoptNode(o3); try { o4.location = "" } catch(e) {}');
|
||||
o1.appendChild(o5);
|
||||
document.documentElement.appendChild(o1);
|
||||
</script>
|
||||
</head>
|
||||
</html>
|
|
@ -14,3 +14,4 @@ load 678872-1.html
|
|||
skip-if(Android) pref(dom.disable_open_during_load,false) load 914521.html
|
||||
pref(browser.send_pings,true) load 1257730-1.html
|
||||
load 1331295.html
|
||||
load 1341657.html
|
||||
|
|
|
@ -9892,9 +9892,14 @@ nsDocShell::InternalLoad(nsIURI* aURI,
|
|||
if (IsFrame() && !isTargetTopLevelDocShell) {
|
||||
nsCOMPtr<Element> requestingElement =
|
||||
mScriptGlobal->AsOuter()->GetFrameElementInternal();
|
||||
NS_ASSERTION(requestingElement, "A frame but no DOM element!?");
|
||||
if (requestingElement) {
|
||||
contentType = requestingElement->IsHTMLElement(nsGkAtoms::iframe) ?
|
||||
nsIContentPolicy::TYPE_INTERNAL_IFRAME : nsIContentPolicy::TYPE_INTERNAL_FRAME;
|
||||
} else {
|
||||
// If we have lost our frame element by now, just assume we're
|
||||
// an iframe since that's more common.
|
||||
contentType = nsIContentPolicy::TYPE_INTERNAL_IFRAME;
|
||||
}
|
||||
} else {
|
||||
contentType = nsIContentPolicy::TYPE_DOCUMENT;
|
||||
isTargetTopLevelDocShell = true;
|
||||
|
@ -9924,6 +9929,7 @@ nsDocShell::InternalLoad(nsIURI* aURI,
|
|||
requestingContext = requestingElement;
|
||||
|
||||
#ifdef DEBUG
|
||||
if (requestingElement) {
|
||||
// Get the docshell type for requestingElement.
|
||||
nsCOMPtr<nsIDocument> requestingDoc = requestingElement->OwnerDoc();
|
||||
nsCOMPtr<nsIDocShell> elementDocShell = requestingDoc->GetDocShell();
|
||||
|
@ -9931,6 +9937,7 @@ nsDocShell::InternalLoad(nsIURI* aURI,
|
|||
// requestingElement docshell type = current docshell type.
|
||||
MOZ_ASSERT(mItemType == elementDocShell->ItemType(),
|
||||
"subframes should have the same docshell type as their parent");
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
|
|
|
@ -234,7 +234,7 @@ AnimationEffectReadOnly::GetComputedTimingAt(
|
|||
thisIterationReverse = (result.mCurrentIteration & 1) == 0;
|
||||
break;
|
||||
default:
|
||||
MOZ_ASSERT(true, "Unknown PlaybackDirection type");
|
||||
MOZ_ASSERT_UNREACHABLE("Unknown PlaybackDirection type");
|
||||
}
|
||||
if (thisIterationReverse) {
|
||||
progress = 1.0 - progress;
|
||||
|
|
|
@ -1,126 +0,0 @@
|
|||
/* -*- 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 "ArchiveEvent.h"
|
||||
|
||||
#include "nsCExternalHandlerService.h"
|
||||
#include "nsProxyRelease.h"
|
||||
|
||||
USING_ARCHIVEREADER_NAMESPACE
|
||||
|
||||
NS_IMPL_ISUPPORTS0(ArchiveItem)
|
||||
|
||||
ArchiveItem::ArchiveItem()
|
||||
{
|
||||
}
|
||||
|
||||
ArchiveItem::~ArchiveItem()
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
nsCString
|
||||
ArchiveItem::GetType()
|
||||
{
|
||||
if (mType.IsEmpty()) {
|
||||
return NS_LITERAL_CSTRING("binary/octet-stream");
|
||||
}
|
||||
|
||||
return mType;
|
||||
}
|
||||
|
||||
void
|
||||
ArchiveItem::SetType(const nsCString& aType)
|
||||
{
|
||||
mType = aType;
|
||||
}
|
||||
|
||||
ArchiveReaderEvent::ArchiveReaderEvent(ArchiveReader* aArchiveReader)
|
||||
: mArchiveReader(aArchiveReader)
|
||||
{
|
||||
}
|
||||
|
||||
ArchiveReaderEvent::~ArchiveReaderEvent()
|
||||
{
|
||||
if (!NS_IsMainThread()) {
|
||||
NS_ReleaseOnMainThread(mMimeService.forget());
|
||||
}
|
||||
}
|
||||
|
||||
// From the filename to the mimetype:
|
||||
nsresult
|
||||
ArchiveReaderEvent::GetType(nsCString& aExt,
|
||||
nsCString& aMimeType)
|
||||
{
|
||||
MOZ_ASSERT(NS_IsMainThread());
|
||||
|
||||
nsresult rv;
|
||||
|
||||
if (mMimeService.get() == nullptr) {
|
||||
mMimeService = do_GetService(NS_MIMESERVICE_CONTRACTID, &rv);
|
||||
NS_ENSURE_SUCCESS(rv, rv);
|
||||
}
|
||||
|
||||
rv = mMimeService->GetTypeFromExtension(aExt, aMimeType);
|
||||
NS_ENSURE_SUCCESS(rv, rv);
|
||||
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
NS_IMETHODIMP
|
||||
ArchiveReaderEvent::Run()
|
||||
{
|
||||
return Exec();
|
||||
}
|
||||
|
||||
nsresult
|
||||
ArchiveReaderEvent::RunShare(nsresult aStatus)
|
||||
{
|
||||
mStatus = aStatus;
|
||||
|
||||
NS_DispatchToMainThread(NewRunnableMethod(this, &ArchiveReaderEvent::ShareMainThread));
|
||||
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
void
|
||||
ArchiveReaderEvent::ShareMainThread()
|
||||
{
|
||||
nsTArray<RefPtr<File>> fileList;
|
||||
|
||||
if (!NS_FAILED(mStatus)) {
|
||||
// This extra step must run in the main thread:
|
||||
for (uint32_t index = 0; index < mFileList.Length(); ++index) {
|
||||
RefPtr<ArchiveItem> item = mFileList[index];
|
||||
|
||||
nsString tmp;
|
||||
nsresult rv = item->GetFilename(tmp);
|
||||
nsCString filename = NS_ConvertUTF16toUTF8(tmp);
|
||||
if (NS_FAILED(rv)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
int32_t offset = filename.RFindChar('.');
|
||||
if (offset != kNotFound) {
|
||||
filename.Cut(0, offset + 1);
|
||||
|
||||
// Just to be sure, if something goes wrong, the mimetype is an empty string:
|
||||
nsCString type;
|
||||
if (NS_SUCCEEDED(GetType(filename, type))) {
|
||||
item->SetType(type);
|
||||
}
|
||||
}
|
||||
|
||||
// This is a File:
|
||||
RefPtr<File> file = item->GetFile(mArchiveReader);
|
||||
if (file) {
|
||||
fileList.AppendElement(file);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mArchiveReader->Ready(fileList, mStatus);
|
||||
}
|
|
@ -1,84 +0,0 @@
|
|||
/* -*- 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_archivereader_domarchiveevent_h__
|
||||
#define mozilla_dom_archivereader_domarchiveevent_h__
|
||||
|
||||
#include "ArchiveReader.h"
|
||||
|
||||
#include "mozilla/dom/File.h"
|
||||
#include "nsISeekableStream.h"
|
||||
#include "nsIMIMEService.h"
|
||||
|
||||
#include "ArchiveReaderCommon.h"
|
||||
|
||||
BEGIN_ARCHIVEREADER_NAMESPACE
|
||||
|
||||
/**
|
||||
* This class contains all the info needed for a single item
|
||||
* It must contain the implementation of the File() method.
|
||||
*/
|
||||
class ArchiveItem : public nsISupports
|
||||
{
|
||||
public:
|
||||
NS_DECL_THREADSAFE_ISUPPORTS
|
||||
|
||||
ArchiveItem();
|
||||
|
||||
// Getter/Setter for the type
|
||||
nsCString GetType();
|
||||
void SetType(const nsCString& aType);
|
||||
|
||||
// Getter for the filename
|
||||
virtual nsresult GetFilename(nsString& aFilename) = 0;
|
||||
|
||||
// Generate a File
|
||||
virtual already_AddRefed<File> GetFile(ArchiveReader* aArchiveReader) = 0;
|
||||
|
||||
protected:
|
||||
virtual ~ArchiveItem();
|
||||
|
||||
nsCString mType;
|
||||
};
|
||||
|
||||
/**
|
||||
* This class must be extended by any archive format supported by ArchiveReader API
|
||||
* This class runs in a different thread and it calls the 'exec()' method.
|
||||
* The exec() must populate mFileList and mStatus then it must call RunShare();
|
||||
*/
|
||||
class ArchiveReaderEvent : public Runnable
|
||||
{
|
||||
public:
|
||||
NS_DECL_NSIRUNNABLE
|
||||
|
||||
explicit ArchiveReaderEvent(ArchiveReader* aArchiveReader);
|
||||
|
||||
protected:
|
||||
virtual ~ArchiveReaderEvent();
|
||||
|
||||
public:
|
||||
// This must be implemented
|
||||
virtual nsresult Exec() = 0;
|
||||
|
||||
protected:
|
||||
nsresult GetType(nsCString& aExt,
|
||||
nsCString& aMimeType);
|
||||
|
||||
nsresult RunShare(nsresult aStatus);
|
||||
void ShareMainThread();
|
||||
|
||||
protected: // data
|
||||
ArchiveReader* mArchiveReader;
|
||||
|
||||
nsCOMPtr<nsIMIMEService> mMimeService;
|
||||
|
||||
nsTArray<RefPtr<ArchiveItem> > mFileList; // this must be populated
|
||||
nsresult mStatus;
|
||||
};
|
||||
|
||||
END_ARCHIVEREADER_NAMESPACE
|
||||
|
||||
#endif // mozilla_dom_archivereader_domarchiveevent_h__
|
|
@ -1,217 +0,0 @@
|
|||
/* -*- 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 "ArchiveReader.h"
|
||||
#include "ArchiveRequest.h"
|
||||
#include "ArchiveEvent.h"
|
||||
#include "ArchiveZipEvent.h"
|
||||
|
||||
#include "nsIURI.h"
|
||||
#include "nsNetCID.h"
|
||||
|
||||
#include "mozilla/dom/ArchiveReaderBinding.h"
|
||||
#include "mozilla/dom/BindingDeclarations.h"
|
||||
#include "mozilla/dom/File.h"
|
||||
#include "mozilla/dom/EncodingUtils.h"
|
||||
#include "mozilla/Preferences.h"
|
||||
|
||||
using namespace mozilla;
|
||||
using namespace mozilla::dom;
|
||||
USING_ARCHIVEREADER_NAMESPACE
|
||||
|
||||
/* static */ already_AddRefed<ArchiveReader>
|
||||
ArchiveReader::Constructor(const GlobalObject& aGlobal,
|
||||
Blob& aBlob,
|
||||
const ArchiveReaderOptions& aOptions,
|
||||
ErrorResult& aError)
|
||||
{
|
||||
nsCOMPtr<nsPIDOMWindowInner> window = do_QueryInterface(aGlobal.GetAsSupports());
|
||||
if (!window) {
|
||||
aError.Throw(NS_ERROR_UNEXPECTED);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
nsAutoCString encoding;
|
||||
if (!EncodingUtils::FindEncodingForLabelNoReplacement(aOptions.mEncoding,
|
||||
encoding)) {
|
||||
aError.ThrowRangeError<MSG_ENCODING_NOT_SUPPORTED>(aOptions.mEncoding);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
RefPtr<ArchiveReader> reader =
|
||||
new ArchiveReader(aBlob, window, encoding);
|
||||
return reader.forget();
|
||||
}
|
||||
|
||||
ArchiveReader::ArchiveReader(Blob& aBlob, nsPIDOMWindowInner* aWindow,
|
||||
const nsACString& aEncoding)
|
||||
: mBlobImpl(aBlob.Impl())
|
||||
, mWindow(aWindow)
|
||||
, mStatus(NOT_STARTED)
|
||||
, mEncoding(aEncoding)
|
||||
{
|
||||
MOZ_ASSERT(aWindow);
|
||||
}
|
||||
|
||||
ArchiveReader::~ArchiveReader()
|
||||
{
|
||||
}
|
||||
|
||||
/* virtual */ JSObject*
|
||||
ArchiveReader::WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto)
|
||||
{
|
||||
return ArchiveReaderBinding::Wrap(aCx, this, aGivenProto);
|
||||
}
|
||||
|
||||
nsresult
|
||||
ArchiveReader::RegisterRequest(ArchiveRequest* aRequest)
|
||||
{
|
||||
switch (mStatus) {
|
||||
// Append to the list and let's start to work:
|
||||
case NOT_STARTED:
|
||||
mRequests.AppendElement(aRequest);
|
||||
return OpenArchive();
|
||||
|
||||
// Just append to the list:
|
||||
case WORKING:
|
||||
mRequests.AppendElement(aRequest);
|
||||
return NS_OK;
|
||||
|
||||
// Return data!
|
||||
case READY:
|
||||
RequestReady(aRequest);
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
NS_ASSERTION(false, "unexpected mStatus value");
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
// This returns the input stream
|
||||
nsresult
|
||||
ArchiveReader::GetInputStream(nsIInputStream** aInputStream)
|
||||
{
|
||||
// Getting the input stream
|
||||
ErrorResult rv;
|
||||
mBlobImpl->GetInternalStream(aInputStream, rv);
|
||||
if (NS_WARN_IF(rv.Failed())) {
|
||||
return rv.StealNSResult();
|
||||
}
|
||||
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
nsresult
|
||||
ArchiveReader::GetSize(uint64_t* aSize)
|
||||
{
|
||||
ErrorResult rv;
|
||||
*aSize = mBlobImpl->GetSize(rv);
|
||||
return rv.StealNSResult();
|
||||
}
|
||||
|
||||
// Here we open the archive:
|
||||
nsresult
|
||||
ArchiveReader::OpenArchive()
|
||||
{
|
||||
mStatus = WORKING;
|
||||
nsresult rv;
|
||||
|
||||
// Target:
|
||||
nsCOMPtr<nsIEventTarget> target = do_GetService(NS_STREAMTRANSPORTSERVICE_CONTRACTID);
|
||||
NS_ASSERTION(target, "Must have stream transport service");
|
||||
|
||||
// Here a Event to make everything async:
|
||||
RefPtr<ArchiveReaderEvent> event;
|
||||
|
||||
/* FIXME: If we want to support more than 1 format we should check the content type here: */
|
||||
event = new ArchiveReaderZipEvent(this, mEncoding);
|
||||
rv = target->Dispatch(event, NS_DISPATCH_NORMAL);
|
||||
NS_ENSURE_SUCCESS(rv, rv);
|
||||
|
||||
// In order to be sure that this object exists when the event finishes its task,
|
||||
// we increase the refcount here:
|
||||
AddRef();
|
||||
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
// Data received from the dispatched event:
|
||||
void
|
||||
ArchiveReader::Ready(nsTArray<RefPtr<File>>& aFileList,
|
||||
nsresult aStatus)
|
||||
{
|
||||
mStatus = READY;
|
||||
|
||||
// Let's store the values:
|
||||
mData.fileList = aFileList;
|
||||
mData.status = aStatus;
|
||||
|
||||
// Propagate the results:
|
||||
for (uint32_t index = 0; index < mRequests.Length(); ++index) {
|
||||
RefPtr<ArchiveRequest> request = mRequests[index];
|
||||
RequestReady(request);
|
||||
}
|
||||
|
||||
mRequests.Clear();
|
||||
|
||||
// The async operation is concluded, we can decrease the reference:
|
||||
Release();
|
||||
}
|
||||
|
||||
void
|
||||
ArchiveReader::RequestReady(ArchiveRequest* aRequest)
|
||||
{
|
||||
// The request will do the rest:
|
||||
aRequest->ReaderReady(mData.fileList, mData.status);
|
||||
}
|
||||
|
||||
already_AddRefed<ArchiveRequest>
|
||||
ArchiveReader::GetFilenames()
|
||||
{
|
||||
RefPtr<ArchiveRequest> request = GenerateArchiveRequest();
|
||||
request->OpGetFilenames();
|
||||
|
||||
return request.forget();
|
||||
}
|
||||
|
||||
already_AddRefed<ArchiveRequest>
|
||||
ArchiveReader::GetFile(const nsAString& filename)
|
||||
{
|
||||
RefPtr<ArchiveRequest> request = GenerateArchiveRequest();
|
||||
request->OpGetFile(filename);
|
||||
|
||||
return request.forget();
|
||||
}
|
||||
|
||||
already_AddRefed<ArchiveRequest>
|
||||
ArchiveReader::GetFiles()
|
||||
{
|
||||
RefPtr<ArchiveRequest> request = GenerateArchiveRequest();
|
||||
request->OpGetFiles();
|
||||
|
||||
return request.forget();
|
||||
}
|
||||
|
||||
already_AddRefed<ArchiveRequest>
|
||||
ArchiveReader::GenerateArchiveRequest()
|
||||
{
|
||||
NS_ASSERTION(NS_IsMainThread(), "Wrong thread!");
|
||||
return ArchiveRequest::Create(mWindow, this);
|
||||
}
|
||||
|
||||
NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(ArchiveReader,
|
||||
mBlobImpl,
|
||||
mWindow,
|
||||
mData.fileList,
|
||||
mRequests)
|
||||
|
||||
NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(ArchiveReader)
|
||||
NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
|
||||
NS_INTERFACE_MAP_ENTRY(nsISupports)
|
||||
NS_INTERFACE_MAP_END
|
||||
|
||||
NS_IMPL_CYCLE_COLLECTING_ADDREF(ArchiveReader)
|
||||
NS_IMPL_CYCLE_COLLECTING_RELEASE(ArchiveReader)
|
|
@ -1,119 +0,0 @@
|
|||
/* -*- 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_archivereader_domarchivereader_h__
|
||||
#define mozilla_dom_archivereader_domarchivereader_h__
|
||||
|
||||
#include "nsWrapperCache.h"
|
||||
|
||||
#include "ArchiveReaderCommon.h"
|
||||
|
||||
#include "nsCOMArray.h"
|
||||
#include "nsIChannel.h"
|
||||
#include "mozilla/Attributes.h"
|
||||
|
||||
namespace mozilla {
|
||||
namespace dom {
|
||||
struct ArchiveReaderOptions;
|
||||
class Blob;
|
||||
class BlobImpl;
|
||||
class File;
|
||||
class GlobalObject;
|
||||
} // namespace dom
|
||||
} // namespace mozilla
|
||||
|
||||
BEGIN_ARCHIVEREADER_NAMESPACE
|
||||
|
||||
class ArchiveRequest;
|
||||
|
||||
/**
|
||||
* This is the ArchiveReader object
|
||||
*/
|
||||
class ArchiveReader final : public nsISupports,
|
||||
public nsWrapperCache
|
||||
{
|
||||
public:
|
||||
NS_DECL_CYCLE_COLLECTING_ISUPPORTS
|
||||
NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS(ArchiveReader)
|
||||
|
||||
static already_AddRefed<ArchiveReader>
|
||||
Constructor(const GlobalObject& aGlobal, Blob& aBlob,
|
||||
const ArchiveReaderOptions& aOptions, ErrorResult& aError);
|
||||
|
||||
ArchiveReader(Blob& aBlob, nsPIDOMWindowInner* aWindow,
|
||||
const nsACString& aEncoding);
|
||||
|
||||
nsPIDOMWindowInner* GetParentObject() const
|
||||
{
|
||||
return mWindow;
|
||||
}
|
||||
|
||||
virtual JSObject* WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto) override;
|
||||
|
||||
already_AddRefed<ArchiveRequest> GetFilenames();
|
||||
already_AddRefed<ArchiveRequest> GetFile(const nsAString& filename);
|
||||
already_AddRefed<ArchiveRequest> GetFiles();
|
||||
|
||||
nsresult GetInputStream(nsIInputStream** aInputStream);
|
||||
nsresult GetSize(uint64_t* aSize);
|
||||
|
||||
public: // for the ArchiveRequest:
|
||||
nsresult RegisterRequest(ArchiveRequest* aRequest);
|
||||
|
||||
public: // For events:
|
||||
BlobImpl* GetBlobImpl() const
|
||||
{
|
||||
return mBlobImpl;
|
||||
}
|
||||
|
||||
void Ready(nsTArray<RefPtr<File>>& aFileList, nsresult aStatus);
|
||||
|
||||
private:
|
||||
~ArchiveReader();
|
||||
|
||||
already_AddRefed<ArchiveRequest> GenerateArchiveRequest();
|
||||
|
||||
nsresult OpenArchive();
|
||||
|
||||
void RequestReady(ArchiveRequest* aRequest);
|
||||
|
||||
protected:
|
||||
// The archive blob/file
|
||||
RefPtr<BlobImpl> mBlobImpl;
|
||||
|
||||
// The window is needed by the requests
|
||||
nsCOMPtr<nsPIDOMWindowInner> mWindow;
|
||||
|
||||
// Are we ready to return data?
|
||||
enum {
|
||||
NOT_STARTED = 0,
|
||||
WORKING,
|
||||
READY
|
||||
} mStatus;
|
||||
|
||||
// State of the read:
|
||||
enum {
|
||||
Header, // We are collecting the header: 30bytes
|
||||
Name, // The name length is contained in the header
|
||||
Data, // The length of the data segment COULD be written in the header
|
||||
Search // ... if the data length is unknown (== 0) we wait until we read a new header
|
||||
} mReadStatus;
|
||||
|
||||
// List of requests to be processed
|
||||
nsTArray<RefPtr<ArchiveRequest> > mRequests;
|
||||
|
||||
// Everything related to the blobs and the status:
|
||||
struct {
|
||||
nsTArray<RefPtr<File>> fileList;
|
||||
nsresult status;
|
||||
} mData;
|
||||
|
||||
nsCString mEncoding;
|
||||
};
|
||||
|
||||
END_ARCHIVEREADER_NAMESPACE
|
||||
|
||||
#endif // mozilla_dom_archivereader_domarchivereader_h__
|
|
@ -1,24 +0,0 @@
|
|||
/* -*- 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_archivereader_archivereader_h
|
||||
#define mozilla_dom_archivereader_archivereader_h
|
||||
|
||||
#include "mozilla/DOMEventTargetHelper.h"
|
||||
#include "nsCOMPtr.h"
|
||||
#include "nsCycleCollectionParticipant.h"
|
||||
#include "nsDebug.h"
|
||||
#include "nsString.h"
|
||||
#include "nsTArray.h"
|
||||
|
||||
#define BEGIN_ARCHIVEREADER_NAMESPACE \
|
||||
namespace mozilla { namespace dom { namespace archivereader {
|
||||
#define END_ARCHIVEREADER_NAMESPACE \
|
||||
} /* namespace archivereader */ } /* namespace dom */ } /* namespace mozilla */
|
||||
#define USING_ARCHIVEREADER_NAMESPACE \
|
||||
using namespace mozilla::dom::archivereader;
|
||||
|
||||
#endif // mozilla_dom_archivereader_archivereadercommon_h
|
|
@ -1,272 +0,0 @@
|
|||
/* -*- 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 "ArchiveRequest.h"
|
||||
|
||||
#include "mozilla/EventDispatcher.h"
|
||||
#include "mozilla/dom/ArchiveRequestBinding.h"
|
||||
#include "mozilla/dom/ScriptSettings.h"
|
||||
#include "nsContentUtils.h"
|
||||
|
||||
using namespace mozilla;
|
||||
|
||||
USING_ARCHIVEREADER_NAMESPACE
|
||||
|
||||
/**
|
||||
* Class used to make asynchronous the ArchiveRequest.
|
||||
*/
|
||||
class ArchiveRequestEvent : public Runnable
|
||||
{
|
||||
public:
|
||||
NS_DECL_NSIRUNNABLE
|
||||
|
||||
explicit ArchiveRequestEvent(ArchiveRequest* aRequest)
|
||||
: mRequest(aRequest)
|
||||
{
|
||||
}
|
||||
|
||||
protected:
|
||||
~ArchiveRequestEvent()
|
||||
{
|
||||
}
|
||||
|
||||
private: //data
|
||||
RefPtr<ArchiveRequest> mRequest;
|
||||
};
|
||||
|
||||
NS_IMETHODIMP
|
||||
ArchiveRequestEvent::Run()
|
||||
{
|
||||
MOZ_ASSERT(mRequest, "the request is not longer valid");
|
||||
mRequest->Run();
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
// ArchiveRequest
|
||||
|
||||
ArchiveRequest::ArchiveRequest(nsPIDOMWindowInner* aWindow,
|
||||
ArchiveReader* aReader)
|
||||
: DOMRequest(aWindow),
|
||||
mArchiveReader(aReader)
|
||||
{
|
||||
MOZ_ASSERT(aReader);
|
||||
|
||||
/* An event to make this request asynchronous: */
|
||||
RefPtr<ArchiveRequestEvent> event = new ArchiveRequestEvent(this);
|
||||
NS_DispatchToCurrentThread(event);
|
||||
}
|
||||
|
||||
ArchiveRequest::~ArchiveRequest()
|
||||
{
|
||||
}
|
||||
|
||||
nsresult
|
||||
ArchiveRequest::GetEventTargetParent(EventChainPreVisitor& aVisitor)
|
||||
{
|
||||
aVisitor.mCanHandle = true;
|
||||
aVisitor.mParentTarget = nullptr;
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
/* virtual */ JSObject*
|
||||
ArchiveRequest::WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto)
|
||||
{
|
||||
return ArchiveRequestBinding::Wrap(aCx, this, aGivenProto);
|
||||
}
|
||||
|
||||
ArchiveReader*
|
||||
ArchiveRequest::Reader() const
|
||||
{
|
||||
NS_ASSERTION(NS_IsMainThread(), "Wrong thread!");
|
||||
|
||||
return mArchiveReader;
|
||||
}
|
||||
|
||||
// Here the request is processed:
|
||||
void
|
||||
ArchiveRequest::Run()
|
||||
{
|
||||
// Register this request to the reader.
|
||||
// When the reader is ready to return data, a 'Ready()' will be called
|
||||
nsresult rv = mArchiveReader->RegisterRequest(this);
|
||||
if (NS_FAILED(rv)) {
|
||||
FireError(rv);
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
ArchiveRequest::OpGetFilenames()
|
||||
{
|
||||
mOperation = GetFilenames;
|
||||
}
|
||||
|
||||
void
|
||||
ArchiveRequest::OpGetFile(const nsAString& aFilename)
|
||||
{
|
||||
mOperation = GetFile;
|
||||
mFilename = aFilename;
|
||||
}
|
||||
|
||||
void
|
||||
ArchiveRequest::OpGetFiles()
|
||||
{
|
||||
mOperation = GetFiles;
|
||||
}
|
||||
|
||||
nsresult
|
||||
ArchiveRequest::ReaderReady(nsTArray<RefPtr<File>>& aFileList,
|
||||
nsresult aStatus)
|
||||
{
|
||||
if (NS_FAILED(aStatus)) {
|
||||
FireError(aStatus);
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
nsresult rv;
|
||||
|
||||
AutoJSAPI jsapi;
|
||||
if (NS_WARN_IF(!jsapi.Init(GetOwner()))) {
|
||||
return NS_ERROR_UNEXPECTED;
|
||||
}
|
||||
JSContext* cx = jsapi.cx();
|
||||
|
||||
JS::Rooted<JS::Value> result(cx);
|
||||
switch (mOperation) {
|
||||
case GetFilenames:
|
||||
rv = GetFilenamesResult(cx, result.address(), aFileList);
|
||||
break;
|
||||
|
||||
case GetFile:
|
||||
rv = GetFileResult(cx, &result, aFileList);
|
||||
break;
|
||||
|
||||
case GetFiles:
|
||||
rv = GetFilesResult(cx, &result, aFileList);
|
||||
break;
|
||||
|
||||
default:
|
||||
rv = NS_ERROR_UNEXPECTED;
|
||||
break;
|
||||
}
|
||||
|
||||
if (NS_FAILED(rv)) {
|
||||
NS_WARNING("Get*Result failed!");
|
||||
}
|
||||
|
||||
if (NS_SUCCEEDED(rv)) {
|
||||
FireSuccess(result);
|
||||
}
|
||||
else {
|
||||
FireError(rv);
|
||||
}
|
||||
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
nsresult
|
||||
ArchiveRequest::GetFilenamesResult(JSContext* aCx,
|
||||
JS::Value* aValue,
|
||||
nsTArray<RefPtr<File>>& aFileList)
|
||||
{
|
||||
JS::Rooted<JSObject*> array(aCx, JS_NewArrayObject(aCx, aFileList.Length()));
|
||||
|
||||
if (!array) {
|
||||
return NS_ERROR_OUT_OF_MEMORY;
|
||||
}
|
||||
|
||||
JS::Rooted<JSString*> str(aCx);
|
||||
for (uint32_t i = 0; i < aFileList.Length(); ++i) {
|
||||
RefPtr<File> file = aFileList[i];
|
||||
|
||||
nsString filename;
|
||||
file->GetName(filename);
|
||||
|
||||
str = JS_NewUCStringCopyZ(aCx, filename.get());
|
||||
NS_ENSURE_TRUE(str, NS_ERROR_OUT_OF_MEMORY);
|
||||
|
||||
if (!JS_DefineElement(aCx, array, i, str, JSPROP_ENUMERATE)) {
|
||||
return NS_ERROR_FAILURE;
|
||||
}
|
||||
}
|
||||
|
||||
if (!JS_FreezeObject(aCx, array)) {
|
||||
return NS_ERROR_FAILURE;
|
||||
}
|
||||
|
||||
aValue->setObject(*array);
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
nsresult
|
||||
ArchiveRequest::GetFileResult(JSContext* aCx,
|
||||
JS::MutableHandle<JS::Value> aValue,
|
||||
nsTArray<RefPtr<File>>& aFileList)
|
||||
{
|
||||
for (uint32_t i = 0; i < aFileList.Length(); ++i) {
|
||||
RefPtr<File> file = aFileList[i];
|
||||
|
||||
nsString filename;
|
||||
file->GetName(filename);
|
||||
|
||||
if (filename == mFilename) {
|
||||
if (!ToJSValue(aCx, file, aValue)) {
|
||||
return NS_ERROR_FAILURE;
|
||||
}
|
||||
|
||||
return NS_OK;
|
||||
}
|
||||
}
|
||||
|
||||
return NS_ERROR_FAILURE;
|
||||
}
|
||||
|
||||
nsresult
|
||||
ArchiveRequest::GetFilesResult(JSContext* aCx,
|
||||
JS::MutableHandle<JS::Value> aValue,
|
||||
nsTArray<RefPtr<File>>& aFileList)
|
||||
{
|
||||
JS::Rooted<JSObject*> array(aCx, JS_NewArrayObject(aCx, aFileList.Length()));
|
||||
if (!array) {
|
||||
return NS_ERROR_OUT_OF_MEMORY;
|
||||
}
|
||||
|
||||
for (uint32_t i = 0; i < aFileList.Length(); ++i) {
|
||||
RefPtr<File> file = aFileList[i];
|
||||
|
||||
JS::Rooted<JS::Value> value(aCx);
|
||||
if (!ToJSValue(aCx, file, &value)) {
|
||||
return NS_ERROR_FAILURE;
|
||||
}
|
||||
|
||||
if (!JS_DefineElement(aCx, array, i, value, JSPROP_ENUMERATE)) {
|
||||
return NS_ERROR_FAILURE;
|
||||
}
|
||||
}
|
||||
|
||||
aValue.setObject(*array);
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
// static
|
||||
already_AddRefed<ArchiveRequest>
|
||||
ArchiveRequest::Create(nsPIDOMWindowInner* aOwner,
|
||||
ArchiveReader* aReader)
|
||||
{
|
||||
NS_ASSERTION(NS_IsMainThread(), "Wrong thread!");
|
||||
|
||||
RefPtr<ArchiveRequest> request = new ArchiveRequest(aOwner, aReader);
|
||||
|
||||
return request.forget();
|
||||
}
|
||||
|
||||
NS_IMPL_CYCLE_COLLECTION_INHERITED(ArchiveRequest, DOMRequest,
|
||||
mArchiveReader)
|
||||
|
||||
NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION_INHERITED(ArchiveRequest)
|
||||
NS_INTERFACE_MAP_END_INHERITING(DOMRequest)
|
||||
|
||||
NS_IMPL_ADDREF_INHERITED(ArchiveRequest, DOMRequest)
|
||||
NS_IMPL_RELEASE_INHERITED(ArchiveRequest, DOMRequest)
|
|
@ -1,89 +0,0 @@
|
|||
/* -*- 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_archivereader_domarchiverequest_h__
|
||||
#define mozilla_dom_archivereader_domarchiverequest_h__
|
||||
|
||||
#include "mozilla/Attributes.h"
|
||||
#include "ArchiveReader.h"
|
||||
#include "DOMRequest.h"
|
||||
|
||||
#include "ArchiveReaderCommon.h"
|
||||
|
||||
namespace mozilla {
|
||||
class EventChainPreVisitor;
|
||||
} // namespace mozilla
|
||||
|
||||
BEGIN_ARCHIVEREADER_NAMESPACE
|
||||
|
||||
/**
|
||||
* This is the ArchiveRequest that handles any operation
|
||||
* related to ArchiveReader
|
||||
*/
|
||||
class ArchiveRequest : public mozilla::dom::DOMRequest
|
||||
{
|
||||
public:
|
||||
virtual JSObject* WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto) override;
|
||||
|
||||
ArchiveReader* Reader() const;
|
||||
|
||||
NS_DECL_ISUPPORTS_INHERITED
|
||||
|
||||
NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(ArchiveRequest, DOMRequest)
|
||||
|
||||
ArchiveRequest(nsPIDOMWindowInner* aWindow,
|
||||
ArchiveReader* aReader);
|
||||
|
||||
// nsIDOMEventTarget
|
||||
virtual nsresult GetEventTargetParent(
|
||||
EventChainPreVisitor& aVisitor) override;
|
||||
|
||||
public:
|
||||
// This is called by the DOMArchiveRequestEvent
|
||||
void Run();
|
||||
|
||||
// Set the types for this request
|
||||
void OpGetFilenames();
|
||||
void OpGetFile(const nsAString& aFilename);
|
||||
void OpGetFiles();
|
||||
|
||||
nsresult ReaderReady(nsTArray<RefPtr<File>>& aFileList, nsresult aStatus);
|
||||
|
||||
public: // static
|
||||
static already_AddRefed<ArchiveRequest> Create(nsPIDOMWindowInner* aOwner,
|
||||
ArchiveReader* aReader);
|
||||
|
||||
private:
|
||||
~ArchiveRequest();
|
||||
|
||||
nsresult GetFilenamesResult(JSContext* aCx,
|
||||
JS::Value* aValue,
|
||||
nsTArray<RefPtr<File>>& aFileList);
|
||||
nsresult GetFileResult(JSContext* aCx,
|
||||
JS::MutableHandle<JS::Value> aValue,
|
||||
nsTArray<RefPtr<File>>& aFileList);
|
||||
nsresult GetFilesResult(JSContext* aCx,
|
||||
JS::MutableHandle<JS::Value> aValue,
|
||||
nsTArray<RefPtr<File>>& aFileList);
|
||||
|
||||
protected:
|
||||
// The reader:
|
||||
RefPtr<ArchiveReader> mArchiveReader;
|
||||
|
||||
// The operation:
|
||||
enum {
|
||||
GetFilenames,
|
||||
GetFile,
|
||||
GetFiles
|
||||
} mOperation;
|
||||
|
||||
// The filename (needed by GetFile):
|
||||
nsString mFilename;
|
||||
};
|
||||
|
||||
END_ARCHIVEREADER_NAMESPACE
|
||||
|
||||
#endif // mozilla_dom_archivereader_domarchiverequest_h__
|
|
@ -1,215 +0,0 @@
|
|||
/* -*- 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 "ArchiveZipEvent.h"
|
||||
#include "ArchiveZipFile.h"
|
||||
|
||||
#include "nsContentUtils.h"
|
||||
#include "nsCExternalHandlerService.h"
|
||||
|
||||
#include "mozilla/UniquePtr.h"
|
||||
#include "nsIInputStream.h"
|
||||
|
||||
using namespace mozilla;
|
||||
using namespace mozilla::dom;
|
||||
|
||||
USING_ARCHIVEREADER_NAMESPACE
|
||||
|
||||
#ifndef PATH_MAX
|
||||
# define PATH_MAX 65536 // The filename length is stored in 2 bytes
|
||||
#endif
|
||||
|
||||
ArchiveZipItem::ArchiveZipItem(const char* aFilename,
|
||||
const ZipCentral& aCentralStruct,
|
||||
const nsACString& aEncoding)
|
||||
: mFilename(aFilename),
|
||||
mCentralStruct(aCentralStruct),
|
||||
mEncoding(aEncoding)
|
||||
{
|
||||
}
|
||||
|
||||
ArchiveZipItem::~ArchiveZipItem()
|
||||
{
|
||||
}
|
||||
|
||||
nsresult
|
||||
ArchiveZipItem::ConvertFilename()
|
||||
{
|
||||
if (mEncoding.IsEmpty()) {
|
||||
return NS_ERROR_FAILURE;
|
||||
}
|
||||
|
||||
nsString filenameU;
|
||||
nsresult rv = nsContentUtils::ConvertStringFromEncoding(
|
||||
mEncoding,
|
||||
mFilename, filenameU);
|
||||
NS_ENSURE_SUCCESS(rv, rv);
|
||||
|
||||
if (filenameU.IsEmpty()) {
|
||||
return NS_ERROR_FAILURE;
|
||||
}
|
||||
|
||||
mFilenameU = filenameU;
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
nsresult
|
||||
ArchiveZipItem::GetFilename(nsString& aFilename)
|
||||
{
|
||||
if (mFilenameU.IsEmpty()) {
|
||||
// Maybe this string is UTF-8:
|
||||
if (IsUTF8(mFilename, false)) {
|
||||
mFilenameU = NS_ConvertUTF8toUTF16(mFilename);
|
||||
}
|
||||
|
||||
// Let's use the enconding value for the dictionary
|
||||
else {
|
||||
nsresult rv = ConvertFilename();
|
||||
NS_ENSURE_SUCCESS(rv, rv);
|
||||
}
|
||||
}
|
||||
|
||||
aFilename = mFilenameU;
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
// From zipItem to File:
|
||||
already_AddRefed<File>
|
||||
ArchiveZipItem::GetFile(ArchiveReader* aArchiveReader)
|
||||
{
|
||||
nsString filename;
|
||||
|
||||
if (NS_FAILED(GetFilename(filename))) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
RefPtr<dom::File> file = dom::File::Create(aArchiveReader,
|
||||
new ArchiveZipBlobImpl(filename,
|
||||
NS_ConvertUTF8toUTF16(GetType()),
|
||||
StrToInt32(mCentralStruct.orglen),
|
||||
mCentralStruct, aArchiveReader->GetBlobImpl()));
|
||||
MOZ_ASSERT(file);
|
||||
return file.forget();
|
||||
}
|
||||
|
||||
uint32_t
|
||||
ArchiveZipItem::StrToInt32(const uint8_t* aStr)
|
||||
{
|
||||
return (uint32_t)( (aStr [0] << 0) |
|
||||
(aStr [1] << 8) |
|
||||
(aStr [2] << 16) |
|
||||
(aStr [3] << 24) );
|
||||
}
|
||||
|
||||
uint16_t
|
||||
ArchiveZipItem::StrToInt16(const uint8_t* aStr)
|
||||
{
|
||||
return (uint16_t) ((aStr [0]) | (aStr [1] << 8));
|
||||
}
|
||||
|
||||
// ArchiveReaderZipEvent
|
||||
|
||||
ArchiveReaderZipEvent::ArchiveReaderZipEvent(ArchiveReader* aArchiveReader,
|
||||
const nsACString& aEncoding)
|
||||
: ArchiveReaderEvent(aArchiveReader),
|
||||
mEncoding(aEncoding)
|
||||
{
|
||||
}
|
||||
|
||||
// NOTE: this runs in a different thread!!
|
||||
nsresult
|
||||
ArchiveReaderZipEvent::Exec()
|
||||
{
|
||||
uint32_t centralOffset(0);
|
||||
nsresult rv;
|
||||
|
||||
nsCOMPtr<nsIInputStream> inputStream;
|
||||
rv = mArchiveReader->GetInputStream(getter_AddRefs(inputStream));
|
||||
if (NS_FAILED(rv) || !inputStream) {
|
||||
return RunShare(NS_ERROR_UNEXPECTED);
|
||||
}
|
||||
|
||||
// From the input stream to a seekable stream
|
||||
nsCOMPtr<nsISeekableStream> seekableStream;
|
||||
seekableStream = do_QueryInterface(inputStream);
|
||||
if (!seekableStream) {
|
||||
return RunShare(NS_ERROR_UNEXPECTED);
|
||||
}
|
||||
|
||||
uint64_t size;
|
||||
rv = mArchiveReader->GetSize(&size);
|
||||
if (NS_FAILED(rv)) {
|
||||
return RunShare(NS_ERROR_UNEXPECTED);
|
||||
}
|
||||
|
||||
// Reading backward.. looking for the ZipEnd signature
|
||||
for (uint64_t curr = size - ZIPEND_SIZE; curr > 4; --curr) {
|
||||
seekableStream->Seek(nsISeekableStream::NS_SEEK_SET, curr);
|
||||
|
||||
uint8_t buffer[ZIPEND_SIZE];
|
||||
uint32_t ret;
|
||||
|
||||
rv = inputStream->Read((char*)buffer, sizeof(buffer), &ret);
|
||||
if (NS_FAILED(rv) || ret != sizeof(buffer)) {
|
||||
return RunShare(NS_ERROR_UNEXPECTED);
|
||||
}
|
||||
|
||||
// Here we are:
|
||||
if (ArchiveZipItem::StrToInt32(buffer) == ENDSIG) {
|
||||
centralOffset = ArchiveZipItem::StrToInt32(((ZipEnd*)buffer)->offset_central_dir);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// No central Offset
|
||||
if (!centralOffset || centralOffset >= size - ZIPEND_SIZE) {
|
||||
return RunShare(NS_ERROR_FAILURE);
|
||||
}
|
||||
|
||||
// Seek to the first central directory:
|
||||
seekableStream->Seek(nsISeekableStream::NS_SEEK_SET, centralOffset);
|
||||
|
||||
// For each central directory:
|
||||
while (centralOffset <= size - ZIPCENTRAL_SIZE) {
|
||||
ZipCentral centralStruct;
|
||||
uint32_t ret;
|
||||
|
||||
rv = inputStream->Read((char*)¢ralStruct, ZIPCENTRAL_SIZE, &ret);
|
||||
if (NS_FAILED(rv) || ret != ZIPCENTRAL_SIZE) {
|
||||
return RunShare(NS_ERROR_UNEXPECTED);
|
||||
}
|
||||
|
||||
uint16_t filenameLen = ArchiveZipItem::StrToInt16(centralStruct.filename_len);
|
||||
uint16_t extraLen = ArchiveZipItem::StrToInt16(centralStruct.extrafield_len);
|
||||
uint16_t commentLen = ArchiveZipItem::StrToInt16(centralStruct.commentfield_len);
|
||||
|
||||
// Point to the next item at the top of loop
|
||||
centralOffset += ZIPCENTRAL_SIZE + filenameLen + extraLen + commentLen;
|
||||
if (filenameLen == 0 || filenameLen >= PATH_MAX || centralOffset >= size) {
|
||||
return RunShare(NS_ERROR_FILE_CORRUPTED);
|
||||
}
|
||||
|
||||
// Read the name:
|
||||
auto filename = MakeUnique<char[]>(filenameLen + 1);
|
||||
rv = inputStream->Read(filename.get(), filenameLen, &ret);
|
||||
if (NS_FAILED(rv) || ret != filenameLen) {
|
||||
return RunShare(NS_ERROR_UNEXPECTED);
|
||||
}
|
||||
|
||||
filename[filenameLen] = 0;
|
||||
|
||||
// We ignore the directories:
|
||||
if (filename[filenameLen - 1] != '/') {
|
||||
mFileList.AppendElement(new ArchiveZipItem(filename.get(), centralStruct,
|
||||
mEncoding));
|
||||
}
|
||||
|
||||
// Ignore the rest
|
||||
seekableStream->Seek(nsISeekableStream::NS_SEEK_CUR, extraLen + commentLen);
|
||||
}
|
||||
|
||||
return RunShare(NS_OK);
|
||||
}
|
|
@ -1,70 +0,0 @@
|
|||
/* -*- 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_archivereader_domarchivezipevent_h__
|
||||
#define mozilla_dom_archivereader_domarchivezipevent_h__
|
||||
|
||||
#include "mozilla/Attributes.h"
|
||||
#include "ArchiveEvent.h"
|
||||
|
||||
#include "ArchiveReaderCommon.h"
|
||||
#include "zipstruct.h"
|
||||
|
||||
BEGIN_ARCHIVEREADER_NAMESPACE
|
||||
|
||||
/**
|
||||
* ArchiveZipItem - ArchiveItem for ArchiveReaderZipEvent
|
||||
*/
|
||||
class ArchiveZipItem : public ArchiveItem
|
||||
{
|
||||
public:
|
||||
ArchiveZipItem(const char* aFilename,
|
||||
const ZipCentral& aCentralStruct,
|
||||
const nsACString& aEncoding);
|
||||
protected:
|
||||
virtual ~ArchiveZipItem();
|
||||
|
||||
public:
|
||||
nsresult GetFilename(nsString& aFilename) override;
|
||||
|
||||
// From zipItem to File:
|
||||
virtual already_AddRefed<File>
|
||||
GetFile(ArchiveReader* aArchiveReader) override;
|
||||
|
||||
public: // for the event
|
||||
static uint32_t StrToInt32(const uint8_t* aStr);
|
||||
static uint16_t StrToInt16(const uint8_t* aStr);
|
||||
|
||||
private:
|
||||
nsresult ConvertFilename();
|
||||
|
||||
private: // data
|
||||
nsCString mFilename;
|
||||
|
||||
nsString mFilenameU;
|
||||
ZipCentral mCentralStruct;
|
||||
|
||||
nsCString mEncoding;
|
||||
};
|
||||
|
||||
/**
|
||||
* ArchiveReaderEvent implements the ArchiveReaderEvent for the ZIP format
|
||||
*/
|
||||
class ArchiveReaderZipEvent : public ArchiveReaderEvent
|
||||
{
|
||||
public:
|
||||
ArchiveReaderZipEvent(ArchiveReader* aArchiveReader,
|
||||
const nsACString& aEncoding);
|
||||
|
||||
nsresult Exec() override;
|
||||
|
||||
private:
|
||||
nsCString mEncoding;
|
||||
};
|
||||
|
||||
END_ARCHIVEREADER_NAMESPACE
|
||||
|
||||
#endif // mozilla_dom_archivereader_domarchivezipevent_h__
|
|
@ -1,399 +0,0 @@
|
|||
/* -*- 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 "ArchiveZipFile.h"
|
||||
#include "ArchiveZipEvent.h"
|
||||
|
||||
#include "nsIInputStream.h"
|
||||
#include "zlib.h"
|
||||
#include "mozilla/Attributes.h"
|
||||
#include "mozilla/dom/File.h"
|
||||
|
||||
using namespace mozilla::dom;
|
||||
USING_ARCHIVEREADER_NAMESPACE
|
||||
|
||||
#define ZIP_CHUNK 16384
|
||||
|
||||
/**
|
||||
* Input stream object for zip files
|
||||
*/
|
||||
class ArchiveInputStream final : public nsIInputStream,
|
||||
public nsISeekableStream
|
||||
{
|
||||
public:
|
||||
ArchiveInputStream(uint64_t aParentSize,
|
||||
nsIInputStream* aInputStream,
|
||||
nsString& aFilename,
|
||||
uint32_t aStart,
|
||||
uint32_t aLength,
|
||||
ZipCentral& aCentral)
|
||||
: mCentral(aCentral),
|
||||
mFilename(aFilename),
|
||||
mStart(aStart),
|
||||
mLength(aLength),
|
||||
mStatus(NotStarted)
|
||||
{
|
||||
// Reset the data:
|
||||
memset(&mData, 0, sizeof(mData));
|
||||
|
||||
mData.parentSize = aParentSize;
|
||||
mData.inputStream = aInputStream;
|
||||
}
|
||||
|
||||
NS_DECL_THREADSAFE_ISUPPORTS
|
||||
NS_DECL_NSIINPUTSTREAM
|
||||
NS_DECL_NSISEEKABLESTREAM
|
||||
|
||||
private:
|
||||
virtual ~ArchiveInputStream()
|
||||
{
|
||||
Close();
|
||||
}
|
||||
|
||||
nsresult Init();
|
||||
|
||||
private: // data
|
||||
ZipCentral mCentral;
|
||||
nsString mFilename;
|
||||
uint32_t mStart;
|
||||
uint32_t mLength;
|
||||
|
||||
z_stream mZs;
|
||||
|
||||
enum {
|
||||
NotStarted,
|
||||
Started,
|
||||
Done
|
||||
} mStatus;
|
||||
|
||||
struct {
|
||||
uint64_t parentSize;
|
||||
nsCOMPtr<nsIInputStream> inputStream;
|
||||
|
||||
unsigned char input[ZIP_CHUNK];
|
||||
uint32_t sizeToBeRead;
|
||||
uint32_t cursor;
|
||||
|
||||
bool compressed; // a zip file can contain stored or compressed files
|
||||
} mData;
|
||||
};
|
||||
|
||||
NS_IMPL_ISUPPORTS(ArchiveInputStream,
|
||||
nsIInputStream,
|
||||
nsISeekableStream)
|
||||
|
||||
nsresult
|
||||
ArchiveInputStream::Init()
|
||||
{
|
||||
nsresult rv;
|
||||
|
||||
memset(&mZs, 0, sizeof(z_stream));
|
||||
int zerr = inflateInit2(&mZs, -MAX_WBITS);
|
||||
if (zerr != Z_OK) {
|
||||
return NS_ERROR_OUT_OF_MEMORY;
|
||||
}
|
||||
|
||||
mData.sizeToBeRead = ArchiveZipItem::StrToInt32(mCentral.size);
|
||||
|
||||
uint32_t offset = ArchiveZipItem::StrToInt32(mCentral.localhdr_offset);
|
||||
|
||||
// The file is corrupt
|
||||
if (mData.parentSize < ZIPLOCAL_SIZE ||
|
||||
offset > mData.parentSize - ZIPLOCAL_SIZE) {
|
||||
return NS_ERROR_UNEXPECTED;
|
||||
}
|
||||
|
||||
// From the input stream to a seekable stream
|
||||
nsCOMPtr<nsISeekableStream> seekableStream;
|
||||
seekableStream = do_QueryInterface(mData.inputStream);
|
||||
if (!seekableStream) {
|
||||
return NS_ERROR_UNEXPECTED;
|
||||
}
|
||||
|
||||
// Seek + read the ZipLocal struct
|
||||
seekableStream->Seek(nsISeekableStream::NS_SEEK_SET, offset);
|
||||
uint8_t buffer[ZIPLOCAL_SIZE];
|
||||
uint32_t ret;
|
||||
|
||||
rv = mData.inputStream->Read((char*)buffer, ZIPLOCAL_SIZE, &ret);
|
||||
if (NS_FAILED(rv) || ret != ZIPLOCAL_SIZE) {
|
||||
return NS_ERROR_UNEXPECTED;
|
||||
}
|
||||
|
||||
// Signature check:
|
||||
if (ArchiveZipItem::StrToInt32(buffer) != LOCALSIG) {
|
||||
return NS_ERROR_UNEXPECTED;
|
||||
}
|
||||
|
||||
ZipLocal local;
|
||||
memcpy(&local, buffer, ZIPLOCAL_SIZE);
|
||||
|
||||
// Seek to the real data:
|
||||
offset += ZIPLOCAL_SIZE +
|
||||
ArchiveZipItem::StrToInt16(local.filename_len) +
|
||||
ArchiveZipItem::StrToInt16(local.extrafield_len);
|
||||
|
||||
// The file is corrupt if there is not enough data
|
||||
if (mData.parentSize < mData.sizeToBeRead ||
|
||||
offset > mData.parentSize - mData.sizeToBeRead) {
|
||||
return NS_ERROR_UNEXPECTED;
|
||||
}
|
||||
|
||||
// Data starts here:
|
||||
seekableStream->Seek(nsISeekableStream::NS_SEEK_SET, offset);
|
||||
|
||||
// The file is compressed or not?
|
||||
mData.compressed = (ArchiveZipItem::StrToInt16(mCentral.method) != 0);
|
||||
|
||||
// We have to skip the first mStart bytes:
|
||||
if (mStart != 0) {
|
||||
rv = Seek(NS_SEEK_SET, mStart);
|
||||
NS_ENSURE_SUCCESS(rv, rv);
|
||||
}
|
||||
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
NS_IMETHODIMP
|
||||
ArchiveInputStream::Close()
|
||||
{
|
||||
if (mStatus != NotStarted) {
|
||||
inflateEnd(&mZs);
|
||||
mStatus = NotStarted;
|
||||
}
|
||||
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
NS_IMETHODIMP
|
||||
ArchiveInputStream::Available(uint64_t* _retval)
|
||||
{
|
||||
*_retval = mLength - mData.cursor - mStart;
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
NS_IMETHODIMP
|
||||
ArchiveInputStream::Read(char* aBuffer,
|
||||
uint32_t aCount,
|
||||
uint32_t* _retval)
|
||||
{
|
||||
NS_ENSURE_ARG_POINTER(aBuffer);
|
||||
NS_ENSURE_ARG_POINTER(_retval);
|
||||
|
||||
nsresult rv;
|
||||
|
||||
// This is the first time:
|
||||
if (mStatus == NotStarted) {
|
||||
mStatus = Started;
|
||||
|
||||
rv = Init();
|
||||
if (NS_FAILED(rv)) {
|
||||
return rv;
|
||||
}
|
||||
|
||||
// Let's set avail_out to -1 so we read something from the stream.
|
||||
mZs.avail_out = (uInt)-1;
|
||||
}
|
||||
|
||||
// Nothing more can be read
|
||||
if (mStatus == Done) {
|
||||
*_retval = 0;
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
// Stored file:
|
||||
if (!mData.compressed) {
|
||||
rv = mData.inputStream->Read(aBuffer,
|
||||
(mData.sizeToBeRead > aCount ?
|
||||
aCount : mData.sizeToBeRead),
|
||||
_retval);
|
||||
if (NS_SUCCEEDED(rv)) {
|
||||
mData.sizeToBeRead -= *_retval;
|
||||
mData.cursor += *_retval;
|
||||
|
||||
if (mData.sizeToBeRead == 0) {
|
||||
mStatus = Done;
|
||||
}
|
||||
}
|
||||
|
||||
return rv;
|
||||
}
|
||||
|
||||
// We have nothing ready to be processed:
|
||||
if (mZs.avail_out != 0 && mData.sizeToBeRead != 0) {
|
||||
uint32_t ret;
|
||||
rv = mData.inputStream->Read((char*)mData.input,
|
||||
(mData.sizeToBeRead > sizeof(mData.input) ?
|
||||
sizeof(mData.input) : mData.sizeToBeRead),
|
||||
&ret);
|
||||
if (NS_FAILED(rv)) {
|
||||
return rv;
|
||||
}
|
||||
|
||||
// Terminator:
|
||||
if (ret == 0) {
|
||||
*_retval = 0;
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
mData.sizeToBeRead -= ret;
|
||||
mZs.avail_in = ret;
|
||||
mZs.next_in = mData.input;
|
||||
}
|
||||
|
||||
mZs.avail_out = aCount;
|
||||
mZs.next_out = (unsigned char*)aBuffer;
|
||||
|
||||
int ret = inflate(&mZs, mData.sizeToBeRead ? Z_NO_FLUSH : Z_FINISH);
|
||||
if (ret != Z_BUF_ERROR && ret != Z_OK && ret != Z_STREAM_END) {
|
||||
return NS_ERROR_UNEXPECTED;
|
||||
}
|
||||
|
||||
if (ret == Z_STREAM_END) {
|
||||
mStatus = Done;
|
||||
}
|
||||
|
||||
*_retval = aCount - mZs.avail_out;
|
||||
mData.cursor += *_retval;
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
NS_IMETHODIMP
|
||||
ArchiveInputStream::ReadSegments(nsWriteSegmentFun aWriter,
|
||||
void* aClosure,
|
||||
uint32_t aCount,
|
||||
uint32_t* _retval)
|
||||
{
|
||||
// don't have a buffer to read from, so this better not be called!
|
||||
NS_NOTREACHED("Consumers should be using Read()!");
|
||||
return NS_ERROR_NOT_IMPLEMENTED;
|
||||
}
|
||||
|
||||
NS_IMETHODIMP
|
||||
ArchiveInputStream::IsNonBlocking(bool* _retval)
|
||||
{
|
||||
// We are blocking
|
||||
*_retval = false;
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
NS_IMETHODIMP
|
||||
ArchiveInputStream::Seek(int32_t aWhence, int64_t aOffset)
|
||||
{
|
||||
int64_t pos = aOffset;
|
||||
|
||||
switch (aWhence) {
|
||||
case NS_SEEK_SET:
|
||||
break;
|
||||
|
||||
case NS_SEEK_CUR:
|
||||
pos += mData.cursor;
|
||||
break;
|
||||
|
||||
case NS_SEEK_END:
|
||||
pos += mLength;
|
||||
break;
|
||||
|
||||
default:
|
||||
NS_NOTREACHED("unexpected whence value");
|
||||
return NS_ERROR_UNEXPECTED;
|
||||
}
|
||||
|
||||
if (pos == int64_t(mData.cursor)) {
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
if (pos < 0 || pos >= mLength) {
|
||||
return NS_ERROR_FAILURE;
|
||||
}
|
||||
|
||||
// We have to terminate the previous operation:
|
||||
nsresult rv;
|
||||
if (mStatus != NotStarted) {
|
||||
rv = Close();
|
||||
NS_ENSURE_SUCCESS(rv, rv);
|
||||
}
|
||||
|
||||
// Reset the cursor:
|
||||
mData.cursor = 0;
|
||||
|
||||
// Note: This code is heavy but inflate does not have any seek() support:
|
||||
uint32_t ret;
|
||||
char buffer[1024];
|
||||
while (pos > 0) {
|
||||
rv = Read(buffer, pos > int64_t(sizeof(buffer)) ? sizeof(buffer) : pos, &ret);
|
||||
if (NS_FAILED(rv)) {
|
||||
return rv;
|
||||
}
|
||||
|
||||
if (ret == 0) {
|
||||
return NS_ERROR_UNEXPECTED;
|
||||
}
|
||||
|
||||
pos -= ret;
|
||||
}
|
||||
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
NS_IMETHODIMP
|
||||
ArchiveInputStream::Tell(int64_t *aResult)
|
||||
{
|
||||
*aResult = mData.cursor;
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
NS_IMETHODIMP
|
||||
ArchiveInputStream::SetEOF()
|
||||
{
|
||||
return NS_ERROR_NOT_IMPLEMENTED;
|
||||
}
|
||||
|
||||
// ArchiveZipBlobImpl
|
||||
|
||||
void
|
||||
ArchiveZipBlobImpl::GetInternalStream(nsIInputStream** aStream,
|
||||
ErrorResult& aRv)
|
||||
{
|
||||
if (mLength > INT32_MAX) {
|
||||
aRv.Throw(NS_ERROR_FAILURE);
|
||||
return;
|
||||
}
|
||||
|
||||
uint64_t size = mBlobImpl->GetSize(aRv);
|
||||
if (NS_WARN_IF(aRv.Failed())) {
|
||||
return;
|
||||
}
|
||||
|
||||
nsCOMPtr<nsIInputStream> inputStream;
|
||||
mBlobImpl->GetInternalStream(getter_AddRefs(inputStream), aRv);
|
||||
if (NS_WARN_IF(aRv.Failed())) {
|
||||
return;
|
||||
}
|
||||
|
||||
RefPtr<ArchiveInputStream> stream = new ArchiveInputStream(size,
|
||||
inputStream,
|
||||
mFilename,
|
||||
mStart,
|
||||
mLength,
|
||||
mCentral);
|
||||
|
||||
stream.forget(aStream);
|
||||
}
|
||||
|
||||
already_AddRefed<mozilla::dom::BlobImpl>
|
||||
ArchiveZipBlobImpl::CreateSlice(uint64_t aStart,
|
||||
uint64_t aLength,
|
||||
const nsAString& aContentType,
|
||||
mozilla::ErrorResult& aRv)
|
||||
{
|
||||
RefPtr<BlobImpl> impl =
|
||||
new ArchiveZipBlobImpl(mFilename, mContentType, aStart, mLength, mCentral,
|
||||
mBlobImpl);
|
||||
return impl.forget();
|
||||
}
|
||||
|
||||
NS_IMPL_ISUPPORTS_INHERITED0(ArchiveZipBlobImpl, BlobImpl)
|
|
@ -1,78 +0,0 @@
|
|||
/* -*- 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_archivereader_domarchivefile_h__
|
||||
#define mozilla_dom_archivereader_domarchivefile_h__
|
||||
|
||||
#include "mozilla/Attributes.h"
|
||||
#include "mozilla/ErrorResult.h"
|
||||
#include "mozilla/dom/BaseBlobImpl.h"
|
||||
|
||||
#include "ArchiveReader.h"
|
||||
|
||||
#include "ArchiveReaderCommon.h"
|
||||
#include "zipstruct.h"
|
||||
|
||||
BEGIN_ARCHIVEREADER_NAMESPACE
|
||||
|
||||
/**
|
||||
* ArchiveZipBlobImpl to BlobImpl
|
||||
*/
|
||||
class ArchiveZipBlobImpl : public BaseBlobImpl
|
||||
{
|
||||
public:
|
||||
NS_DECL_ISUPPORTS_INHERITED
|
||||
|
||||
ArchiveZipBlobImpl(const nsAString& aName,
|
||||
const nsAString& aContentType,
|
||||
uint64_t aLength,
|
||||
ZipCentral& aCentral,
|
||||
BlobImpl* aBlobImpl)
|
||||
: BaseBlobImpl(aName, aContentType, aLength),
|
||||
mCentral(aCentral),
|
||||
mBlobImpl(aBlobImpl),
|
||||
mFilename(aName)
|
||||
{
|
||||
MOZ_ASSERT(mBlobImpl);
|
||||
}
|
||||
|
||||
ArchiveZipBlobImpl(const nsAString& aName,
|
||||
const nsAString& aContentType,
|
||||
uint64_t aStart,
|
||||
uint64_t aLength,
|
||||
ZipCentral& aCentral,
|
||||
BlobImpl* aBlobImpl)
|
||||
: BaseBlobImpl(aContentType, aStart, aLength),
|
||||
mCentral(aCentral),
|
||||
mBlobImpl(aBlobImpl),
|
||||
mFilename(aName)
|
||||
{
|
||||
MOZ_ASSERT(mBlobImpl);
|
||||
}
|
||||
|
||||
// Overrides:
|
||||
virtual void GetInternalStream(nsIInputStream** aInputStream,
|
||||
ErrorResult& aRv) override;
|
||||
|
||||
protected:
|
||||
virtual ~ArchiveZipBlobImpl()
|
||||
{
|
||||
}
|
||||
|
||||
virtual already_AddRefed<BlobImpl>
|
||||
CreateSlice(uint64_t aStart, uint64_t aLength, const nsAString& aContentType,
|
||||
mozilla::ErrorResult& aRv) override;
|
||||
|
||||
private: // Data
|
||||
ZipCentral mCentral;
|
||||
RefPtr<BlobImpl> mBlobImpl;
|
||||
|
||||
nsString mFilename;
|
||||
};
|
||||
|
||||
END_ARCHIVEREADER_NAMESPACE
|
||||
|
||||
#endif // mozilla_dom_archivereader_domarchivefile_h__
|
|
@ -1,33 +0,0 @@
|
|||
# -*- 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/.
|
||||
|
||||
with Files("**"):
|
||||
BUG_COMPONENT = ("Core", "DOM")
|
||||
|
||||
EXPORTS.mozilla.dom.archivereader += [
|
||||
'ArchiveEvent.h',
|
||||
'ArchiveReader.h',
|
||||
'ArchiveReaderCommon.h',
|
||||
'ArchiveRequest.h',
|
||||
'ArchiveZipEvent.h',
|
||||
'ArchiveZipFile.h',
|
||||
]
|
||||
|
||||
UNIFIED_SOURCES += [
|
||||
'ArchiveEvent.cpp',
|
||||
'ArchiveReader.cpp',
|
||||
'ArchiveRequest.cpp',
|
||||
'ArchiveZipEvent.cpp',
|
||||
'ArchiveZipFile.cpp',
|
||||
]
|
||||
|
||||
LOCAL_INCLUDES += [
|
||||
'../base',
|
||||
]
|
||||
|
||||
FINAL_LIBRARY = 'xul'
|
||||
|
||||
MOCHITEST_MANIFESTS += ['test/mochitest.ini']
|
|
@ -1,27 +0,0 @@
|
|||
/**
|
||||
* Any copyright is dedicated to the Public Domain.
|
||||
* http://creativecommons.org/publicdomain/zero/1.0/
|
||||
*/
|
||||
|
||||
function runTest()
|
||||
{
|
||||
SimpleTest.waitForExplicitFinish();
|
||||
|
||||
SpecialPowers.pushPrefEnv({'set': [ ["dom.archivereader.enabled", true] ]}, function() {
|
||||
SpecialPowers.createFiles(filesToCreate(),
|
||||
function (files) {
|
||||
testSteps(files);
|
||||
},
|
||||
function (msg) {
|
||||
ok(false, "File creation error: " + msg);
|
||||
finishTest();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function finishTest()
|
||||
{
|
||||
SpecialPowers.popPrefEnv(function() {
|
||||
SimpleTest.finish();
|
||||
});
|
||||
}
|
|
@ -1,7 +0,0 @@
|
|||
[DEFAULT]
|
||||
support-files =
|
||||
helpers.js
|
||||
|
||||
[test_basic.html]
|
||||
[test_nonUnicode.html]
|
||||
[test_zip_in_zip.html]
|
|
@ -1,226 +0,0 @@
|
|||
<!--
|
||||
Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/
|
||||
-->
|
||||
<html>
|
||||
<head>
|
||||
<title>Archive Reader Test</title>
|
||||
|
||||
<script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
|
||||
<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
|
||||
|
||||
<script type="text/javascript">
|
||||
function filesToCreate() {
|
||||
var binaryString = '504B03040A00000000002E6BF14000000000000000000000000005001C00746573742F555409000337CA055039CA055075780B' +
|
||||
'000104E803000004E8030000504B03041400000008002D6BF1401780E15015000000580200000A001C00746573742F612E7478' +
|
||||
'74555409000336CA05503ACA055075780B000104E803000004E8030000CB48CDC9C95728CF2FCA49E1CA18658FB2A9C4060050' +
|
||||
'4B03040A00000000002F88EC40662E847010000000100000000A001C00746573742F622E74787455540900035A65FF4F42C505' +
|
||||
'5075780B000104E803000004E803000068656C6C6F20776F726C642C2032210A504B01021E030A00000000002E6BF140000000' +
|
||||
'000000000000000000050018000000000000001000FD4100000000746573742F555405000337CA055075780B000104E8030000' +
|
||||
'04E8030000504B01021E031400000008002D6BF1401780E15015000000580200000A0018000000000001000000B4813F000000' +
|
||||
'746573742F612E747874555405000336CA055075780B000104E803000004E8030000504B01021E030A00000000002F88EC4066' +
|
||||
'2E847010000000100000000A0018000000000001000000B48198000000746573742F622E74787455540500035A65FF4F75780B' +
|
||||
'000104E803000004E8030000504B05060000000003000300EB000000EC0000000000';
|
||||
|
||||
var binaryData = "";
|
||||
for (var i = 0, len = binaryString.length / 2; i < len; ++i) {
|
||||
var hex = binaryString[i * 2] + binaryString[i * 2 + 1];
|
||||
binaryData += String.fromCharCode(parseInt(hex,16));
|
||||
}
|
||||
|
||||
return [ {name: "fileArchiveReader.zip", data: binaryData},
|
||||
{name: "fileArchiveReader.txt", data: "Hello World"}];
|
||||
}
|
||||
|
||||
handleFinished = 0;
|
||||
function markTestDone() {
|
||||
++handleFinished;
|
||||
if (isFinished()) {
|
||||
finishTest();
|
||||
}
|
||||
}
|
||||
function isFinished() {
|
||||
return handleFinished == 6;
|
||||
}
|
||||
|
||||
function testSteps(files)
|
||||
{
|
||||
var binaryFile = files[0];
|
||||
var textFile = files[1];
|
||||
|
||||
var status;
|
||||
|
||||
// Create - wrong 1
|
||||
try {
|
||||
var r = new ArchiveReader();
|
||||
status = false;
|
||||
}
|
||||
catch(e) {
|
||||
status = true;
|
||||
}
|
||||
ok(status, "ArchiveReader() without args MUST fail");
|
||||
|
||||
// Create - wrong 2
|
||||
try {
|
||||
var r = new ArchiveReader(true);
|
||||
status = false;
|
||||
}
|
||||
catch(e) {
|
||||
status = true;
|
||||
}
|
||||
ok(status, "ArchiveReader() without a blob arg MUST fail");
|
||||
|
||||
// Create - wrong 3
|
||||
try {
|
||||
var r = new ArchiveReader("hello world");
|
||||
status = false;
|
||||
}
|
||||
catch(e) {
|
||||
status = true;
|
||||
}
|
||||
ok(status, "ArchiveReader() without a blob arg MUST fail");
|
||||
|
||||
// Create - good! (but with a text file)
|
||||
var rt = new ArchiveReader(textFile);
|
||||
isnot(rt, null, "ArchiveReader cannot be null");
|
||||
|
||||
// GetFilename
|
||||
var handle = rt.getFilenames();
|
||||
isnot(handle, null, "ArchiveReader.getFilenames() cannot be null");
|
||||
handle.onsuccess = function() {
|
||||
ok(false, "ArchiveReader.getFilenames() should return a 'failure' if the input file is not a zip");
|
||||
markTestDone();
|
||||
}
|
||||
handle.onerror = function() {
|
||||
ok(true, "ArchiveReader.getFilenames() should return a 'error' if the input file is a zip file");
|
||||
is(this.reader, rt, "ArchiveRequest.reader == ArchiveReader");
|
||||
markTestDone();
|
||||
}
|
||||
|
||||
// Create - good!
|
||||
var r = new ArchiveReader(binaryFile);
|
||||
isnot(r, null, "ArchiveReader cannot be null");
|
||||
|
||||
// GetFilename
|
||||
handle = r.getFilenames();
|
||||
isnot(handle, null, "ArchiveReader.getFilenames() cannot be null");
|
||||
handle.onsuccess = function() {
|
||||
ok(true, "ArchiveReader.getFilenames() should return a 'success'");
|
||||
is(this.result instanceof Array, true, "ArchiveReader.getFilenames() should return an array");
|
||||
is(this.result.length, 2, "ArchiveReader.getFilenames(): the array contains 2 items");
|
||||
is(this.result[0], "test/a.txt", "ArchiveReader.getFilenames(): first file is 'test/a.txt'");
|
||||
is(this.result[1], "test/b.txt", "ArchiveReader.getFilenames(): second file is 'test/b.txt'");
|
||||
ok(this.reader, r, "ArchiveRequest.reader should be == ArchiveReader");
|
||||
markTestDone();
|
||||
}
|
||||
handle.onerror = function() {
|
||||
ok(false, "ArchiveReader.getFilenames() should not return any 'error'");
|
||||
markTestDone();
|
||||
}
|
||||
|
||||
// GetFile - wrong (no args)
|
||||
try {
|
||||
r.getFile();
|
||||
status = false;
|
||||
}
|
||||
catch(e) {
|
||||
status = true;
|
||||
}
|
||||
ok(status, "ArchiveReader.getFile() without args fail");
|
||||
|
||||
var handle2 = r.getFile("hello world");
|
||||
isnot(handle2, null, "ArchiveReader.getFile() cannot be null");
|
||||
handle2.onsuccess = function() {
|
||||
ok(false, "ArchiveReader.getFile('unknown file') should not return a 'success'");
|
||||
markTestDone();
|
||||
}
|
||||
handle2.onerror = function() {
|
||||
ok(true, "ArchiveReader.getFile('unknown file') should return an 'error'");
|
||||
ok(this.reader, r, "ArchiveRequest.reader should be == ArchiveReader");
|
||||
markTestDone();
|
||||
}
|
||||
|
||||
var handle3 = r.getFile("test/b.txt");
|
||||
isnot(handle3, null, "ArchiveReader.getFile() cannot be null");
|
||||
handle3.onsuccess = function() {
|
||||
ok(true, "ArchiveReader.getFile('test/b.txt') should return a 'success'");
|
||||
ok(this.reader, r, "ArchiveRequest.reader should be == ArchiveReader");
|
||||
is(this.result.name, "test/b.txt", "ArchiveReader.getFile('test/b.txt') the name MUST be 'test/b.txt'");
|
||||
is(this.result.type, "text/plain", "ArchiveReader.getFile('test/b.txt') the type MUST be 'text/plain'");
|
||||
|
||||
var fr = new FileReader();
|
||||
fr.readAsText(this.result);
|
||||
fr.onerror = function() {
|
||||
ok(false, "ArchiveReader + FileReader should work!");
|
||||
markTestDone();
|
||||
}
|
||||
fr.onload = function(event) {
|
||||
is(event.target.result, "hello world, 2!\n", "ArchiveReader + FileReader are working together.");
|
||||
markTestDone();
|
||||
}
|
||||
}
|
||||
|
||||
handle3.onerror = function() {
|
||||
ok(false, "ArchiveReader.getFile('test/b.txt') should not return an 'error'");
|
||||
markTestDone();
|
||||
}
|
||||
|
||||
var handle4 = r.getFile("test/a.txt");
|
||||
isnot(handle4, null, "ArchiveReader.getFile() cannot be null");
|
||||
handle4.onsuccess = function() {
|
||||
ok(true, "ArchiveReader.getFile('test/a.txt') should return a 'success'");
|
||||
ok(this.reader, r, "ArchiveRequest.reader should be == ArchiveReader");
|
||||
is(this.result.name, "test/a.txt", "ArchiveReader.getFile('test/a.txt') the name MUST be 'test/a.txt'");
|
||||
is(this.result.type, "text/plain", "ArchiveReader.getFile('test/a.txt') the type MUST be 'text/plain'");
|
||||
|
||||
var fr = new FileReader();
|
||||
fr.readAsText(this.result);
|
||||
fr.onerror = function() {
|
||||
ok(false, "ArchiveReader + FileReader should work!");
|
||||
markTestDone();
|
||||
}
|
||||
fr.onload = function(event) {
|
||||
is(event.target.result.length, 600, "ArchiveReader + FileReader are working with a compress data");
|
||||
var p = event.target.result.trim().split('\n');
|
||||
is(p.length, 50, "ArchiveReader + FileReader are working with a compress data");
|
||||
|
||||
for (var i = 0; i < p.length; ++i)
|
||||
is(p[i], "hello world", "ArchiveReader + FileReader are working with a compress data");
|
||||
markTestDone();
|
||||
}
|
||||
}
|
||||
handle4.onerror = function() {
|
||||
ok(false, "ArchiveReader.getFile('test/a.txt') should not return an 'error'");
|
||||
markTestDone();
|
||||
}
|
||||
|
||||
var handle5 = r.getFiles();
|
||||
isnot(handle5, null, "ArchiveReader.getFiles() cannot be null");
|
||||
handle5.onsuccess = function() {
|
||||
ok(true, "ArchiveReader.getFiles() should return a 'success'");
|
||||
ok(this.reader, r, "ArchiveRequest.reader should be == ArchiveReader");
|
||||
|
||||
is(this.result.length, 2, "ArchiveReader.getFilenames(): the array contains 2 items");
|
||||
is(this.result[0].name, "test/a.txt", "ArchiveReader.getFilenames(): first file is 'test/a.txt'");
|
||||
is(this.result[1].name, "test/b.txt", "ArchiveReader.getFilenames(): second file is 'test/b.txt'");
|
||||
is(this.result[0].type, "text/plain", "ArchiveReader.getFile('test/a.txt') the type MUST be 'text/plain'");
|
||||
is(this.result[1].type, "text/plain", "ArchiveReader.getFile('test/a.txt') the type MUST be 'text/plain'");
|
||||
|
||||
markTestDone();
|
||||
}
|
||||
handle5.onerror = function() {
|
||||
ok(false, "ArchiveReader.getFiles() should not return an 'error'");
|
||||
markTestDone();
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
<script type="text/javascript" src="helpers.js"></script>
|
||||
</head>
|
||||
|
||||
<body onload="runTest();">
|
||||
<p id="display">
|
||||
</p>
|
||||
</body>
|
||||
|
||||
</html>
|
|
@ -1,76 +0,0 @@
|
|||
<!--
|
||||
Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/
|
||||
-->
|
||||
<html>
|
||||
<head>
|
||||
<title>Archive Reader Non-Unicode Test</title>
|
||||
|
||||
<script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
|
||||
<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
|
||||
|
||||
<script type="text/javascript">
|
||||
function filesToCreate() {
|
||||
var binaryData = "";
|
||||
for (var i = 0, len = binaryString.length / 2; i < len; ++i) {
|
||||
var hex = binaryString[i * 2] + binaryString[i * 2 + 1];
|
||||
binaryData += String.fromCharCode(parseInt(hex,16));
|
||||
}
|
||||
return [ {name: "fileArchiveReader_nonUnicode.zip", data: binaryData} ];
|
||||
}
|
||||
|
||||
function test1(binaryFile)
|
||||
{
|
||||
var r = new ArchiveReader(binaryFile, { encoding: "ISO-8859-1" });
|
||||
isnot(r, null, "ArchiveReader cannot be null");
|
||||
|
||||
// GetFilename
|
||||
var handle = r.getFilenames();
|
||||
isnot(handle, null, "ArchiveReader.getFilenames() cannot be null");
|
||||
handle.onsuccess = function() {
|
||||
ok(true, "ArchiveReader.getFilenames() should return a 'success'");
|
||||
is(this.result instanceof Array, true, "ArchiveReader.getFilenames() should return an array");
|
||||
is(this.result.length, 1, "ArchiveReader.getFilenames(): the array contains 1 item");
|
||||
ok(this.reader, r, "ArchiveRequest.reader should be == ArchiveReader");
|
||||
dump('Content: ' + this.result[0] + '\n');
|
||||
test2(binaryFile);
|
||||
}
|
||||
}
|
||||
|
||||
function test2(binaryFile)
|
||||
{
|
||||
try {
|
||||
new ArchiveReader(binaryFile, { encoding: "random stuff" });
|
||||
ok(false, "Should have thrown for bogus encoding label.");
|
||||
} catch (e) {
|
||||
ok(e instanceof RangeError, "Expected a RangeError");
|
||||
}
|
||||
finishTest();
|
||||
}
|
||||
|
||||
function testSteps(files)
|
||||
{
|
||||
test1(files[0]);
|
||||
}
|
||||
|
||||
|
||||
</script>
|
||||
<script type="text/javascript" src="helpers.js"></script>
|
||||
|
||||
</head>
|
||||
|
||||
<body onload="runTest();">
|
||||
<p id="display">
|
||||
</p>
|
||||
<script type="text/javascript">
|
||||
var binaryString = '' +
|
||||
'504B0304140000000000255D094100000000000000000000000002000000912F504B03040A0000000000285D09416BB50A5A' +
|
||||
'010000000100000007000000912F9B2E747874D8504B01023F00140000000000255D09410000000000000000000000000200' +
|
||||
'24000000000000001000000000000000912F0A002000000000000100180062505F1A1376CD0162505F1A1376CD01FE3F8D59' +
|
||||
'1176CD01504B01023F000A0000000000285D09416BB50A5A0100000001000000070024000000000000002000000020000000' +
|
||||
'912F9B2E7478740A0020000000000001001800565EF41D1376CD0107BD73631176CD0107BD73631176CD01504B0506000000' +
|
||||
'0002000200AD000000460000000000';
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
|
@ -1,109 +0,0 @@
|
|||
<!--
|
||||
Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/
|
||||
-->
|
||||
<html>
|
||||
<head>
|
||||
<title>Archive Reader Zip-In-Zip Test</title>
|
||||
|
||||
<script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
|
||||
<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
|
||||
|
||||
<script type="text/javascript">
|
||||
function filesToCreate() {
|
||||
var binaryData = "";
|
||||
for (var i = 0, len = binaryString.length / 2; i < len; ++i) {
|
||||
var hex = binaryString[i * 2] + binaryString[i * 2 + 1];
|
||||
binaryData += String.fromCharCode(parseInt(hex,16));
|
||||
}
|
||||
return [ {name: "fileArchiveReader_42.zip", data: binaryData} ];
|
||||
}
|
||||
|
||||
function testSteps(files)
|
||||
{
|
||||
var binaryFile = files[0];
|
||||
|
||||
// The input is 4 nested zip archives:
|
||||
doLoop(binaryFile, 4);
|
||||
}
|
||||
|
||||
function doLoop(blob, loop)
|
||||
{
|
||||
var r = new ArchiveReader(blob);
|
||||
isnot(r, null, "ArchiveReader cannot be null");
|
||||
|
||||
// GetFilename
|
||||
var handle = r.getFilenames();
|
||||
isnot(handle, null, "ArchiveReader.getFilenames() cannot be null");
|
||||
handle.onsuccess = function() {
|
||||
ok(true, "ArchiveReader.getFilenames() should return a 'success'");
|
||||
is(this.result instanceof Array, true, "ArchiveReader.getFilenames() should return an array");
|
||||
is(this.result.length, 1, "ArchiveReader.getFilenames(): the array contains 1 item");
|
||||
ok(this.reader, r, "ArchiveRequest.reader should be == ArchiveReader");
|
||||
|
||||
dump('Content:\n');
|
||||
for (var i = 0; i < this.result.length; ++i)
|
||||
dump(' * ' + this.result[i] + '\n');
|
||||
|
||||
var handle = r.getFile(this.result[0]);
|
||||
handle.onerror = function() {
|
||||
ok(false, "ArchiveReader.getFile() should not return any 'error'");
|
||||
}
|
||||
handle.onsuccess = function() {
|
||||
ok(true, "ArchiveReader.getFilenames() should return a 'success'");
|
||||
ok(this.reader, r, "ArchiveRequest.reader should be == ArchiveReader");
|
||||
|
||||
if (loop > 0)
|
||||
doLoop(this.result, loop - 1);
|
||||
else
|
||||
doLastLoop(this.result);
|
||||
}
|
||||
}
|
||||
handle.onerror = function() {
|
||||
ok(false, "ArchiveReader.getFilenames() should not return any 'error'");
|
||||
}
|
||||
}
|
||||
|
||||
function doLastLoop(blob)
|
||||
{
|
||||
ok(blob.size == 262144, "The last file size is wrong");
|
||||
ok(blob.name == 'empty.dat', "The last filename is wrong");
|
||||
finishTest();
|
||||
}
|
||||
|
||||
</script>
|
||||
<script type="text/javascript" src="helpers.js"></script>
|
||||
|
||||
</head>
|
||||
|
||||
<body onload="runTest();">
|
||||
<p id="display">
|
||||
</p>
|
||||
<script type="text/javascript">
|
||||
var binaryString = '' +
|
||||
'504B03040A0000000000B0620E415F715F15970300009703000005001C00642E7A69705554090003AC262A50AC262A507578' +
|
||||
'0B000104E803000004E8030000504B03040A0000000000B0620E41CFE25F1EF7020000F702000005001C00632E7A69705554' +
|
||||
'090003AC262A50AC262A5075780B000104E803000004E8030000504B03040A0000000000B0620E4107D702A4570200005702' +
|
||||
'000005001C00622E7A69705554090003AC262A50AC262A5075780B000104E803000004E8030000504B03040A0000000000B0' +
|
||||
'620E417E45286DB7010000B701000005001C00612E7A69705554090003AC262A50AC262A5075780B000104E803000004E803' +
|
||||
'0000504B0304140000000800F7610E41784909B70F0100000000040009001C00656D7074792E646174555409000351252A50' +
|
||||
'57252A5075780B000104E803000004E8030000EDC13101000000C2A0FEA9E76D07A000000000000000000000000000000000' +
|
||||
'0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000' +
|
||||
'0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000' +
|
||||
'0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000' +
|
||||
'0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000' +
|
||||
'0000000000000000000000000000000000000000000000000000000000000000000000000000DE00504B01021E0314000000' +
|
||||
'0800F7610E41784909B70F01000000000400090018000000000001000000B48100000000656D7074792E6461745554050003' +
|
||||
'51252A5075780B000104E803000004E8030000504B050600000000010001004F000000520100000000504B01021E030A0000' +
|
||||
'000000B0620E417E45286DB7010000B7010000050018000000000000000000B48100000000612E7A69705554050003AC262A' +
|
||||
'5075780B000104E803000004E8030000504B050600000000010001004B000000F60100000000504B01021E030A0000000000' +
|
||||
'B0620E4107D702A45702000057020000050018000000000000000000B48100000000622E7A69705554050003AC262A507578' +
|
||||
'0B000104E803000004E8030000504B050600000000010001004B000000960200000000504B01021E030A0000000000B0620E' +
|
||||
'41CFE25F1EF7020000F7020000050018000000000000000000B48100000000632E7A69705554050003AC262A5075780B0001' +
|
||||
'04E803000004E8030000504B050600000000010001004B000000360300000000504B01021E030A0000000000B0620E415F71' +
|
||||
'5F159703000097030000050018000000000000000000B48100000000642E7A69705554050003AC262A5075780B000104E803' +
|
||||
'000004E8030000504B050600000000010001004B000000D60300000000';
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
|
@ -0,0 +1,62 @@
|
|||
/* 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 { classes: Cc, interfaces: Ci, utils: Cu } = Components;
|
||||
|
||||
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||
Cu.import('resource://gre/modules/Services.jsm');
|
||||
|
||||
const BASE_PREF = "dom.ipc.processCount"
|
||||
const PREF_BRANCH = BASE_PREF + ".";
|
||||
|
||||
// Utilities:
|
||||
function getMaxContentParents(processType) {
|
||||
let maxContentParents = -1;
|
||||
try {
|
||||
maxContentParents = Services.prefs.getIntPref(PREF_BRANCH + processType);
|
||||
} catch (e) {
|
||||
// Pref probably didn't exist, get the default number of processes.
|
||||
try {
|
||||
maxContentParents = Services.prefs.getIntPref(BASE_PREF);
|
||||
} catch (e) {
|
||||
// No prefs? That's odd, use only one process.
|
||||
maxContentParents = 1;
|
||||
}
|
||||
}
|
||||
|
||||
return maxContentParents;
|
||||
}
|
||||
|
||||
// Fills up aProcesses until max and then selects randomly from the available
|
||||
// ones.
|
||||
function RandomSelector() {
|
||||
}
|
||||
|
||||
RandomSelector.prototype = {
|
||||
classID: Components.ID("{c616fcfd-9737-41f1-aa74-cee72a38f91b}"),
|
||||
QueryInterface: XPCOMUtils.generateQI([Ci.nsIContentProcessProvider]),
|
||||
|
||||
provideProcess(aType, aOpener, aProcesses, aCount) {
|
||||
let maxContentParents = getMaxContentParents(aType);
|
||||
if (aCount < maxContentParents) {
|
||||
return Ci.nsIContentProcessProvider.NEW_PROCESS;
|
||||
}
|
||||
|
||||
let startIdx = Math.floor(Math.random() * maxContentParents);
|
||||
let curIdx = startIdx;
|
||||
|
||||
do {
|
||||
if (aProcesses[curIdx].opener === aOpener) {
|
||||
return curIdx;
|
||||
}
|
||||
|
||||
curIdx = (curIdx + 1) % maxContentParents;
|
||||
} while (curIdx !== startIdx);
|
||||
|
||||
return Ci.nsIContentProcessProvider.NEW_PROCESS;
|
||||
},
|
||||
};
|
||||
|
||||
var components = [RandomSelector];
|
||||
this.NSGetFactory = XPCOMUtils.generateNSGetFactory(components);
|
|
@ -0,0 +1,2 @@
|
|||
component {c616fcfd-9737-41f1-aa74-cee72a38f91b} ProcessSelector.js
|
||||
contract @mozilla.org/ipc/processselector;1 {c616fcfd-9737-41f1-aa74-cee72a38f91b}
|
|
@ -71,6 +71,11 @@ uint32_t TimeoutManager::sNestingLevel = 0;
|
|||
|
||||
namespace {
|
||||
|
||||
// The maximum number of timer callbacks we will try to run in a single event
|
||||
// loop runnable.
|
||||
#define DEFAULT_TARGET_MAX_CONSECUTIVE_CALLBACKS 5
|
||||
uint32_t gTargetMaxConsecutiveCallbacks;
|
||||
|
||||
// The number of queued runnables within the TabGroup ThrottledEventQueue
|
||||
// at which to begin applying back pressure to the window.
|
||||
#define DEFAULT_THROTTLED_EVENT_QUEUE_BACK_PRESSURE 5000
|
||||
|
@ -182,6 +187,10 @@ TimeoutManager::Initialize()
|
|||
Preferences::AddUintVarCache(&gBackPressureDelayMinimumMS,
|
||||
"dom.timeout.back_pressure_delay_minimum_ms",
|
||||
DEFAULT_BACK_PRESSURE_DELAY_MINIMUM_MS);
|
||||
|
||||
Preferences::AddUintVarCache(&gTargetMaxConsecutiveCallbacks,
|
||||
"dom.timeout.max_consecutive_callbacks",
|
||||
DEFAULT_TARGET_MAX_CONSECUTIVE_CALLBACKS);
|
||||
}
|
||||
|
||||
uint32_t
|
||||
|
@ -444,6 +453,10 @@ TimeoutManager::RunTimeout(Timeout* aTimeout)
|
|||
mTrackingTimeouts,
|
||||
nullptr,
|
||||
nullptr);
|
||||
|
||||
uint32_t numTimersToRun = 0;
|
||||
bool targetTimerSeen = false;
|
||||
|
||||
while (true) {
|
||||
Timeout* timeout = expiredIter.Next();
|
||||
if (!timeout || timeout->When() > deadline) {
|
||||
|
@ -461,19 +474,29 @@ TimeoutManager::RunTimeout(Timeout* aTimeout)
|
|||
last_expired_tracking_timeout = timeout;
|
||||
}
|
||||
|
||||
// Run available timers until we see our target timer. After
|
||||
// that, however, stop coalescing timers so we can yield the
|
||||
// main thread. Further timers that are ready will get picked
|
||||
// up by their own nsITimer runnables when they execute.
|
||||
// Note that we have seen our target timer. This means we can now
|
||||
// stop processing timers once we hit our threshold below.
|
||||
if (timeout == aTimeout) {
|
||||
targetTimerSeen = true;
|
||||
}
|
||||
|
||||
// Run only a limited number of timers based on the configured
|
||||
// maximum. Note, we must always run our target timer however.
|
||||
// Further timers that are ready will get picked up by their own
|
||||
// nsITimer runnables when they execute.
|
||||
//
|
||||
// For chrome windows, however, we do coalesce all timers and
|
||||
// do not yield the main thread. This is partly because we
|
||||
// trust chrome windows not to misbehave and partly because a
|
||||
// number of browser chrome tests have races that depend on this
|
||||
// coalescing.
|
||||
if (timeout == aTimeout && !mWindow.IsChromeWindow()) {
|
||||
if (targetTimerSeen &&
|
||||
numTimersToRun >= gTargetMaxConsecutiveCallbacks &&
|
||||
!mWindow.IsChromeWindow()) {
|
||||
break;
|
||||
}
|
||||
|
||||
numTimersToRun += 1;
|
||||
}
|
||||
|
||||
expiredIter.UpdateIterator();
|
||||
|
|
|
@ -62,6 +62,12 @@ attribute OfflineResourceList.onupdateready
|
|||
attribute OfflineResourceList.oncached
|
||||
attribute OfflineResourceList.onobsolete
|
||||
|
||||
// Non-standard IndexedDB API
|
||||
method IDBDatabase.createMutableFile
|
||||
method IDBDatabase.mozCreateFileHandle
|
||||
method IDBMutableFile.open
|
||||
method IDBMutableFile.getFile
|
||||
|
||||
// DataTransfer API (gecko-only methods)
|
||||
method DataTransfer.addElement
|
||||
attribute DataTransfer.mozItemCount
|
||||
|
|
|
@ -401,6 +401,8 @@ EXTRA_COMPONENTS += [
|
|||
'contentAreaDropListener.manifest',
|
||||
'messageWakeupService.js',
|
||||
'messageWakeupService.manifest',
|
||||
'ProcessSelector.js',
|
||||
'ProcessSelector.manifest',
|
||||
'SlowScriptDebug.js',
|
||||
'SlowScriptDebug.manifest',
|
||||
]
|
||||
|
|
|
@ -1602,7 +1602,7 @@ nsScriptLoader::ProcessScriptElement(nsIScriptElement *aElement)
|
|||
}
|
||||
return false;
|
||||
}
|
||||
if (!aElement->GetParserCreated() && !request->IsModuleRequest()) {
|
||||
if (!aElement->GetParserCreated()) {
|
||||
// Violate the HTML5 spec in order to make LABjs and the "order" plug-in
|
||||
// for RequireJS work with their Gecko-sniffed code path. See
|
||||
// http://lists.w3.org/Archives/Public/public-html/2010Oct/0088.html
|
||||
|
|
|
@ -1281,6 +1281,10 @@ nsTreeSanitizer::SanitizeURL(mozilla::dom::Element* aElement,
|
|||
static const char* kWhitespace = "\n\r\t\b";
|
||||
const nsAString& v =
|
||||
nsContentUtils::TrimCharsInSet(kWhitespace, value);
|
||||
// Fragment-only url cannot be harmful.
|
||||
if (!v.IsEmpty() && v.First() == u'#') {
|
||||
return false;
|
||||
}
|
||||
|
||||
nsIScriptSecurityManager* secMan = nsContentUtils::GetSecurityManager();
|
||||
uint32_t flags = nsIScriptSecurityManager::DISALLOW_INHERIT_PRINCIPAL;
|
||||
|
|
|
@ -4,8 +4,6 @@
|
|||
*/
|
||||
|
||||
var testGenerator = testSteps();
|
||||
var archiveReaderEnabled = false;
|
||||
|
||||
// The test js is shared between xpcshell (which has no SpecialPowers object)
|
||||
// and content mochitests (where the |Components| object is accessible only as
|
||||
// SpecialPowers.Components). Expose Components if necessary here to make things
|
||||
|
@ -75,7 +73,6 @@ function* testHarnessSteps() {
|
|||
"set": [
|
||||
["dom.indexedDB.testing", true],
|
||||
["dom.indexedDB.experimental", true],
|
||||
["dom.archivereader.enabled", true]
|
||||
]
|
||||
},
|
||||
nextTestHarnessStep
|
||||
|
|
|
@ -127,7 +127,6 @@ support-files =
|
|||
[test_autoIncrement.html]
|
||||
[test_autoIncrement_indexes.html]
|
||||
[test_bfcache.html]
|
||||
[test_blob_archive.html]
|
||||
[test_blob_file_backed.html]
|
||||
[test_blob_simple.html]
|
||||
[test_blob_worker_crash.html]
|
||||
|
|
|
@ -1,126 +0,0 @@
|
|||
<!--
|
||||
Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/
|
||||
-->
|
||||
<html>
|
||||
<head>
|
||||
<title>Indexed Database Property Test</title>
|
||||
|
||||
<script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
|
||||
<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
|
||||
|
||||
<script type="text/javascript">
|
||||
|
||||
function* testSteps()
|
||||
{
|
||||
const BLOB_DATA =
|
||||
"504B03040A00000000002E6BF14000000000000000000000000005001C00746573742F" +
|
||||
"555409000337CA055039CA055075780B000104E803000004E8030000504B0304140000" +
|
||||
"0008002D6BF1401780E15015000000580200000A001C00746573742F612E7478745554" +
|
||||
"09000336CA05503ACA055075780B000104E803000004E8030000CB48CDC9C95728CF2F" +
|
||||
"CA49E1CA18658FB2A9C40600504B03040A00000000002F88EC40662E84701000000010" +
|
||||
"0000000A001C00746573742F622E74787455540900035A65FF4F42C5055075780B0001" +
|
||||
"04E803000004E803000068656C6C6F20776F726C642C2032210A504B01021E030A0000" +
|
||||
"0000002E6BF140000000000000000000000000050018000000000000001000FD410000" +
|
||||
"0000746573742F555405000337CA055075780B000104E803000004E8030000504B0102" +
|
||||
"1E031400000008002D6BF1401780E15015000000580200000A00180000000000010000" +
|
||||
"00B4813F000000746573742F612E747874555405000336CA055075780B000104E80300" +
|
||||
"0004E8030000504B01021E030A00000000002F88EC40662E847010000000100000000A" +
|
||||
"0018000000000001000000B48198000000746573742F622E74787455540500035A65FF" +
|
||||
"4F75780B000104E803000004E8030000504B05060000000003000300EB000000EC0000" +
|
||||
"000000";
|
||||
|
||||
const TEST_FILE_1 = "test/a.txt";
|
||||
const TEST_FILE_2 = "test/b.txt";
|
||||
|
||||
let TEST_FILE_1_CONTENTS = "";
|
||||
for (let i = 0; i < 50; i++) {
|
||||
TEST_FILE_1_CONTENTS += "hello world\n";
|
||||
}
|
||||
const TEST_FILE_2_CONTENTS = "hello world, 2!\n";
|
||||
|
||||
let binaryData = new Uint8Array(BLOB_DATA.length / 2);
|
||||
for (let i = 0, len = BLOB_DATA.length / 2; i < len; i++) {
|
||||
let hex = BLOB_DATA[i * 2] + BLOB_DATA[i * 2 + 1];
|
||||
binaryData[i] = parseInt(hex, 16);
|
||||
}
|
||||
|
||||
let request = indexedDB.open(window.location.pathname, 1);
|
||||
request.onerror = errorHandler;
|
||||
request.onupgradeneeded = grabEventAndContinueHandler;
|
||||
request.onsuccess = unexpectedSuccessHandler;
|
||||
let event = yield undefined;
|
||||
|
||||
let db = event.target.result;
|
||||
db.onerror = errorHandler;
|
||||
|
||||
let objectStore = db.createObjectStore("foo", { autoIncrement: true });
|
||||
let index = objectStore.createIndex("foo", "index");
|
||||
|
||||
request.onsuccess = grabEventAndContinueHandler;
|
||||
event = yield undefined;
|
||||
|
||||
let data = new Blob([binaryData]);
|
||||
|
||||
objectStore = db.transaction("foo", "readwrite").objectStore("foo");
|
||||
objectStore.add(data).onsuccess = grabEventAndContinueHandler;
|
||||
event = yield undefined;
|
||||
|
||||
let key = event.target.result;
|
||||
|
||||
objectStore = db.transaction("foo").objectStore("foo");
|
||||
objectStore.get(key).onsuccess = grabEventAndContinueHandler;
|
||||
event = yield undefined;
|
||||
|
||||
let archiveReader = new ArchiveReader(event.target.result);
|
||||
ok(archiveReader, "Got an ArchiveReader");
|
||||
|
||||
request = archiveReader.getFilenames();
|
||||
request.onsuccess = grabEventAndContinueHandler;
|
||||
request.onerror = errorHandler;
|
||||
event = yield undefined;
|
||||
|
||||
is(event.target.result.length, 2, "Got 2 archive items");
|
||||
is(event.target.result[0], TEST_FILE_1,
|
||||
"First file is '" + TEST_FILE_1 + "'");
|
||||
is(event.target.result[1], TEST_FILE_2,
|
||||
"Second file is '" + TEST_FILE_2 + "'");
|
||||
|
||||
request = archiveReader.getFile(TEST_FILE_1);
|
||||
request.onsuccess = grabEventAndContinueHandler;
|
||||
request.onerror = errorHandler;
|
||||
event = yield undefined;
|
||||
|
||||
let fileReader = new FileReader();
|
||||
fileReader.readAsText(event.target.result);
|
||||
fileReader.onload = grabEventAndContinueHandler;
|
||||
fileReader.onerror = errorHandler;
|
||||
event = yield undefined;
|
||||
|
||||
// Don't use is() because it prints out 100 lines of text...
|
||||
ok(event.target.result == TEST_FILE_1_CONTENTS, "Correct text");
|
||||
|
||||
request = archiveReader.getFile(TEST_FILE_2);
|
||||
request.onsuccess = grabEventAndContinueHandler;
|
||||
request.onerror = errorHandler;
|
||||
event = yield undefined;
|
||||
|
||||
fileReader = new FileReader();
|
||||
fileReader.readAsText(event.target.result);
|
||||
fileReader.onload = grabEventAndContinueHandler;
|
||||
fileReader.onerror = errorHandler;
|
||||
event = yield undefined;
|
||||
|
||||
// Don't use is() because it prints out a newline...
|
||||
ok(event.target.result == TEST_FILE_2_CONTENTS, "Correct text");
|
||||
|
||||
finishTest();
|
||||
}
|
||||
</script>
|
||||
<script type="text/javascript" src="helpers.js"></script>
|
||||
|
||||
</head>
|
||||
|
||||
<body onload="runTest();"></body>
|
||||
|
||||
</html>
|
|
@ -11,6 +11,7 @@ XPIDL_SOURCES += [
|
|||
'nsIContentPermissionPrompt.idl',
|
||||
'nsIContentPrefService.idl',
|
||||
'nsIContentPrefService2.idl',
|
||||
'nsIContentProcess.idl',
|
||||
'nsIContentURIGrouper.idl',
|
||||
'nsIDOMChromeWindow.idl',
|
||||
'nsIDOMClientRect.idl',
|
||||
|
|
|
@ -0,0 +1,53 @@
|
|||
/* 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 nsIDOMElement;
|
||||
interface nsIMessageSender;
|
||||
interface nsIURI;
|
||||
|
||||
[scriptable, builtinclass, uuid(456f58be-29dd-4973-885b-95aece1c9a8a)]
|
||||
interface nsIContentProcessInfo : nsISupports
|
||||
{
|
||||
/**
|
||||
* Is this content process alive?
|
||||
*/
|
||||
readonly attribute boolean isAlive;
|
||||
|
||||
/**
|
||||
* The content process's PID.
|
||||
* Throws if the process is not alive.
|
||||
*/
|
||||
readonly attribute int32_t processId;
|
||||
|
||||
/**
|
||||
* This content process's opener.
|
||||
*/
|
||||
readonly attribute nsIContentProcessInfo opener;
|
||||
|
||||
/**
|
||||
* The process manager for this ContentParent (so a process message manager
|
||||
* as opposed to a frame message manager.
|
||||
*/
|
||||
readonly attribute nsIMessageSender messageManager;
|
||||
};
|
||||
|
||||
[scriptable, uuid(83ffb063-5f65-4c45-ae07-3f553e0809bb)]
|
||||
interface nsIContentProcessProvider : nsISupports
|
||||
{
|
||||
/**
|
||||
* Return this from provideProcess to create a new process.
|
||||
*/
|
||||
const int32_t NEW_PROCESS = -1;
|
||||
|
||||
/**
|
||||
* Given aAliveProcesses (with an opener aOpener), choose which process of
|
||||
* aType to use. Return nsIContentProcessProvider.NEW_PROCESS to ask the
|
||||
* caller to create a new content process.
|
||||
*/
|
||||
int32_t provideProcess(in AString aType, in nsIContentProcessInfo aOpener,
|
||||
[array, size_is(aCount)] in nsIContentProcessInfo aAliveProcesses,
|
||||
in uint32_t aCount);
|
||||
};
|
|
@ -112,6 +112,7 @@
|
|||
#include "nsIAlertsService.h"
|
||||
#include "nsIClipboard.h"
|
||||
#include "nsContentPermissionHelper.h"
|
||||
#include "nsIContentProcess.h"
|
||||
#include "nsICycleCollectorListener.h"
|
||||
#include "nsIDocShellTreeOwner.h"
|
||||
#include "nsIDocument.h"
|
||||
|
@ -435,6 +436,90 @@ ContentParentsMemoryReporter::CollectReports(
|
|||
}
|
||||
|
||||
nsClassHashtable<nsStringHashKey, nsTArray<ContentParent*>>* ContentParent::sBrowserContentParents;
|
||||
|
||||
namespace {
|
||||
|
||||
class ScriptableCPInfo final : public nsIContentProcessInfo
|
||||
{
|
||||
public:
|
||||
explicit ScriptableCPInfo(ContentParent* aParent)
|
||||
: mContentParent(aParent)
|
||||
{
|
||||
MOZ_ASSERT(mContentParent);
|
||||
}
|
||||
|
||||
NS_DECL_ISUPPORTS
|
||||
NS_DECL_NSICONTENTPROCESSINFO
|
||||
|
||||
void ProcessDied()
|
||||
{
|
||||
mContentParent = nullptr;
|
||||
}
|
||||
|
||||
private:
|
||||
~ScriptableCPInfo()
|
||||
{
|
||||
MOZ_ASSERT(!mContentParent, "must call ProcessDied");
|
||||
}
|
||||
|
||||
ContentParent* mContentParent;
|
||||
};
|
||||
|
||||
NS_IMPL_ISUPPORTS(ScriptableCPInfo, nsIContentProcessInfo)
|
||||
|
||||
NS_IMETHODIMP
|
||||
ScriptableCPInfo::GetIsAlive(bool* aIsAlive)
|
||||
{
|
||||
*aIsAlive = mContentParent != nullptr;
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
NS_IMETHODIMP
|
||||
ScriptableCPInfo::GetProcessId(int32_t* aPID)
|
||||
{
|
||||
if (!mContentParent) {
|
||||
*aPID = -1;
|
||||
return NS_ERROR_NOT_INITIALIZED;
|
||||
}
|
||||
|
||||
*aPID = mContentParent->Pid();
|
||||
if (*aPID == -1) {
|
||||
return NS_ERROR_FAILURE;
|
||||
}
|
||||
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
NS_IMETHODIMP
|
||||
ScriptableCPInfo::GetOpener(nsIContentProcessInfo** aInfo)
|
||||
{
|
||||
*aInfo = nullptr;
|
||||
if (!mContentParent) {
|
||||
return NS_ERROR_NOT_INITIALIZED;
|
||||
}
|
||||
|
||||
if (ContentParent* opener = mContentParent->Opener()) {
|
||||
nsCOMPtr<nsIContentProcessInfo> info = opener->ScriptableHelper();
|
||||
info.forget(aInfo);
|
||||
}
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
NS_IMETHODIMP
|
||||
ScriptableCPInfo::GetMessageManager(nsIMessageSender** aMessenger)
|
||||
{
|
||||
*aMessenger = nullptr;
|
||||
if (!mContentParent) {
|
||||
return NS_ERROR_NOT_INITIALIZED;
|
||||
}
|
||||
|
||||
nsCOMPtr<nsIMessageSender> manager = mContentParent->GetMessageManager();
|
||||
manager.forget(aMessenger);
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
} // anonymous namespace
|
||||
|
||||
nsTArray<ContentParent*>* ContentParent::sPrivateContent;
|
||||
StaticAutoPtr<LinkedList<ContentParent> > ContentParent::sContentParents;
|
||||
#if defined(XP_LINUX) && defined(MOZ_CONTENT_SANDBOX)
|
||||
|
@ -697,37 +782,64 @@ ContentParent::GetNewOrUsedBrowserProcess(const nsAString& aRemoteType,
|
|||
ContentParent* aOpener)
|
||||
{
|
||||
nsTArray<ContentParent*>& contentParents = GetOrCreatePool(aRemoteType);
|
||||
|
||||
uint32_t maxContentParents = GetMaxProcessCount(aRemoteType);
|
||||
|
||||
RefPtr<ContentParent> p;
|
||||
if (contentParents.Length() >= maxContentParents) {
|
||||
// We never want to re-use Large-Allocation processes.
|
||||
if (aRemoteType.EqualsLiteral(LARGE_ALLOCATION_REMOTE_TYPE)) {
|
||||
// We never want to re-use Large-Allocation processes.
|
||||
if (contentParents.Length() >= maxContentParents) {
|
||||
return GetNewOrUsedBrowserProcess(NS_LITERAL_STRING(DEFAULT_REMOTE_TYPE),
|
||||
aPriority,
|
||||
aOpener);
|
||||
}
|
||||
} else {
|
||||
nsTArray<nsIContentProcessInfo*> infos(contentParents.Length());
|
||||
for (auto* cp : contentParents) {
|
||||
infos.AppendElement(cp->mScriptableHelper);
|
||||
}
|
||||
|
||||
if ((p = RandomSelect(contentParents, aOpener, maxContentParents))) {
|
||||
return p.forget();
|
||||
nsCOMPtr<nsIContentProcessProvider> cpp =
|
||||
do_GetService("@mozilla.org/ipc/processselector;1");
|
||||
nsIContentProcessInfo* openerInfo = aOpener ? aOpener->mScriptableHelper.get() : nullptr;
|
||||
int32_t index;
|
||||
if (cpp &&
|
||||
NS_SUCCEEDED(cpp->ProvideProcess(aRemoteType, openerInfo,
|
||||
infos.Elements(), infos.Length(),
|
||||
&index))) {
|
||||
// If the provider returned an existing ContentParent, use that one.
|
||||
if (0 <= index && static_cast<uint32_t>(index) <= maxContentParents) {
|
||||
RefPtr<ContentParent> retval = contentParents[index];
|
||||
return retval.forget();
|
||||
}
|
||||
} else {
|
||||
// If there was a problem with the JS chooser, fall back to a random
|
||||
// selection.
|
||||
NS_WARNING("nsIContentProcessProvider failed to return a process");
|
||||
RefPtr<ContentParent> random;
|
||||
if (contentParents.Length() >= maxContentParents &&
|
||||
(random = RandomSelect(contentParents, aOpener, maxContentParents))) {
|
||||
return random.forget();
|
||||
}
|
||||
}
|
||||
|
||||
// Try to take the preallocated process only for the default process type.
|
||||
if (aRemoteType.Equals(NS_LITERAL_STRING(DEFAULT_REMOTE_TYPE)) &&
|
||||
RefPtr<ContentParent> p;
|
||||
if (aRemoteType.EqualsLiteral(DEFAULT_REMOTE_TYPE) &&
|
||||
(p = PreallocatedProcessManager::Take())) {
|
||||
// For pre-allocated process we have not set the opener yet.
|
||||
p->mOpener = aOpener;
|
||||
} else {
|
||||
p = new ContentParent(aOpener, aRemoteType);
|
||||
contentParents.AppendElement(p);
|
||||
return p.forget();
|
||||
}
|
||||
}
|
||||
|
||||
// Create a new process from scratch.
|
||||
RefPtr<ContentParent> p = new ContentParent(aOpener, aRemoteType);
|
||||
|
||||
if (!p->LaunchSubprocess(aPriority)) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
p->Init();
|
||||
}
|
||||
|
||||
contentParents.AppendElement(p);
|
||||
return p.forget();
|
||||
|
@ -1204,6 +1316,8 @@ ContentParent::Init()
|
|||
|
||||
RefPtr<GeckoMediaPluginServiceParent> gmps(GeckoMediaPluginServiceParent::GetSingleton());
|
||||
gmps->UpdateContentProcessGMPCapabilities();
|
||||
|
||||
mScriptableHelper = new ScriptableCPInfo(this);
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
@ -1274,6 +1388,11 @@ ContentParent::SetPriorityAndCheckIsAlive(ProcessPriority aPriority)
|
|||
void
|
||||
ContentParent::ShutDownProcess(ShutDownMethod aMethod)
|
||||
{
|
||||
if (mScriptableHelper) {
|
||||
static_cast<ScriptableCPInfo*>(mScriptableHelper.get())->ProcessDied();
|
||||
mScriptableHelper = nullptr;
|
||||
}
|
||||
|
||||
// Shutting down by sending a shutdown message works differently than the
|
||||
// other methods. We first call Shutdown() in the child. After the child is
|
||||
// ready, it calls FinishShutdown() on us. Then we close the channel.
|
||||
|
|
|
@ -45,6 +45,7 @@
|
|||
#define LARGE_ALLOCATION_REMOTE_TYPE "webLargeAllocation"
|
||||
|
||||
class nsConsoleService;
|
||||
class nsIContentProcessInfo;
|
||||
class nsICycleCollectorLogSink;
|
||||
class nsIDumpGCAndCCLogsCallback;
|
||||
class nsITabParent;
|
||||
|
@ -371,6 +372,10 @@ public:
|
|||
{
|
||||
return mOpener;
|
||||
}
|
||||
nsIContentProcessInfo* ScriptableHelper() const
|
||||
{
|
||||
return mScriptableHelper;
|
||||
}
|
||||
|
||||
bool NeedsPermissionsUpdate() const
|
||||
{
|
||||
|
@ -1173,6 +1178,7 @@ private:
|
|||
|
||||
RefPtr<nsConsoleService> mConsoleService;
|
||||
nsConsoleService* GetConsoleService();
|
||||
nsCOMPtr<nsIContentProcessInfo> mScriptableHelper;
|
||||
|
||||
nsTArray<nsCOMPtr<nsIObserver>> mIdleListeners;
|
||||
|
||||
|
|
|
@ -68,6 +68,9 @@ const char* mozilla::dom::ContentPrefs::gInitPrefs[] = {
|
|||
"full-screen-api.allow-trusted-requests-only",
|
||||
"full-screen-api.enabled",
|
||||
"full-screen-api.unprefix.enabled",
|
||||
#ifdef FUZZING
|
||||
"fuzzing.enabled",
|
||||
#endif
|
||||
"gfx.font_rendering.opentype_svg.enabled",
|
||||
"hangmonitor.timeout",
|
||||
"html5.flushtimer.initialdelay",
|
||||
|
|
|
@ -63,8 +63,7 @@ public:
|
|||
}
|
||||
|
||||
void Decrypt(GMPBuffer* aBuffer,
|
||||
GMPEncryptedBufferMetadata* aMetadata,
|
||||
uint64_t aDurationUses) override
|
||||
GMPEncryptedBufferMetadata* aMetadata) override
|
||||
{
|
||||
}
|
||||
|
||||
|
|
|
@ -696,7 +696,7 @@ GMPCDMProxy::gmp_Decrypt(RefPtr<DecryptJob> aJob)
|
|||
|
||||
nsTArray<uint8_t> data;
|
||||
data.AppendElements(aJob->mSample->Data(), aJob->mSample->Size());
|
||||
mCDM->Decrypt(aJob->mId, aJob->mSample->mCrypto, data, aJob->mSample->mDuration);
|
||||
mCDM->Decrypt(aJob->mId, aJob->mSample->mCrypto, data);
|
||||
mDecryptionJobs.AppendElement(aJob.forget());
|
||||
}
|
||||
|
||||
|
|
|
@ -332,8 +332,7 @@ GMPDecryptorChild::RecvSetServerCertificate(const uint32_t& aPromiseId,
|
|||
mozilla::ipc::IPCResult
|
||||
GMPDecryptorChild::RecvDecrypt(const uint32_t& aId,
|
||||
InfallibleTArray<uint8_t>&& aBuffer,
|
||||
const GMPDecryptionData& aMetadata,
|
||||
const uint64_t& aDurationUsecs)
|
||||
const GMPDecryptionData& aMetadata)
|
||||
{
|
||||
if (!mSession) {
|
||||
return IPC_FAIL_NO_REASON(this);
|
||||
|
@ -347,7 +346,7 @@ GMPDecryptorChild::RecvDecrypt(const uint32_t& aId,
|
|||
GMPEncryptedBufferDataImpl* metadata = new GMPEncryptedBufferDataImpl(aMetadata);
|
||||
buffer->SetMetadata(metadata);
|
||||
|
||||
mSession->Decrypt(buffer, metadata, aDurationUsecs);
|
||||
mSession->Decrypt(buffer, metadata);
|
||||
return IPC_OK();
|
||||
}
|
||||
|
||||
|
|
|
@ -103,8 +103,7 @@ private:
|
|||
|
||||
mozilla::ipc::IPCResult RecvDecrypt(const uint32_t& aId,
|
||||
InfallibleTArray<uint8_t>&& aBuffer,
|
||||
const GMPDecryptionData& aMetadata,
|
||||
const uint64_t& aDurationUsecs) override;
|
||||
const GMPDecryptionData& aMetadata) override;
|
||||
|
||||
// Resolve/reject promise on completion.
|
||||
mozilla::ipc::IPCResult RecvSetServerCertificate(const uint32_t& aPromiseId,
|
||||
|
|
|
@ -173,8 +173,7 @@ GMPDecryptorParent::SetServerCertificate(uint32_t aPromiseId,
|
|||
void
|
||||
GMPDecryptorParent::Decrypt(uint32_t aId,
|
||||
const CryptoSample& aCrypto,
|
||||
const nsTArray<uint8_t>& aBuffer,
|
||||
uint64_t aDurationUsecs)
|
||||
const nsTArray<uint8_t>& aBuffer)
|
||||
{
|
||||
LOGV(("GMPDecryptorParent[%p]::Decrypt(id=%d)", this, aId));
|
||||
|
||||
|
@ -193,10 +192,10 @@ GMPDecryptorParent::Decrypt(uint32_t aId,
|
|||
aCrypto.mEncryptedSizes,
|
||||
aCrypto.mSessionIds);
|
||||
|
||||
Unused << SendDecrypt(aId, aBuffer, data, aDurationUsecs);
|
||||
Unused << SendDecrypt(aId, aBuffer, data);
|
||||
} else {
|
||||
GMPDecryptionData data;
|
||||
Unused << SendDecrypt(aId, aBuffer, data, aDurationUsecs);
|
||||
Unused << SendDecrypt(aId, aBuffer, data);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -61,8 +61,7 @@ public:
|
|||
|
||||
void Decrypt(uint32_t aId,
|
||||
const CryptoSample& aCrypto,
|
||||
const nsTArray<uint8_t>& aBuffer,
|
||||
uint64_t aDurationUsecs) override;
|
||||
const nsTArray<uint8_t>& aBuffer) override;
|
||||
|
||||
void Close() override;
|
||||
|
||||
|
|
|
@ -55,8 +55,7 @@ public:
|
|||
|
||||
virtual void Decrypt(uint32_t aId,
|
||||
const mozilla::CryptoSample& aCrypto,
|
||||
const nsTArray<uint8_t>& aBuffer,
|
||||
uint64_t aDurationUsecs) = 0;
|
||||
const nsTArray<uint8_t>& aBuffer) = 0;
|
||||
|
||||
virtual void Close() = 0;
|
||||
};
|
||||
|
|
|
@ -46,8 +46,7 @@ child:
|
|||
|
||||
async Decrypt(uint32_t aId,
|
||||
uint8_t[] aBuffer,
|
||||
GMPDecryptionData aMetadata,
|
||||
uint64_t aDurationUsecs);
|
||||
GMPDecryptionData aMetadata);
|
||||
|
||||
async DecryptingComplete();
|
||||
|
||||
|
|
|
@ -324,8 +324,7 @@ public:
|
|||
// is deleted. Don't forget to call Decrypted(), as otherwise aBuffer's
|
||||
// memory will leak!
|
||||
virtual void Decrypt(GMPBuffer* aBuffer,
|
||||
GMPEncryptedBufferMetadata* aMetadata,
|
||||
uint64_t aDurationUsecs) = 0;
|
||||
GMPEncryptedBufferMetadata* aMetadata) = 0;
|
||||
|
||||
// Called when the decryption operations are complete.
|
||||
// Do not call the GMPDecryptorCallback's functions after this is called.
|
||||
|
|
|
@ -8,9 +8,7 @@
|
|||
#include "WidevineAdapter.h"
|
||||
#include "WidevineUtils.h"
|
||||
#include "WidevineFileIO.h"
|
||||
#include "GMPPlatform.h"
|
||||
#include <stdarg.h>
|
||||
#include "TimeUnits.h"
|
||||
|
||||
using namespace cdm;
|
||||
using namespace std;
|
||||
|
@ -160,108 +158,29 @@ WidevineDecryptor::SetServerCertificate(uint32_t aPromiseId,
|
|||
CDM()->SetServerCertificate(aPromiseId, aServerCert, aServerCertSize);
|
||||
}
|
||||
|
||||
cdm::Time
|
||||
WidevineDecryptor::ThrottleDecrypt(cdm::Time aWallTime, cdm::Time aSampleDuration)
|
||||
{
|
||||
const cdm::Time WindowSize = 1.0;
|
||||
const cdm::Time MaxThroughput = 2.0;
|
||||
|
||||
// Forget decrypts that happened before the start of our window.
|
||||
while (!mDecrypts.empty() && mDecrypts.front().mWallTime < aWallTime - WindowSize) {
|
||||
mDecrypts.pop_front();
|
||||
}
|
||||
|
||||
// How much time duration of the media would we have decrypted inside the
|
||||
// time window if we did decrypt this block?
|
||||
cdm::Time durationDecrypted = aSampleDuration;
|
||||
for (const DecryptJob& job : mDecrypts) {
|
||||
durationDecrypted += job.mSampleDuration;
|
||||
}
|
||||
|
||||
if (durationDecrypted > MaxThroughput) {
|
||||
// If we decrypted a sample of this duration, we would have decrypted more than
|
||||
// our threshold for max throughput, over the preceding wall time window.
|
||||
return durationDecrypted - MaxThroughput;
|
||||
}
|
||||
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
void
|
||||
WidevineDecryptor::Decrypt(GMPBuffer* aBuffer,
|
||||
GMPEncryptedBufferMetadata* aMetadata,
|
||||
uint64_t aDurationUsecs)
|
||||
GMPEncryptedBufferMetadata* aMetadata)
|
||||
{
|
||||
if (!mCallback) {
|
||||
CDM_LOG("WidevineDecryptor::Decrypt() this=%p FAIL; !mCallback", this);
|
||||
return;
|
||||
}
|
||||
|
||||
cdm::Time duration = double(aDurationUsecs) / USECS_PER_S;
|
||||
mPendingDecrypts.push({aBuffer, aMetadata, duration});
|
||||
ProcessDecrypts();
|
||||
}
|
||||
|
||||
void
|
||||
WidevineDecryptor::ProcessDecryptsFromTimer()
|
||||
{
|
||||
MOZ_ASSERT(mPendingDecryptTimerSet);
|
||||
mPendingDecryptTimerSet = false;
|
||||
ProcessDecrypts();
|
||||
}
|
||||
|
||||
void
|
||||
WidevineDecryptor::ProcessDecrypts()
|
||||
{
|
||||
while (!mPendingDecrypts.empty()) {
|
||||
PendingDecrypt job = mPendingDecrypts.front();
|
||||
|
||||
// We throttle our decrypt so that we don't decrypt more than a certain
|
||||
// duration of samples per second. This is to work around bugs in the
|
||||
// Widevine CDM. See bug 1338924.
|
||||
cdm::Time now = GetCurrentWallTime();
|
||||
cdm::Time delay = ThrottleDecrypt(now, job.mSampleDuration);
|
||||
|
||||
if (delay > 0.0) {
|
||||
// If we decrypted this sample now, we'd decrypt more than our threshold
|
||||
// per second of samples. Enqueue the sample, and wait until we'd be able
|
||||
// to decrypt it without breaking our throughput threshold.
|
||||
if (!mPendingDecryptTimerSet) {
|
||||
mPendingDecryptTimerSet = true;
|
||||
RefPtr<WidevineDecryptor> self = this;
|
||||
GMPTask* task = gmp::NewGMPTask(
|
||||
[self]() {
|
||||
self->ProcessDecryptsFromTimer();
|
||||
});
|
||||
gmp::SetTimerOnMainThread(task, delay * 1000);
|
||||
}
|
||||
return;
|
||||
}
|
||||
DecryptBuffer(job);
|
||||
mDecrypts.push_back(DecryptJob(now, job.mSampleDuration));
|
||||
mPendingDecrypts.pop();
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
WidevineDecryptor::DecryptBuffer(const PendingDecrypt& aJob)
|
||||
{
|
||||
GMPBuffer* buffer = aJob.mBuffer;
|
||||
const GMPEncryptedBufferMetadata* crypto = aJob.mMetadata;
|
||||
const GMPEncryptedBufferMetadata* crypto = aMetadata;
|
||||
InputBuffer sample;
|
||||
nsTArray<SubsampleEntry> subsamples;
|
||||
InitInputBuffer(crypto, buffer->Id(), buffer->Data(), buffer->Size(), sample, subsamples);
|
||||
InitInputBuffer(crypto, aBuffer->Id(), aBuffer->Data(), aBuffer->Size(), sample, subsamples);
|
||||
WidevineDecryptedBlock decrypted;
|
||||
Status rv = CDM()->Decrypt(sample, &decrypted);
|
||||
CDM_LOG("Decryptor::Decrypt(timestamp=%" PRId64 ") rv=%d sz=%d",
|
||||
sample.timestamp, rv, decrypted.DecryptedBuffer()->Size());
|
||||
if (rv == kSuccess) {
|
||||
buffer->Resize(decrypted.DecryptedBuffer()->Size());
|
||||
memcpy(buffer->Data(),
|
||||
aBuffer->Resize(decrypted.DecryptedBuffer()->Size());
|
||||
memcpy(aBuffer->Data(),
|
||||
decrypted.DecryptedBuffer()->Data(),
|
||||
decrypted.DecryptedBuffer()->Size());
|
||||
}
|
||||
mCallback->Decrypted(buffer, ToGMPErr(rv));
|
||||
mCallback->Decrypted(aBuffer, ToGMPErr(rv));
|
||||
}
|
||||
|
||||
void
|
||||
|
@ -269,16 +188,6 @@ WidevineDecryptor::DecryptingComplete()
|
|||
{
|
||||
CDM_LOG("WidevineDecryptor::DecryptingComplete() this=%p, instanceId=%u",
|
||||
this, mInstanceId);
|
||||
|
||||
// Ensure buffers are freed.
|
||||
while (!mPendingDecrypts.empty()) {
|
||||
PendingDecrypt& job = mPendingDecrypts.front();
|
||||
if (mCallback) {
|
||||
mCallback->Decrypted(job.mBuffer, GMPAbortedErr);
|
||||
}
|
||||
mPendingDecrypts.pop();
|
||||
}
|
||||
|
||||
// Drop our references to the CDMWrapper. When any other references
|
||||
// held elsewhere are dropped (for example references held by a
|
||||
// WidevineVideoDecoder, or a runnable), the CDMWrapper destroys
|
||||
|
|
|
@ -12,8 +12,6 @@
|
|||
#include "mozilla/RefPtr.h"
|
||||
#include "WidevineUtils.h"
|
||||
#include <map>
|
||||
#include <deque>
|
||||
#include <queue>
|
||||
|
||||
namespace mozilla {
|
||||
|
||||
|
@ -66,8 +64,7 @@ public:
|
|||
uint32_t aServerCertSize) override;
|
||||
|
||||
void Decrypt(GMPBuffer* aBuffer,
|
||||
GMPEncryptedBufferMetadata* aMetadata,
|
||||
uint64_t aDurationUsecs) override;
|
||||
GMPEncryptedBufferMetadata* aMetadata) override;
|
||||
|
||||
void DecryptingComplete() override;
|
||||
|
||||
|
@ -121,45 +118,6 @@ public:
|
|||
GMPDecryptorCallback* Callback() const { return mCallback; }
|
||||
RefPtr<CDMWrapper> GetCDMWrapper() const { return mCDM; }
|
||||
private:
|
||||
|
||||
struct PendingDecrypt
|
||||
{
|
||||
PendingDecrypt(GMPBuffer* aBuffer,
|
||||
GMPEncryptedBufferMetadata* aMetadata,
|
||||
cdm::Time aSampleDuration)
|
||||
: mBuffer(aBuffer)
|
||||
, mMetadata(aMetadata)
|
||||
, mSampleDuration(aSampleDuration)
|
||||
{
|
||||
}
|
||||
GMPBuffer* mBuffer;
|
||||
GMPEncryptedBufferMetadata* mMetadata;
|
||||
cdm::Time mSampleDuration;
|
||||
};
|
||||
|
||||
// Queue of buffers waiting to be decrypted.
|
||||
std::queue<PendingDecrypt> mPendingDecrypts;
|
||||
|
||||
void DecryptBuffer(const PendingDecrypt& aJob);
|
||||
cdm::Time ThrottleDecrypt(cdm::Time aWallClock, cdm::Time aSampleDuration);
|
||||
void ProcessDecrypts();
|
||||
void ProcessDecryptsFromTimer();
|
||||
|
||||
struct DecryptJob
|
||||
{
|
||||
DecryptJob(cdm::Time aWallTime, cdm::Time aSampleDuration)
|
||||
: mWallTime(aWallTime)
|
||||
, mSampleDuration(aSampleDuration)
|
||||
{}
|
||||
cdm::Time mWallTime;
|
||||
cdm::Time mSampleDuration;
|
||||
};
|
||||
|
||||
// Queue of durations of buffers that have been decrypted, along with the
|
||||
// wall-clock timestamp when they were decrypted. This enables us to
|
||||
// throttle the throughput against the wall-clock.
|
||||
std::deque<DecryptJob> mDecrypts;
|
||||
|
||||
~WidevineDecryptor();
|
||||
RefPtr<CDMWrapper> mCDM;
|
||||
cdm::ContentDecryptionModule_8* CDM() { return mCDM->GetCDM(); }
|
||||
|
@ -169,7 +127,6 @@ private:
|
|||
bool mDistinctiveIdentifierRequired = false;
|
||||
bool mPersistentStateRequired = false;
|
||||
uint32_t mInstanceId = 0;
|
||||
bool mPendingDecryptTimerSet = false;
|
||||
};
|
||||
|
||||
} // namespace mozilla
|
||||
|
|
|
@ -0,0 +1,106 @@
|
|||
/* -*- 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 DecryptThroughputLimit_h
|
||||
#define DecryptThroughputLimit_h
|
||||
|
||||
#include "PlatformDecoderModule.h"
|
||||
#include "MediaTimer.h"
|
||||
#include <deque>
|
||||
|
||||
namespace mozilla {
|
||||
|
||||
// We throttle our decrypt so that we don't decrypt more than a certain
|
||||
// duration of samples per second. This is to work around bugs in the
|
||||
// Widevine CDM. See bug 1338924 and bug 1342822.
|
||||
class DecryptThroughputLimit
|
||||
{
|
||||
public:
|
||||
|
||||
explicit DecryptThroughputLimit(AbstractThread* aTargetThread)
|
||||
: mThrottleScheduler(aTargetThread)
|
||||
{
|
||||
}
|
||||
|
||||
typedef MozPromise<RefPtr<MediaRawData>, MediaResult, true> ThrottlePromise;
|
||||
|
||||
// Resolves promise after a delay if necessary in order to reduce the
|
||||
// throughput of samples sent through the CDM for decryption.
|
||||
RefPtr<ThrottlePromise>
|
||||
Throttle(MediaRawData* aSample)
|
||||
{
|
||||
// We should only have one decrypt request being processed at once.
|
||||
MOZ_RELEASE_ASSERT(!mThrottleScheduler.IsScheduled());
|
||||
|
||||
const TimeDuration WindowSize = TimeDuration::FromSeconds(1.0);
|
||||
const TimeDuration MaxThroughput = TimeDuration::FromSeconds(2.0);
|
||||
|
||||
// Forget decrypts that happened before the start of our window.
|
||||
const TimeStamp now = TimeStamp::Now();
|
||||
while (!mDecrypts.empty() && mDecrypts.front().mTimestamp < now - WindowSize) {
|
||||
mDecrypts.pop_front();
|
||||
}
|
||||
|
||||
// How much time duration of the media would we have decrypted inside the
|
||||
// time window if we did decrypt this block?
|
||||
TimeDuration sampleDuration = TimeDuration::FromMicroseconds(aSample->mDuration);
|
||||
TimeDuration durationDecrypted = sampleDuration;
|
||||
for (const DecryptedJob& job : mDecrypts) {
|
||||
durationDecrypted += job.mSampleDuration;
|
||||
}
|
||||
|
||||
if (durationDecrypted < MaxThroughput) {
|
||||
// If we decrypted a sample of this duration, we would *not* have
|
||||
// decrypted more than our threshold for max throughput, over the
|
||||
// preceding wall time window. So we're safe to proceed with this
|
||||
// decrypt.
|
||||
mDecrypts.push_back(DecryptedJob({ now, sampleDuration }));
|
||||
return ThrottlePromise::CreateAndResolve(aSample, __func__);
|
||||
}
|
||||
|
||||
// Otherwise, we need to delay until decrypting won't exceed our
|
||||
// throughput threshold.
|
||||
|
||||
RefPtr<ThrottlePromise> p = mPromiseHolder.Ensure(__func__);
|
||||
|
||||
TimeDuration delay = durationDecrypted - MaxThroughput;
|
||||
TimeStamp target = now + delay;
|
||||
RefPtr<MediaRawData> sample(aSample);
|
||||
mThrottleScheduler.Ensure(target,
|
||||
[this, sample, sampleDuration]() {
|
||||
mThrottleScheduler.CompleteRequest();
|
||||
mDecrypts.push_back(DecryptedJob({ TimeStamp::Now(), sampleDuration }));
|
||||
mPromiseHolder.Resolve(sample, __func__);
|
||||
},
|
||||
[] () {
|
||||
MOZ_DIAGNOSTIC_ASSERT(false);
|
||||
});
|
||||
|
||||
return p;
|
||||
}
|
||||
|
||||
void
|
||||
Flush()
|
||||
{
|
||||
mThrottleScheduler.Reset();
|
||||
mPromiseHolder.RejectIfExists(NS_ERROR_DOM_MEDIA_CANCELED, __func__);
|
||||
}
|
||||
|
||||
private:
|
||||
DelayedScheduler mThrottleScheduler;
|
||||
MozPromiseHolder<ThrottlePromise> mPromiseHolder;
|
||||
|
||||
struct DecryptedJob
|
||||
{
|
||||
TimeStamp mTimestamp;
|
||||
TimeDuration mSampleDuration;
|
||||
};
|
||||
std::deque<DecryptedJob> mDecrypts;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif // DecryptThroughputLimit_h
|
|
@ -20,6 +20,7 @@
|
|||
#include "nsAutoPtr.h"
|
||||
#include "nsClassHashtable.h"
|
||||
#include "nsServiceManagerUtils.h"
|
||||
#include "DecryptThroughputLimit.h"
|
||||
|
||||
namespace mozilla {
|
||||
|
||||
|
@ -37,6 +38,7 @@ public:
|
|||
, mProxy(aProxy)
|
||||
, mSamplesWaitingForKey(
|
||||
new SamplesWaitingForKey(mProxy, aType, aOnWaitingForKey))
|
||||
, mThroughputLimiter(aDecodeTaskQueue)
|
||||
, mIsShutdown(false)
|
||||
{
|
||||
}
|
||||
|
@ -53,10 +55,37 @@ public:
|
|||
MOZ_RELEASE_ASSERT(mDecrypts.Count() == 0,
|
||||
"Can only process one sample at a time");
|
||||
RefPtr<DecodePromise> p = mDecodePromise.Ensure(__func__);
|
||||
AttemptDecode(aSample);
|
||||
|
||||
RefPtr<EMEDecryptor> self = this;
|
||||
mSamplesWaitingForKey->WaitIfKeyNotUsable(aSample)
|
||||
->Then(mTaskQueue, __func__,
|
||||
[self, this](MediaRawData* aSample) {
|
||||
mKeyRequest.Complete();
|
||||
ThrottleDecode(aSample);
|
||||
},
|
||||
[self, this]() {
|
||||
mKeyRequest.Complete();
|
||||
})
|
||||
->Track(mKeyRequest);
|
||||
|
||||
return p;
|
||||
}
|
||||
|
||||
void ThrottleDecode(MediaRawData* aSample)
|
||||
{
|
||||
RefPtr<EMEDecryptor> self = this;
|
||||
mThroughputLimiter.Throttle(aSample)
|
||||
->Then(mTaskQueue, __func__,
|
||||
[self, this] (MediaRawData* aSample) {
|
||||
mThrottleRequest.Complete();
|
||||
AttemptDecode(aSample);
|
||||
},
|
||||
[self, this]() {
|
||||
mThrottleRequest.Complete();
|
||||
})
|
||||
->Track(mThrottleRequest);
|
||||
}
|
||||
|
||||
void AttemptDecode(MediaRawData* aSample)
|
||||
{
|
||||
MOZ_ASSERT(mTaskQueue->IsCurrentThreadIn());
|
||||
|
@ -66,11 +95,6 @@ public:
|
|||
return;
|
||||
}
|
||||
|
||||
RefPtr<EMEDecryptor> self = this;
|
||||
mSamplesWaitingForKey->WaitIfKeyNotUsable(aSample)
|
||||
->Then(mTaskQueue, __func__,
|
||||
[self, this](MediaRawData* aSample) {
|
||||
mKeyRequest.Complete();
|
||||
nsAutoPtr<MediaRawDataWriter> writer(aSample->CreateWriter());
|
||||
mProxy->GetSessionIdsForKeyId(aSample->mCrypto.mKeyId,
|
||||
writer->mCrypto.mSessionIds);
|
||||
|
@ -81,9 +105,6 @@ public:
|
|||
&EMEDecryptor::Decrypted,
|
||||
&EMEDecryptor::Decrypted)
|
||||
->Track(*mDecrypts.Get(aSample));
|
||||
},
|
||||
[self, this]() { mKeyRequest.Complete(); })
|
||||
->Track(mKeyRequest);
|
||||
}
|
||||
|
||||
void Decrypted(const DecryptResult& aDecrypted)
|
||||
|
@ -142,8 +163,10 @@ public:
|
|||
MOZ_ASSERT(mTaskQueue->IsCurrentThreadIn());
|
||||
MOZ_ASSERT(!mIsShutdown);
|
||||
mKeyRequest.DisconnectIfExists();
|
||||
mThrottleRequest.DisconnectIfExists();
|
||||
mDecodeRequest.DisconnectIfExists();
|
||||
mDecodePromise.RejectIfExists(NS_ERROR_DOM_MEDIA_CANCELED, __func__);
|
||||
mThroughputLimiter.Flush();
|
||||
for (auto iter = mDecrypts.Iter(); !iter.Done(); iter.Next()) {
|
||||
nsAutoPtr<DecryptPromiseRequestHolder>& holder = iter.Data();
|
||||
holder->DisconnectIfExists();
|
||||
|
@ -202,6 +225,8 @@ private:
|
|||
mDecrypts;
|
||||
RefPtr<SamplesWaitingForKey> mSamplesWaitingForKey;
|
||||
MozPromiseRequestHolder<SamplesWaitingForKey::WaitForKeyPromise> mKeyRequest;
|
||||
DecryptThroughputLimit mThroughputLimiter;
|
||||
MozPromiseRequestHolder<DecryptThroughputLimit::ThrottlePromise> mThrottleRequest;
|
||||
MozPromiseHolder<DecodePromise> mDecodePromise;
|
||||
MozPromiseHolder<DecodePromise> mDrainPromise;
|
||||
MozPromiseHolder<FlushPromise> mFlushPromise;
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
EXPORTS += [
|
||||
'DecryptThroughputLimit.h',
|
||||
'EMEDecoderModule.h',
|
||||
'EMEVideoDecoder.h',
|
||||
'SamplesWaitingForKey.h',
|
||||
|
|
|
@ -36,7 +36,6 @@ DIRS += ['interfaces/' + i for i in interfaces]
|
|||
DIRS += [
|
||||
'animation',
|
||||
'base',
|
||||
'archivereader',
|
||||
'bindings',
|
||||
'battery',
|
||||
'browser-element',
|
||||
|
|
|
@ -1,16 +0,0 @@
|
|||
/* -*- Mode: IDL; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
|
||||
/* 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/. */
|
||||
|
||||
[Pref="dom.archivereader.enabled",
|
||||
Constructor(Blob blob, optional ArchiveReaderOptions options)]
|
||||
interface ArchiveReader {
|
||||
ArchiveRequest getFilenames();
|
||||
ArchiveRequest getFile(DOMString filename);
|
||||
ArchiveRequest getFiles();
|
||||
};
|
||||
|
||||
dictionary ArchiveReaderOptions {
|
||||
DOMString encoding = "windows-1252"; // Default fallback encoding
|
||||
};
|
|
@ -1,10 +0,0 @@
|
|||
/* -*- Mode: IDL; 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/. */
|
||||
|
||||
[Pref="dom.archivereader.enabled"]
|
||||
interface ArchiveRequest : DOMRequest {
|
||||
readonly attribute ArchiveReader reader;
|
||||
};
|
Некоторые файлы не были показаны из-за слишком большого количества измененных файлов Показать больше
Загрузка…
Ссылка в новой задаче