Merge fx-team to m-c a=merge

This commit is contained in:
Wes Kocher 2015-05-08 10:29:41 -07:00
commit e43a05d6d2
92 changed files with 2817 additions and 1049 deletions

View File

@ -143,6 +143,11 @@ function createBookmark (data) {
}
exports.createBookmark = createBookmark;
function historyBatch () {
hsrv.runInBatchMode(() => {}, null);
}
exports.historyBatch = historyBatch;
function createBookmarkItem (data) {
let deferred = defer();
data = data || {};

View File

@ -29,7 +29,7 @@ const { search } = require('sdk/places/history');
const {
invalidResolve, invalidReject, createTree, createBookmark,
compareWithHost, addVisits, resetPlaces, createBookmarkItem,
removeVisits
removeVisits, historyBatch
} = require('./places-helper');
const { save, MENU, UNSORTED } = require('sdk/places/bookmarks');
const { promisedEmitter } = require('sdk/places/utils');
@ -236,9 +236,8 @@ exports['test history-start-batch, history-end-batch, history-start-clear'] = fu
on(startEvent, 'data', startHandler);
on(clearEvent, 'data', clearHandler);
createBookmark().then(() => {
resetPlaces(complete);
})
historyBatch();
resetPlaces(complete);
};
exports['test history-visit, history-title-changed'] = function (assert, done) {

View File

@ -1861,7 +1861,7 @@ pref("browser.polaris.enabled", false);
pref("privacy.trackingprotection.ui.enabled", false);
#endif
#ifdef E10S_TESTING_ONLY
#ifdef NIGHTLY_BUILD
// At the moment, autostart.2 is used, while autostart.1 is unused.
// We leave it here set to false to reset users' defaults and allow
// us to change everybody to true in the future, when desired.

View File

@ -164,6 +164,9 @@ const gXPInstallObserver = {
this._removeProgressNotification(browser);
break; }
case "addon-install-confirmation": {
let unsigned = installInfo.installs.filter(i => i.addon.signedState <= AddonManager.SIGNEDSTATE_MISSING);
let someUnsigned = unsigned.length > 0 && unsigned.length < installInfo.installs.length;
options.eventCallback = (aEvent) => {
switch (aEvent) {
case "removed":
@ -179,10 +182,21 @@ const gXPInstallObserver = {
addonList.firstChild.remove();
for (let install of installInfo.installs) {
let container = document.createElement("hbox");
let name = document.createElement("label");
name.setAttribute("value", install.addon.name);
name.setAttribute("class", "addon-install-confirmation-name");
addonList.appendChild(name);
container.appendChild(name);
if (someUnsigned && install.addon.signedState <= AddonManager.SIGNEDSTATE_MISSING) {
let unsigned = document.createElement("label");
unsigned.setAttribute("value", gNavigatorBundle.getString("addonInstall.unsigned"));
unsigned.setAttribute("class", "addon-install-confirmation-unsigned");
container.appendChild(unsigned);
}
addonList.appendChild(container);
}
this.acceptInstallation = () => {
@ -201,7 +215,20 @@ const gXPInstallObserver = {
options.learnMoreURL = Services.urlFormatter.formatURLPref("app.support.baseURL") +
"find-and-install-add-ons";
messageString = gNavigatorBundle.getString("addonConfirmInstall.message");
if (unsigned.length == installInfo.installs.length) {
// None of the add-ons are verified
messageString = gNavigatorBundle.getString("addonConfirmInstallUnsigned.message");
}
else if (unsigned.length == 0) {
// All add-ons are verified or don't need to be verified
messageString = gNavigatorBundle.getString("addonConfirmInstall.message");
}
else {
// Some of the add-ons are unverified, the list of names will indicate
// which
messageString = gNavigatorBundle.getString("addonConfirmInstallSomeUnsigned.message");
}
messageString = PluralForm.get(installInfo.installs.length, messageString);
messageString = messageString.replace("#1", brandShortName);
messageString = messageString.replace("#2", installInfo.installs.length);

View File

@ -773,6 +773,14 @@ window[chromehidden~="toolbar"] toolbar:not(#nav-bar):not(#TabsToolbar):not(#pri
-moz-binding: url("chrome://browser/content/urlbarBindings.xml#click-to-play-plugins-notification");
}
#password-fill-notification {
-moz-binding: url("chrome://browser/content/urlbarBindings.xml#password-fill-notification");
}
.login-fill-item {
-moz-binding: url("chrome://passwordmgr/content/login.xml#login");
}
.plugin-popupnotification-centeritem {
-moz-binding: url("chrome://browser/content/urlbarBindings.xml#plugin-popupnotification-center-item");
}

View File

@ -62,6 +62,13 @@
</popupnotificationcontent>
</popupnotification>
<vbox id="login-fill-doorhanger" hidden="true">
<description id="login-fill-testing"
value="Thanks for testing the login fill doorhanger!"/>
<textbox id="login-fill-filter"/>
<richlistbox id="login-fill-list"/>
</vbox>
#ifdef E10S_TESTING_ONLY
<popupnotification id="enable-e10s-notification" hidden="true">
<popupnotificationcontent orient="vertical"/>

View File

@ -455,6 +455,110 @@ function test_multiple() {
gBrowser.loadURI(TESTROOT + "installtrigger.html?" + triggers);
},
function test_someunverified() {
// This test is only relevant if using the new doorhanger UI and allowing
// unsigned add-ons
if (!Preferences.get("xpinstall.customConfirmationUI", false) ||
Preferences.get("xpinstall.signatures.required", true)) {
runNextTest();
return;
}
// Wait for the progress notification
wait_for_progress_notification(function(aPanel) {
// Wait for the install confirmation dialog
wait_for_install_dialog(function() {
let notification = document.getElementById("addon-install-confirmation-notification");
let message = notification.getAttribute("label");
is(message, "Caution: This site would like to install 2 add-ons in " + gApp + ", some of which are unverified. Proceed at your own risk.",
"Should see the right message");
let container = document.getElementById("addon-install-confirmation-content");
is(container.childNodes.length, 2, "Should be two items listed");
is(container.childNodes[0].firstChild.getAttribute("value"), "XPI Test", "Should have the right add-on");
is(container.childNodes[0].lastChild.getAttribute("class"),
"addon-install-confirmation-unsigned", "Should have the unverified marker");
is(container.childNodes[1].firstChild.getAttribute("value"), "Theme Test", "Should have the right add-on");
is(container.childNodes[1].childNodes.length, 1, "Shouldn't have the unverified marker");
// Wait for the complete notification
wait_for_notification("addon-install-complete", function(aPanel) {
AddonManager.getAddonsByIDs(["restartless-xpi@tests.mozilla.org",
"theme-xpi@tests.mozilla.org"], function([a, t]) {
a.uninstall();
// Installing a new theme tries to switch to it, switch back to the
// default theme.
t.userDisabled = true;
t.uninstall();
Services.perms.remove("example.com", "install");
wait_for_notification_close(runNextTest);
gBrowser.removeTab(gBrowser.selectedTab);
});
});
accept_install_dialog();
});
});
var pm = Services.perms;
pm.add(makeURI("http://example.com/"), "install", pm.ALLOW_ACTION);
var triggers = encodeURIComponent(JSON.stringify({
"Extension XPI": "restartless.xpi",
"Theme XPI": "theme.xpi"
}));
gBrowser.selectedTab = gBrowser.addTab();
gBrowser.loadURI(TESTROOT + "installtrigger.html?" + triggers);
},
function test_allunverified() {
// This test is only relevant if using the new doorhanger UI and allowing
// unsigned add-ons
if (!Preferences.get("xpinstall.customConfirmationUI", false) ||
Preferences.get("xpinstall.signatures.required", true)) {
runNextTest();
return;
}
// Wait for the progress notification
wait_for_progress_notification(function(aPanel) {
// Wait for the install confirmation dialog
wait_for_install_dialog(function() {
let notification = document.getElementById("addon-install-confirmation-notification");
let message = notification.getAttribute("label");
is(message, "Caution: This site would like to install an unverified add-on in " + gApp + ". Proceed at your own risk.");
let container = document.getElementById("addon-install-confirmation-content");
is(container.childNodes.length, 1, "Should be one item listed");
is(container.childNodes[0].firstChild.getAttribute("value"), "XPI Test", "Should have the right add-on");
is(container.childNodes[0].childNodes.length, 1, "Shouldn't have the unverified marker");
// Wait for the complete notification
wait_for_notification("addon-install-complete", function(aPanel) {
AddonManager.getAddonByID("restartless-xpi@tests.mozilla.org", function(aAddon) {
aAddon.uninstall();
Services.perms.remove("example.com", "install");
wait_for_notification_close(runNextTest);
gBrowser.removeTab(gBrowser.selectedTab);
});
});
accept_install_dialog();
});
});
var pm = Services.perms;
pm.add(makeURI("http://example.com/"), "install", pm.ALLOW_ACTION);
var triggers = encodeURIComponent(JSON.stringify({
"Extension XPI": "restartless.xpi"
}));
gBrowser.selectedTab = gBrowser.addTab();
gBrowser.loadURI(TESTROOT + "installtrigger.html?" + triggers);
},
function test_url() {
// Wait for the progress notification
wait_for_progress_notification(function(aPanel) {

View File

@ -2773,6 +2773,16 @@ file, You can obtain one at http://mozilla.org/MPL/2.0/.
</handlers>
</binding>
<!-- This is the XBL notification definition for the login fill doorhanger,
which is empty because the actual panel is not implemented inside an XBL
binding, but made of elements added to the notification panel. This
allows accessing the full structure while the panel is hidden. -->
<binding id="password-fill-notification" extends="chrome://global/content/bindings/notification.xml#popup-notification">
<content>
<children/>
</content>
</binding>
<binding id="splitmenu">
<content>
<xul:hbox anonid="menuitem" flex="1"

View File

@ -332,6 +332,7 @@ const PanelUI = {
tempPanel.setAttribute("type", "arrow");
tempPanel.setAttribute("id", "customizationui-widget-panel");
tempPanel.setAttribute("class", "cui-widget-panel");
tempPanel.setAttribute("viewId", aViewId);
if (this._disableAnimations) {
tempPanel.setAttribute("animate", "false");
}

View File

@ -11,4 +11,15 @@ standalone/content/libs
standalone/node_modules
# Libs we don't need to check
test/shared/vendor
# These are generated react files that we don't need to check
content/js/contacts.js
content/js/conversation.js
content/js/conversationViews.js
content/js/panel.js
content/js/roomViews.js
content/shared/js/feedbackViews.js
content/shared/js/views.js
standalone/content/js/fxOSMarketplace.js
standalone/content/js/standaloneRoomViews.js
standalone/content/js/webapp.js
ui/ui-showcase.js

View File

@ -53,7 +53,6 @@
"no-return-assign": 0, // TODO: Remove (use default)
"no-shadow": 0, // TODO: Remove (use default)
"no-spaced-func": 0, // TODO: Remove (use default)
"no-trailing-spaces": 0, // TODO: Remove (use default)
"no-undef": 0, // TODO: Remove (use default)
"no-underscore-dangle": 0, // Leave as 0. Commonly used for private variables.
"no-unused-expressions": 0, // TODO: Remove (use default)

View File

@ -512,7 +512,8 @@ loop.roomViews = (function(mozL10n) {
valueLink: this.linkState("newRoomDescription")})
),
React.createElement("button", {className: "room-context-btn-close",
onClick: this.handleCloseClick})
onClick: this.handleCloseClick,
title: mozL10n.get("cancel_button")})
)
)
);
@ -537,9 +538,11 @@ loop.roomViews = (function(mozL10n) {
React.createElement("div", {className: "room-context-comment"}, this.props.roomData.roomDescription) :
null,
React.createElement("button", {className: "room-context-btn-close",
onClick: this.handleCloseClick}),
onClick: this.handleCloseClick,
title: mozL10n.get("context_hide_tooltip")}),
React.createElement("button", {className: "room-context-btn-edit",
onClick: this.handleEditClick})
onClick: this.handleEditClick,
title: mozL10n.get("context_edit_tooltip")})
)
)
);

View File

@ -512,7 +512,8 @@ loop.roomViews = (function(mozL10n) {
valueLink={this.linkState("newRoomDescription")} />
</form>
<button className="room-context-btn-close"
onClick={this.handleCloseClick}/>
onClick={this.handleCloseClick}
title={mozL10n.get("cancel_button")}/>
</div>
</div>
);
@ -537,9 +538,11 @@ loop.roomViews = (function(mozL10n) {
<div className="room-context-comment">{this.props.roomData.roomDescription}</div> :
null}
<button className="room-context-btn-close"
onClick={this.handleCloseClick}/>
onClick={this.handleCloseClick}
title={mozL10n.get("context_hide_tooltip")}/>
<button className="room-context-btn-edit"
onClick={this.handleEditClick}/>
onClick={this.handleEditClick}
title={mozL10n.get("context_edit_tooltip")}/>
</div>
</div>
);

View File

@ -733,7 +733,9 @@ BrowserGlue.prototype = {
LightweightThemeManager.addBuiltInTheme({
id: "firefox-devedition@mozilla.org",
name: themeName,
#ifdef XP_WIN
accentcolor: "transparent",
#endif
headerURL: "resource:///chrome/browser/content/browser/defaultthemes/devedition.header.png",
iconURL: "resource:///chrome/browser/content/browser/defaultthemes/devedition.icon.png",
author: vendorShortName,
@ -2789,8 +2791,11 @@ let E10SUINotification = {
checkStatus: function() {
let skipE10sChecks = false;
try {
skipE10sChecks = (UpdateChannel.get() != "nightly") ||
Services.prefs.getBoolPref("browser.tabs.remote.autostart.disabled-because-using-a11y");
let updateChannel = UpdateChannel.get();
let channelAuthorized = updateChannel == "nightly" || updateChannel == "aurora";
skipE10sChecks = !channelAuthorized ||
UpdateServices.prefs.getBoolPref("browser.tabs.remote.autostart.disabled-because-using-a11y");
} catch(e) {}
if (skipE10sChecks) {
@ -2909,7 +2914,7 @@ let E10SUINotification = {
let browser = win.gBrowser.selectedBrowser;
let promptMessage = "Would you like to help us test multiprocess Nightly (e10s)? You can also enable e10s in Nightly preferences. Notable fixes:";
let promptMessage = "Multi-process is coming soon to Firefox. You can start using it now to get early access to some of the benefits:";
let mainAction = {
label: "Enable and Restart",
accessKey: "E",
@ -2942,9 +2947,8 @@ let E10SUINotification = {
win.PopupNotifications.show(browser, "enable-e10s", promptMessage, null, mainAction, secondaryActions, options);
let highlights = [
"Less crashing!",
"Improved add-on compatibility and DevTools",
"PDF.js, Web Console, Spellchecking, WebRTC now work"
"Improved responsiveness",
"Fewer crashes"
];
let doorhangerExtraContent = win.document.getElementById("enable-e10s-notification")

View File

@ -82,12 +82,18 @@ let gEditItemOverlay = {
// Check if the pane is initialized to show only read-only fields.
get readOnly() {
// Bug 1120314 - Folder shortcuts are read-only due to some quirky implementation
// details (the most important being the "smart" semantics of node.title).
return (!this.initialized ||
(!this._paneInfo.visibleRows.has("tagsRow") &&
(this._paneInfo.isFolderShortcut ||
this._paneInfo.isParentReadOnly)));
// TODO (Bug 1120314): Folder shortcuts are currently read-only due to some
// quirky implementation details (the most important being the "smart"
// semantics of node.title that makes hard to edit the right entry).
// This pane is read-only if:
// * the panel is not initialized
// * the node is a folder shortcut
// * the node is not bookmarked
// * the node is child of a read-only container and is not a bookmarked URI
return !this.initialized ||
this._paneInfo.isFolderShortcut ||
!this._paneInfo.isItem ||
(this._paneInfo.isParentReadOnly && !this._paneInfo.isBookmark);
},
// the first field which was edited after this panel was initialized for
@ -184,19 +190,25 @@ let gEditItemOverlay = {
this._namePicker.readOnly = this.readOnly;
}
if (showOrCollapse("locationRow", isURI, "location")) {
// In some cases we want to hide the location field, since it's not
// human-readable, but we still want to initialize it.
showOrCollapse("locationRow", isURI, "location");
if (isURI) {
this._initLocationField();
this._locationField.readOnly = !this._paneInfo.isItem;
this._locationField.readOnly = !this.readOnly;
}
if (showOrCollapse("descriptionRow",
this._paneInfo.isItem && !this.readOnly,
// hide the description field for
if (showOrCollapse("descriptionRow", isItem && !this.readOnly,
"description")) {
this._initDescriptionField();
this._descriptionField.readOnly = this.readOnly;
}
if (showOrCollapse("keywordRow", isBookmark, "keyword"))
if (showOrCollapse("keywordRow", isBookmark, "keyword")) {
this._initKeywordField();
this._keywordField.readOnly = this.readOnly;
}
// Collapse the tag selector if the item does not accept tags.
if (showOrCollapse("tagsRow", isURI || bulkTagging, "tags"))
@ -355,7 +367,7 @@ let gEditItemOverlay = {
// Hide the folders-separator if no folder is annotated as recently-used
this._element("foldersSeparator").hidden = (menupopup.childNodes.length <= 6);
this._folderMenuList.disabled = this._readOnly;
this._folderMenuList.disabled = this.readOnly;
},
QueryInterface:
@ -387,7 +399,7 @@ let gEditItemOverlay = {
},
onTagsFieldChange() {
if (!this.readOnly) {
if (this._paneInfo.isURI || this._paneInfo.bulkTagging) {
this._updateTags().then(
anyChanges => {
if (anyChanges)

View File

@ -15,13 +15,28 @@ XPCOMUtils.defineLazyModuleGetter(this, "ReaderMode",
"resource://gre/modules/ReaderMode.jsm");
let Pocket = {
get site() Services.prefs.getCharPref("browser.pocket.site"),
get listURL() { return "https://" + Pocket.site; },
/**
* Functions related to the Pocket panel UI.
*/
onPanelViewShowing(event) {
let window = event.target.ownerDocument.defaultView;
window.addEventListener("popupshowing", Pocket.onPocketPanelShowing, true);
window.addEventListener("popupshown", Pocket.onPocketPanelShown, true);
},
onPocketPanelShowing(event) {
let window = event.target.ownerDocument.defaultView;
window.removeEventListener("popupshowing", Pocket.onPocketPanelShowing, true);
window.pktUI.pocketButtonOnCommand(event);
window.pktUI.pocketPanelDidShow(event)
},
onPocketPanelShown(event) {
let window = event.target.ownerDocument.defaultView;
window.removeEventListener("popupshown", Pocket.onPocketPanelShown, true);
window.pktUI.pocketPanelDidShow(event);
},
onPanelViewHiding(event) {

View File

@ -301,12 +301,10 @@ var pktUI = (function() {
}
showPanel("chrome://browser/content/pocket/panels/signup.html?pockethost=" + Services.prefs.getCharPref("browser.pocket.site") + "&fxasignedin=" + fxasignedin + "&variant=" + pktApi.getSignupAB(), {
onShow: function() {
resizePanel({
width: 300,
height: startheight
});
},
onHide: panelDidHide,
width: 300,
height: startheight
});
});
}
@ -325,12 +323,6 @@ var pktUI = (function() {
showPanel("chrome://browser/content/pocket/panels/saved.html?pockethost=" + Services.prefs.getCharPref("browser.pocket.site") + "&premiumStatus=" + (pktApi.isPremiumUser() ? '1' : '0'), {
onShow: function() {
// Open and resize the panel
resizePanel({
width: 350,
height: 263
});
// Send error message for invalid url
if (!isValidURL) {
var error = new Error('Only links can be saved');
@ -369,6 +361,8 @@ var pktUI = (function() {
pktApi.addLink(url, options);
},
onHide: panelDidHide,
width: 350,
height: 267
});
}
@ -397,6 +391,11 @@ var pktUI = (function() {
// do it this hacky way for now
currentPanelDidShow = options.onShow;
currentPanelDidHide = options.onHide;
resizePanel({
width: options.width,
height: options.height
});
}
/**
@ -411,15 +410,9 @@ var pktUI = (function() {
var iframe = getPanelFrame();
iframe.width = options.width;
iframe.height = options.height;
return;
// TODO : Animate the change if given options.animate = true
getPanel().sizeTo(options.width, options.height);
setTimeout(function(){
// we set the iframe size directly because it does not automatically stretch vertically
var height = document.getElementById('pocket-panel-container').clientHeight + 'px';
getPanelFrame().style.height = height;
},1);
}
/**

View File

@ -36,9 +36,9 @@ XPCOMUtils.defineLazyModuleGetter(this, "Utils",
* In a typical restore, content-sessionStore.js will call the following based
* on messages and events it receives:
*
* restoreHistory(epoch, tabData, callbacks)
* restoreHistory(tabData, loadArguments, callbacks)
* Restores the tab's history and session cookies.
* restoreTabContent(finishCallback)
* restoreTabContent(loadArguments, finishCallback)
* Starts loading the data for the current page to restore.
* restoreDocument()
* Restore form and scroll data.
@ -54,11 +54,6 @@ XPCOMUtils.defineLazyModuleGetter(this, "Utils",
* At any time, SessionStore.jsm can cancel the ongoing restore by sending a
* reset message, which causes resetRestore to be called. At that point it's
* legal to begin another restore.
*
* The epoch that is passed into restoreHistory is merely a token. All messages
* sent back to SessionStore.jsm include the epoch. This way, SessionStore.jsm
* can discard messages that relate to restores that it has canceled (by
* starting a new restore, say).
*/
function ContentRestore(chromeGlobal) {
let internal = new ContentRestoreInternal(chromeGlobal);
@ -67,8 +62,7 @@ function ContentRestore(chromeGlobal) {
let EXPORTED_METHODS = ["restoreHistory",
"restoreTabContent",
"restoreDocument",
"resetRestore",
"getRestoreEpoch",
"resetRestore"
];
for (let method of EXPORTED_METHODS) {
@ -84,9 +78,6 @@ function ContentRestoreInternal(chromeGlobal) {
// The following fields are only valid during certain phases of the restore
// process.
// The epoch that was passed into restoreHistory. Removed in restoreDocument.
this._epoch = 0;
// The tabData for the restore. Set in restoreHistory and removed in
// restoreTabContent.
this._tabData = null;
@ -123,9 +114,8 @@ ContentRestoreInternal.prototype = {
* non-zero) is passed through to all the callbacks. If a load in the tab
* is started while it is pending, the appropriate callbacks are called.
*/
restoreHistory(epoch, tabData, loadArguments, callbacks) {
restoreHistory(tabData, loadArguments, callbacks) {
this._tabData = tabData;
this._epoch = epoch;
// In case about:blank isn't done yet.
let webNavigation = this.docShell.QueryInterface(Ci.nsIWebNavigation);
@ -290,8 +280,6 @@ ContentRestoreInternal.prototype = {
* called when the "load" event fires for the restoring tab.
*/
restoreDocument: function () {
this._epoch = 0;
if (!this._restoringDocument) {
return;
}
@ -327,15 +315,7 @@ ContentRestoreInternal.prototype = {
this._progressListener.uninstall();
}
this._progressListener = null;
},
/**
* If a restore is ongoing, this function returns the value of |epoch| that
* was passed to restoreHistory. If no restore is ongoing, it returns 0.
*/
getRestoreEpoch: function () {
return this._epoch;
},
}
};
/*

View File

@ -88,6 +88,16 @@ const NOTAB_MESSAGES = new Set([
"SessionStore:update",
]);
// The list of messages we accept without an "epoch" parameter.
// See getCurrentEpoch() and friends to find out what an "epoch" is.
const NOEPOCH_MESSAGES = new Set([
// For a description see above.
"SessionStore:setupSyncHandler",
// For a description see above.
"SessionStore:crashedTabRevived",
]);
// The list of messages we want to receive even during the short period after a
// frame has been removed from the DOM and before its frame script has finished
// unloading.
@ -113,6 +123,7 @@ Cu.import("resource://gre/modules/osfile.jsm", this);
Cu.import("resource://gre/modules/PrivateBrowsingUtils.jsm", this);
Cu.import("resource://gre/modules/Promise.jsm", this);
Cu.import("resource://gre/modules/Task.jsm", this);
Cu.import("resource://gre/modules/debug.js", this);
XPCOMUtils.defineLazyServiceGetter(this, "gSessionStartup",
"@mozilla.org/browser/sessionstartup;1", "nsISessionStartup");
@ -329,10 +340,7 @@ let SessionStoreInternal = {
// windows yet to be restored
_restoreCount: -1,
// This number gets incremented each time we start to restore a tab.
_nextRestoreEpoch: 1,
// For each <browser> element being restored, records the current epoch.
// For each <browser> element, records the current epoch.
_browserEpochs: new WeakMap(),
// Any browsers that fires the oop-browser-crashed event gets stored in
@ -615,6 +623,19 @@ let SessionStoreInternal = {
`from a browser that has no tab`);
}
let data = aMessage.data || {};
let hasEpoch = data.hasOwnProperty("epoch");
// Most messages sent by frame scripts require to pass an epoch.
if (!hasEpoch && !NOEPOCH_MESSAGES.has(aMessage.name)) {
throw new Error(`received message '${aMessage.name}' without an epoch`);
}
// Ignore messages from previous epochs.
if (hasEpoch && !this.isCurrentEpoch(browser, data.epoch)) {
return;
}
switch (aMessage.name) {
case "SessionStore:setupSyncHandler":
TabState.setSyncHandler(browser, aMessage.objects.handler);
@ -680,77 +701,69 @@ let SessionStoreInternal = {
}
break;
case "SessionStore:restoreHistoryComplete":
if (this.isCurrentEpoch(browser, aMessage.data.epoch)) {
// Notify the tabbrowser that the tab chrome has been restored.
let tabData = browser.__SS_data;
// Notify the tabbrowser that the tab chrome has been restored.
let tabData = browser.__SS_data;
// wall-paper fix for bug 439675: make sure that the URL to be loaded
// is always visible in the address bar
let activePageData = tabData.entries[tabData.index - 1] || null;
let uri = activePageData ? activePageData.url || null : null;
browser.userTypedValue = uri;
// wall-paper fix for bug 439675: make sure that the URL to be loaded
// is always visible in the address bar
let activePageData = tabData.entries[tabData.index - 1] || null;
let uri = activePageData ? activePageData.url || null : null;
browser.userTypedValue = uri;
// If the page has a title, set it.
if (activePageData) {
if (activePageData.title) {
tab.label = activePageData.title;
tab.crop = "end";
} else if (activePageData.url != "about:blank") {
tab.label = activePageData.url;
tab.crop = "center";
}
// If the page has a title, set it.
if (activePageData) {
if (activePageData.title) {
tab.label = activePageData.title;
tab.crop = "end";
} else if (activePageData.url != "about:blank") {
tab.label = activePageData.url;
tab.crop = "center";
}
// Restore the tab icon.
if ("image" in tabData) {
win.gBrowser.setIcon(tab, tabData.image);
}
let event = win.document.createEvent("Events");
event.initEvent("SSTabRestoring", true, false);
tab.dispatchEvent(event);
}
// Restore the tab icon.
if ("image" in tabData) {
win.gBrowser.setIcon(tab, tabData.image);
}
let event = win.document.createEvent("Events");
event.initEvent("SSTabRestoring", true, false);
tab.dispatchEvent(event);
break;
case "SessionStore:restoreTabContentStarted":
if (this.isCurrentEpoch(browser, aMessage.data.epoch)) {
if (browser.__SS_restoreState == TAB_STATE_NEEDS_RESTORE) {
// If a load not initiated by sessionstore was started in a
// previously pending tab. Mark the tab as no longer pending.
this.markTabAsRestoring(tab);
} else {
// If the user was typing into the URL bar when we crashed, but hadn't hit
// enter yet, then we just need to write that value to the URL bar without
// loading anything. This must happen after the load, since it will clear
// userTypedValue.
let tabData = browser.__SS_data;
if (tabData.userTypedValue && !tabData.userTypedClear) {
browser.userTypedValue = tabData.userTypedValue;
win.URLBarSetURI();
}
if (browser.__SS_restoreState == TAB_STATE_NEEDS_RESTORE) {
// If a load not initiated by sessionstore was started in a
// previously pending tab. Mark the tab as no longer pending.
this.markTabAsRestoring(tab);
} else {
// If the user was typing into the URL bar when we crashed, but hadn't hit
// enter yet, then we just need to write that value to the URL bar without
// loading anything. This must happen after the load, since it will clear
// userTypedValue.
let tabData = browser.__SS_data;
if (tabData.userTypedValue && !tabData.userTypedClear) {
browser.userTypedValue = tabData.userTypedValue;
win.URLBarSetURI();
}
}
break;
case "SessionStore:restoreTabContentComplete":
if (this.isCurrentEpoch(browser, aMessage.data.epoch)) {
// This callback is used exclusively by tests that want to
// monitor the progress of network loads.
if (gDebuggingEnabled) {
Services.obs.notifyObservers(browser, NOTIFY_TAB_RESTORED, null);
}
delete browser.__SS_data;
SessionStoreInternal._resetLocalTabRestoringState(tab);
SessionStoreInternal.restoreNextTab();
this._sendTabRestoredNotification(tab);
// This callback is used exclusively by tests that want to
// monitor the progress of network loads.
if (gDebuggingEnabled) {
Services.obs.notifyObservers(browser, NOTIFY_TAB_RESTORED, null);
}
delete browser.__SS_data;
SessionStoreInternal._resetLocalTabRestoringState(tab);
SessionStoreInternal.restoreNextTab();
this._sendTabRestoredNotification(tab);
break;
case "SessionStore:reloadPendingTab":
if (this.isCurrentEpoch(browser, aMessage.data.epoch)) {
if (browser.__SS_restoreState == TAB_STATE_NEEDS_RESTORE) {
this.restoreTabContent(tab);
}
if (browser.__SS_restoreState == TAB_STATE_NEEDS_RESTORE) {
this.restoreTabContent(tab);
}
break;
case "SessionStore:crashedTabRevived":
@ -1613,8 +1626,10 @@ let SessionStoreInternal = {
// restore the selected tab and lazily restore the rest. We'll make no
// efforts at this time to be smart and restore all of the tabs that had
// been in a restored state at the time of the crash.
let tab = aWindow.gBrowser.getTabForBrowser(aBrowser);
this._resetLocalTabRestoringState(tab);
if (aBrowser.__SS_restoreState) {
let tab = aWindow.gBrowser.getTabForBrowser(aBrowser);
this._resetLocalTabRestoringState(tab);
}
},
// Clean up data that has been closed a long time ago.
@ -2728,6 +2743,9 @@ let SessionStoreInternal = {
// Restores the given tab state for a given tab.
restoreTab(tab, tabData, options = {}) {
NS_ASSERT(!tab.linkedBrowser.__SS_restoreState,
"must reset tab before calling restoreTab()");
let restoreImmediately = options.restoreImmediately;
let loadArguments = options.loadArguments;
let browser = tab.linkedBrowser;
@ -2788,11 +2806,6 @@ let SessionStoreInternal = {
// Tab is now open.
delete tabData.closedAt;
// Flush all data from the content script synchronously. This is done so
// that all async messages that are still on their way to chrome will
// be ignored and don't override any tab data set when restoring.
TabState.flush(browser);
// Ensure the index is in bounds.
let activeIndex = (tabData.index || tabData.entries.length) - 1;
activeIndex = Math.min(activeIndex, tabData.entries.length - 1);
@ -2810,11 +2823,10 @@ let SessionStoreInternal = {
}
tabbrowser.updateBrowserRemotenessByURL(browser, uri);
// Start a new epoch and include the epoch in the restoreHistory
// message. If a message is received that relates to a previous epoch, we
// discard it.
let epoch = this._nextRestoreEpoch++;
this._browserEpochs.set(browser.permanentKey, epoch);
// Start a new epoch to discard all frame script messages relating to a
// previous epoch. All async messages that are still on their way to chrome
// will be ignored and don't override any tab data set when restoring.
let epoch = this.startNextEpoch(browser);
// keep the data around to prevent dataloss in case
// a tab gets closed before it's been properly restored
@ -3590,6 +3602,9 @@ let SessionStoreInternal = {
* The tab that will be "reset"
*/
_resetLocalTabRestoringState: function (aTab) {
NS_ASSERT(aTab.linkedBrowser.__SS_restoreState,
"given tab is not restoring");
let window = aTab.ownerDocument.defaultView;
let browser = aTab.linkedBrowser;
@ -3598,7 +3613,6 @@ let SessionStoreInternal = {
// The browser is no longer in any sort of restoring state.
delete browser.__SS_restoreState;
this._browserEpochs.delete(browser.permanentKey);
aTab.removeAttribute("pending");
browser.removeAttribute("pending");
@ -3615,13 +3629,34 @@ let SessionStoreInternal = {
},
_resetTabRestoringState: function (tab) {
NS_ASSERT(tab.linkedBrowser.__SS_restoreState,
"given tab is not restoring");
let browser = tab.linkedBrowser;
if (browser.__SS_restoreState) {
browser.messageManager.sendAsyncMessage("SessionStore:resetRestore", {});
}
browser.messageManager.sendAsyncMessage("SessionStore:resetRestore", {});
this._resetLocalTabRestoringState(tab);
},
/**
* Each fresh tab starts out with epoch=0. This function can be used to
* start a next epoch by incrementing the current value. It will enables us
* to ignore stale messages sent from previous epochs. The function returns
* the new epoch ID for the given |browser|.
*/
startNextEpoch(browser) {
let next = this.getCurrentEpoch(browser) + 1;
this._browserEpochs.set(browser.permanentKey, next);
return next;
},
/**
* Returns the current epoch for the given <browser>. If we haven't assigned
* a new epoch this will default to zero for new tabs.
*/
getCurrentEpoch(browser) {
return this._browserEpochs.get(browser.permanentKey) || 0;
},
/**
* Each time a <browser> element is restored, we increment its "epoch". To
* check if a message from content-sessionStore.js is out of date, we can
@ -3630,7 +3665,7 @@ let SessionStoreInternal = {
* with respect to |browser|.
*/
isCurrentEpoch: function (browser, epoch) {
return (this._browserEpochs.get(browser.permanentKey) || 0) == epoch;
return this.getCurrentEpoch(browser) == epoch;
},
};

View File

@ -40,6 +40,9 @@ Cu.import("resource:///modules/sessionstore/ContentRestore.jsm", this);
XPCOMUtils.defineLazyGetter(this, 'gContentRestore',
() => { return new ContentRestore(this) });
// The current epoch.
let gCurrentEpoch = 0;
/**
* Returns a lazy function that will evaluate the given
* function |fn| only once and cache its return value.
@ -88,14 +91,8 @@ let EventListener = {
return;
}
// If we're in the process of restoring, this load may signal
// the end of the restoration.
let epoch = gContentRestore.getRestoreEpoch();
if (!epoch) {
return;
}
// Restore the form data and scroll position.
// Restore the form data and scroll position. If we're not currently
// restoring a tab state then this call will simply be a noop.
gContentRestore.restoreDocument();
}
};
@ -122,6 +119,14 @@ let MessageListener = {
return;
}
// A fresh tab always starts with epoch=0. The parent has the ability to
// override that to signal a new era in this tab's life. This enables it
// to ignore async messages that were already sent but not yet received
// and would otherwise confuse the internal tab state.
if (data.epoch && data.epoch != gCurrentEpoch) {
gCurrentEpoch = data.epoch;
}
switch (name) {
case "SessionStore:restoreHistory":
this.restoreHistory(data);
@ -139,7 +144,7 @@ let MessageListener = {
},
restoreHistory({epoch, tabData, loadArguments}) {
gContentRestore.restoreHistory(epoch, tabData, loadArguments, {
gContentRestore.restoreHistory(tabData, loadArguments, {
onReload() {
// Inform SessionStore.jsm about the reload. It will send
// restoreTabContent in response.
@ -172,7 +177,7 @@ let MessageListener = {
},
restoreTabContent({loadArguments}) {
let epoch = gContentRestore.getRestoreEpoch();
let epoch = gCurrentEpoch;
// We need to pass the value of didStartLoad back to SessionStore.jsm.
let didStartLoad = gContentRestore.restoreTabContent(loadArguments, () => {
@ -698,7 +703,8 @@ let MessageQueue = {
// Send all data to the parent process.
sendMessage("SessionStore:update", {
id: this._id, data, telemetry,
isFinal: options.isFinal || false
isFinal: options.isFinal || false,
epoch: gCurrentEpoch
});
// Increase our unique message ID.

View File

@ -165,6 +165,7 @@ function TabTarget(tab) {
this._handleThreadState = this._handleThreadState.bind(this);
this.on("thread-resumed", this._handleThreadState);
this.on("thread-paused", this._handleThreadState);
this.activeTab = this.activeConsole = null;
// Only real tabs need initialization here. Placeholder objects for remote
// targets will be initialized after a makeRemote method call.
if (tab && !["client", "form", "chrome"].every(tab.hasOwnProperty, tab)) {
@ -420,6 +421,19 @@ TabTarget.prototype = {
}
this.activeTab = aTabClient;
this.threadActor = aResponse.threadActor;
attachConsole();
});
};
let attachConsole = () => {
this._client.attachConsole(this._form.consoleActor,
[ "NetworkActivity" ],
(aResponse, aWebConsoleClient) => {
if (!aWebConsoleClient) {
this._remote.reject("Unable to attach to the console");
return;
}
this.activeConsole = aWebConsoleClient;
this._remote.resolve(null);
});
};
@ -439,7 +453,7 @@ TabTarget.prototype = {
} else {
// AddonActor and chrome debugging on RootActor doesn't inherits from TabActor and
// doesn't need to be attached.
this._remote.resolve(null);
attachConsole();
}
return this._remote.promise;
@ -593,17 +607,15 @@ TabTarget.prototype = {
// We started with a local tab and created the client ourselves, so we
// should close it.
this._client.close(cleanupAndResolve);
} else {
} else if (this.activeTab) {
// The client was handed to us, so we are not responsible for closing
// it. We just need to detach from the tab, if already attached.
if (this.activeTab) {
// |detach| may fail if the connection is already dead, so proceed
// cleanup directly after this.
this.activeTab.detach();
cleanupAndResolve();
} else {
cleanupAndResolve();
}
// |detach| may fail if the connection is already dead, so proceed with
// cleanup directly after this.
this.activeTab.detach();
cleanupAndResolve();
} else {
cleanupAndResolve();
}
}
@ -620,6 +632,7 @@ TabTarget.prototype = {
promiseTargets.delete(this._form);
}
this.activeTab = null;
this.activeConsole = null;
this._client = null;
this._tab = null;
this._form = null;

View File

@ -110,6 +110,9 @@ function Toolbox(target, selectedTool, hostType, hostOptions) {
this._toolPanels = new Map();
this._telemetry = new Telemetry();
this._initInspector = null;
this._inspector = null;
this._toolRegistered = this._toolRegistered.bind(this);
this._toolUnregistered = this._toolUnregistered.bind(this);
this._refreshHostTitle = this._refreshHostTitle.bind(this);
@ -367,7 +370,7 @@ Toolbox.prototype = {
this._pingTelemetry();
let panel = yield this.selectTool(this._defaultToolId);
yield this.selectTool(this._defaultToolId);
// Wait until the original tool is selected so that the split
// console input will receive focus.

View File

@ -196,7 +196,9 @@ let NetMonitorController = {
/**
* Initiates remote or chrome network monitoring based on the current target,
* wiring event handlers as necessary.
* wiring event handlers as necessary. Since the TabTarget will have already
* started listening to network requests by now, this is largely
* netmonitor-specific initialization.
*
* @return object
* A promise that is resolved when the monitor finishes connecting.
@ -209,15 +211,18 @@ let NetMonitorController = {
let deferred = promise.defer();
this._connection = deferred.promise;
let target = this._target;
let { client, form } = target;
this.client = this._target.client;
// Some actors like AddonActor or RootActor for chrome debugging
// do not support attach/detach and can be used directly
if (!target.isTabActor) {
this._startChromeMonitoring(client, form.consoleActor, deferred.resolve);
} else {
this._startMonitoringTab(client, form, deferred.resolve);
// aren't actual tabs.
if (this._target.isTabActor) {
this.tabClient = this._target.activeTab;
}
this.webConsoleClient = this._target.activeConsole;
this.webConsoleClient.setPreferences(NET_PREFS, () => {
this.TargetEventsHandler.connect();
this.NetworkEventsHandler.connect();
deferred.resolve();
});
yield deferred.promise;
window.emit(EVENTS.CONNECTED);
@ -243,82 +248,6 @@ let NetMonitorController = {
return !!this.client;
},
/**
* Sets up a monitoring session.
*
* @param DebuggerClient aClient
* The debugger client.
* @param object aTabGrip
* The remote protocol grip of the tab.
* @param function aCallback
* A function to invoke once the client attached to the console client.
*/
_startMonitoringTab: function(aClient, aTabGrip, aCallback) {
if (!aClient) {
Cu.reportError("No client found!");
return;
}
this.client = aClient;
aClient.attachTab(aTabGrip.actor, (aResponse, aTabClient) => {
if (!aTabClient) {
Cu.reportError("No tab client found!");
return;
}
this.tabClient = aTabClient;
aClient.attachConsole(aTabGrip.consoleActor, LISTENERS, (aResponse, aWebConsoleClient) => {
if (!aWebConsoleClient) {
Cu.reportError("Couldn't attach to console: " + aResponse.error);
return;
}
this.webConsoleClient = aWebConsoleClient;
this.webConsoleClient.setPreferences(NET_PREFS, () => {
this.TargetEventsHandler.connect();
this.NetworkEventsHandler.connect();
if (aCallback) {
aCallback();
}
});
});
});
},
/**
* Sets up a chrome monitoring session.
*
* @param DebuggerClient aClient
* The debugger client.
* @param object aConsoleActor
* The remote protocol grip of the chrome debugger.
* @param function aCallback
* A function to invoke once the client attached to the console client.
*/
_startChromeMonitoring: function(aClient, aConsoleActor, aCallback) {
if (!aClient) {
Cu.reportError("No client found!");
return;
}
this.client = aClient;
aClient.attachConsole(aConsoleActor, LISTENERS, (aResponse, aWebConsoleClient) => {
if (!aWebConsoleClient) {
Cu.reportError("Couldn't attach to console: " + aResponse.error);
return;
}
this.webConsoleClient = aWebConsoleClient;
this.webConsoleClient.setPreferences(NET_PREFS, () => {
this.TargetEventsHandler.connect();
this.NetworkEventsHandler.connect();
if (aCallback) {
aCallback();
}
});
});
},
/**
* Gets the activity currently performed by the frontend.
* @return number
@ -440,7 +369,6 @@ function TargetEventsHandler() {
TargetEventsHandler.prototype = {
get target() NetMonitorController._target,
get webConsoleClient() NetMonitorController.webConsoleClient,
/**
* Listen for events emitted by the current tab target.
@ -528,8 +456,28 @@ NetworkEventsHandler.prototype = {
*/
connect: function() {
dumpn("NetworkEventsHandler is connecting...");
this.client.addListener("networkEvent", this._onNetworkEvent);
this.client.addListener("networkEventUpdate", this._onNetworkEventUpdate);
this.webConsoleClient.on("networkEvent", this._onNetworkEvent);
this.webConsoleClient.on("networkEventUpdate", this._onNetworkEventUpdate);
this._displayCachedEvents();
},
/**
* Display any network events already in the cache.
*/
_displayCachedEvents: function() {
for (let cachedEvent of this.webConsoleClient.getNetworkEvents()) {
// First add the request to the timeline.
this._onNetworkEvent("networkEvent", cachedEvent);
// Then replay any updates already received.
for (let update of cachedEvent.updates) {
this._onNetworkEventUpdate("networkEventUpdate", {
packet: {
updateType: update
},
networkInfo: cachedEvent
});
}
}
},
/**
@ -540,25 +488,21 @@ NetworkEventsHandler.prototype = {
return;
}
dumpn("NetworkEventsHandler is disconnecting...");
this.client.removeListener("networkEvent", this._onNetworkEvent);
this.client.removeListener("networkEventUpdate", this._onNetworkEventUpdate);
this.webConsoleClient.off("networkEvent", this._onNetworkEvent);
this.webConsoleClient.off("networkEventUpdate", this._onNetworkEventUpdate);
},
/**
* The "networkEvent" message type handler.
*
* @param string aType
* @param string type
* Message type.
* @param object aPacket
* The message received from the server.
* @param object networkInfo
* The network request information.
*/
_onNetworkEvent: function(aType, aPacket) {
if (aPacket.from != this.webConsoleClient.actor) {
// Skip events from different console actors.
return;
}
_onNetworkEvent: function(type, networkInfo) {
let { actor, startedDateTime, request: { method, url }, isXHR, fromCache } = networkInfo;
let { actor, startedDateTime, method, url, isXHR, fromCache } = aPacket.eventActor;
NetMonitorView.RequestsMenu.addRequest(
actor, startedDateTime, method, url, isXHR, fromCache
);
@ -568,19 +512,17 @@ NetworkEventsHandler.prototype = {
/**
* The "networkEventUpdate" message type handler.
*
* @param string aType
* @param string type
* Message type.
* @param object aPacket
* @param object packet
* The message received from the server.
* @param object networkInfo
* The network request information.
*/
_onNetworkEventUpdate: function(aType, aPacket) {
let actor = aPacket.from;
if (!NetMonitorView.RequestsMenu.getItemByValue(actor)) {
// Skip events from unknown actors.
return;
}
_onNetworkEventUpdate: function(type, { packet, networkInfo }) {
let actor = networkInfo.actor;
switch (aPacket.updateType) {
switch (packet.updateType) {
case "requestHeaders":
this.webConsoleClient.getRequestHeaders(actor, this._onRequestHeaders);
window.emit(EVENTS.UPDATING_REQUEST_HEADERS, actor);
@ -594,8 +536,8 @@ NetworkEventsHandler.prototype = {
window.emit(EVENTS.UPDATING_REQUEST_POST_DATA, actor);
break;
case "securityInfo":
NetMonitorView.RequestsMenu.updateRequest(aPacket.from, {
securityState: aPacket.state,
NetMonitorView.RequestsMenu.updateRequest(actor, {
securityState: networkInfo.securityInfo,
});
this.webConsoleClient.getSecurityInfo(actor, this._onSecurityInfo);
window.emit(EVENTS.UPDATING_SECURITY_INFO, actor);
@ -609,28 +551,28 @@ NetworkEventsHandler.prototype = {
window.emit(EVENTS.UPDATING_RESPONSE_COOKIES, actor);
break;
case "responseStart":
NetMonitorView.RequestsMenu.updateRequest(aPacket.from, {
httpVersion: aPacket.response.httpVersion,
remoteAddress: aPacket.response.remoteAddress,
remotePort: aPacket.response.remotePort,
status: aPacket.response.status,
statusText: aPacket.response.statusText,
headersSize: aPacket.response.headersSize
NetMonitorView.RequestsMenu.updateRequest(actor, {
httpVersion: networkInfo.response.httpVersion,
remoteAddress: networkInfo.response.remoteAddress,
remotePort: networkInfo.response.remotePort,
status: networkInfo.response.status,
statusText: networkInfo.response.statusText,
headersSize: networkInfo.response.headersSize
});
window.emit(EVENTS.STARTED_RECEIVING_RESPONSE, actor);
break;
case "responseContent":
NetMonitorView.RequestsMenu.updateRequest(aPacket.from, {
contentSize: aPacket.contentSize,
transferredSize: aPacket.transferredSize,
mimeType: aPacket.mimeType
NetMonitorView.RequestsMenu.updateRequest(actor, {
contentSize: networkInfo.response.bodySize,
transferredSize: networkInfo.response.transferredSize,
mimeType: networkInfo.response.content.mimeType
});
this.webConsoleClient.getResponseContent(actor, this._onResponseContent);
window.emit(EVENTS.UPDATING_RESPONSE_CONTENT, actor);
break;
case "eventTimings":
NetMonitorView.RequestsMenu.updateRequest(aPacket.from, {
totalTime: aPacket.totalTime
NetMonitorView.RequestsMenu.updateRequest(actor, {
totalTime: networkInfo.totalTime
});
this.webConsoleClient.getEventTimings(actor, this._onEventTimings);
window.emit(EVENTS.UPDATING_EVENT_TIMINGS, actor);

View File

@ -15,9 +15,6 @@ function test() {
let { RequestsMenu } = NetMonitorView;
reqMenu = RequestsMenu;
is(reqMenu.itemCount, 0,
"The request menu should empty before reloading");
let button = document.querySelector("#requests-menu-reload-notice-button");
button.click();
})

View File

@ -147,6 +147,11 @@ function initNetMonitor(aUrl, aWindow, aEnableCache) {
if(!aEnableCache) {
yield toggleCache(target, true);
info("Cache disabled when the current and all future toolboxes are open.");
// Remove any requests generated by the reload while toggling the cache to
// avoid interfering with the test.
isnot([...target.activeConsole.getNetworkEvents()].length, 0,
"Request to reconfigure the tab was recorded.");
target.activeConsole.clearNetworkRequests();
}
let toolbox = yield gDevTools.showToolbox(target, "netmonitor");

View File

@ -201,11 +201,13 @@ function buggyDragStop(graph, x, y = 1) {
x /= window.devicePixelRatio;
y /= window.devicePixelRatio;
// Only fire a mousemove instead of a mouseup.
// This happens when the mouseup happens outside of the toolbox,
// see Bug 1066504.
graph._onMouseMove({ testX: x, testY: y });
graph._onMouseMove({ testX: x, testY: y, buttons: 0 });
// Only fire a mousemove with no buttons instead of a mouseup.
// This happens when the mouseup happens outside of the window.
// Send different coordinates to make sure the selection is preserved,
// see Bugs 1066504 and 1144779.
graph._onMouseMove({ testX: x+1, testY: y+1, buttons: 0 });
}
function scroll(graph, wheel, x, y = 1) {

View File

@ -986,12 +986,12 @@ AbstractCanvasGraph.prototype = {
e.stopPropagation();
}
// If a mouseup happened outside the toolbox and the current operation
// is causing the selection changed, then end it.
// If a mouseup happened outside the window and the current operation
// is causing the selection to change, then end it.
if (e.buttons == 0 && (this.hasSelectionInProgress() ||
resizer.margin != null ||
dragger.origin != null)) {
return this._onMouseUp(e);
return this._onMouseUp();
}
let {mouseX,mouseY} = this._getRelativeEventCoordinates(e);
@ -1093,10 +1093,8 @@ AbstractCanvasGraph.prototype = {
/**
* Listener for the "mouseup" event on the graph's container.
*/
_onMouseUp: function(e) {
_onMouseUp: function() {
this._isMouseActive = false;
let {mouseX} = this._getRelativeEventCoordinates(e);
switch (this._canvas.getAttribute("input")) {
case "hovering-background":
case "hovering-region":
@ -1115,7 +1113,7 @@ AbstractCanvasGraph.prototype = {
this.emit("deselecting");
}
} else {
this._selection.end = mouseX;
this._selection.end = this._cursor.x;
this.emit("selecting");
}
break;

View File

@ -385,3 +385,5 @@ skip-if = e10s # Bug 1042253 - webconsole e10s tests (Linux debug timeout)
[browser_webconsole_column_numbers.js]
[browser_console_open_or_focus.js]
[browser_webconsole_bug_922212_console_dirxml.js]
[browser_webconsole_shows_reqs_in_netmonitor.js]
[browser_netmonitor_shows_reqs_in_webconsole.js]

View File

@ -0,0 +1,71 @@
/*
* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/
*/
const TEST_URI = "data:text/html;charset=utf8,Test that the netmonitor " +
"displays requests that have been recorded in the " +
"web console, even if the netmonitor hadn't opened yet.";
const TEST_FILE = "test-network-request.html";
const TEST_PATH = "http://example.com/browser/browser/devtools/webconsole/test/" +
TEST_FILE;
const NET_PREF = "devtools.webconsole.filter.networkinfo";
Services.prefs.setBoolPref(NET_PREF, true);
registerCleanupFunction(() => {
Services.prefs.clearUserPref(NET_PREF);
});
add_task(function* () {
let { tab, browser } = yield loadTab(TEST_URI);
// Test that the request appears in the console.
let hud = yield openConsole();
info("Web console is open");
yield loadDocument(browser);
info("Document loaded.");
yield waitForMessages({
webconsole: hud,
messages: [
{
name: "network message",
text: TEST_FILE,
category: CATEGORY_NETWORK,
severity: SEVERITY_LOG
}
]
});
// Test that the request appears in the network panel.
let target = TargetFactory.forTab(tab);
let toolbox = yield gDevTools.showToolbox(target, "netmonitor");
info("Network panel is open.");
testNetmonitor(toolbox);
});
function loadDocument(browser) {
let deferred = promise.defer();
browser.addEventListener("load", function onLoad() {
browser.removeEventListener("load", onLoad, true);
deferred.resolve();
}, true);
content.location = TEST_PATH;
return deferred.promise;
}
function testNetmonitor(toolbox) {
let monitor = toolbox.getCurrentPanel();
let { RequestsMenu } = monitor.panelWin.NetMonitorView;
is(RequestsMenu.itemCount, 1, "Network request appears in the network panel");
let item = RequestsMenu.getItemAtIndex(0);
is(item.attachment.method, "GET", "The attached method is correct.");
is(item.attachment.url, TEST_PATH, "The attached url is correct.");
}

View File

@ -73,7 +73,8 @@ function testPageLoad()
ok(!lastRequest.request.postData.text, "No request body was stored");
ok(lastRequest.discardRequestBody, "Request body was discarded");
ok(!lastRequest.response.content.text, "No response body was stored");
ok(lastRequest.discardResponseBody, "Response body was discarded");
ok(lastRequest.discardResponseBody || lastRequest.fromCache,
"Response body was discarded or response came from the cache");
lastRequest = null;
requestCallback = null;

View File

@ -0,0 +1,71 @@
/*
* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/
*/
const TEST_URI = "data:text/html;charset=utf8,Test that the web console " +
"displays requests that have been recorded in the " +
"netmonitor, even if the console hadn't opened yet.";
const TEST_FILE = "test-network-request.html";
const TEST_PATH = "http://example.com/browser/browser/devtools/webconsole/test/" +
TEST_FILE;
const NET_PREF = "devtools.webconsole.filter.networkinfo";
Services.prefs.setBoolPref(NET_PREF, true);
registerCleanupFunction(() => {
Services.prefs.clearUserPref(NET_PREF);
});
add_task(function* () {
let { tab, browser } = yield loadTab(TEST_URI);
let target = TargetFactory.forTab(tab);
let toolbox = yield gDevTools.showToolbox(target, "netmonitor");
info("Network panel is open.");
yield loadDocument(browser);
info("Document loaded.");
// Test that the request appears in the network panel.
testNetmonitor(toolbox);
// Test that the request appears in the console.
let hud = yield openConsole();
info("Web console is open");
yield waitForMessages({
webconsole: hud,
messages: [
{
name: "network message",
text: TEST_FILE,
category: CATEGORY_NETWORK,
severity: SEVERITY_LOG
}
]
});
});
function loadDocument(browser) {
let deferred = promise.defer();
browser.addEventListener("load", function onLoad() {
browser.removeEventListener("load", onLoad, true);
deferred.resolve();
}, true);
content.location = TEST_PATH;
return deferred.promise;
}
function testNetmonitor(toolbox) {
let monitor = toolbox.getCurrentPanel();
let { RequestsMenu } = monitor.panelWin.NetMonitorView;
is(RequestsMenu.itemCount, 1, "Network request appears in the network panel");
let item = RequestsMenu.getItemAtIndex(0);
is(item.attachment.method, "GET", "The attached method is correct.");
is(item.attachment.url, TEST_PATH, "The attached url is correct.");
}

View File

@ -15,7 +15,6 @@ let {Utils: WebConsoleUtils} = require("devtools/toolkit/webconsole/utils");
let {Messages} = require("devtools/webconsole/console-output");
const asyncStorage = require("devtools/toolkit/shared/async-storage");
// promise._reportErrors = true; // please never leave me.
//Services.prefs.setBoolPref("devtools.debugger.log", true);
let gPendingOutputTest = 0;

View File

@ -205,7 +205,6 @@ function WebConsoleFrame(aWebConsoleOwner)
this._outputQueue = [];
this._itemDestroyQueue = [];
this._pruneCategoriesQueue = {};
this._networkRequests = {};
this.filterPrefs = {};
this.output = new ConsoleOutput(this);
@ -253,14 +252,6 @@ WebConsoleFrame.prototype = {
*/
_initDefer: null,
/**
* Holds the network requests currently displayed by the Web Console. Each key
* represents the connection ID and the value is network request information.
* @private
* @type object
*/
_networkRequests: null,
/**
* Last time when we displayed any message in the output.
*
@ -1222,6 +1213,9 @@ WebConsoleFrame.prototype = {
this.outputMessage(CATEGORY_WEBDEV, this.logConsoleAPIMessage,
[aMessage]);
break;
case "NetworkEvent":
this.outputMessage(CATEGORY_NETWORK, this.logNetEvent, [aMessage]);
break;
}
}, this);
},
@ -1528,19 +1522,14 @@ WebConsoleFrame.prototype = {
/**
* Log network event.
*
* @param object aActor
* The network event actor to log.
* @param object networkInfo
* The network request information to log.
* @return nsIDOMElement|null
* The message element to display in the Web Console output.
*/
logNetEvent: function WCF_logNetEvent(aActor)
logNetEvent: function(networkInfo)
{
let actorId = aActor.actor;
let networkInfo = this._networkRequests[actorId];
if (!networkInfo) {
return null;
}
let actorId = networkInfo.actor;
let request = networkInfo.request;
let clipboardText = request.method + " " + request.url;
let severity = SEVERITY_LOG;
@ -1560,7 +1549,8 @@ WebConsoleFrame.prototype = {
let messageNode = this.createMessageNode(CATEGORY_NETWORK, severity,
methodNode, null, null,
clipboardText);
clipboardText, null,
networkInfo.timeStamp);
if (networkInfo.private) {
messageNode.setAttribute("private", true);
}
@ -1798,84 +1788,29 @@ WebConsoleFrame.prototype = {
/**
* Handle the network events coming from the remote Web Console.
*
* @param object aActor
* The NetworkEventActor grip.
* @param object networkInfo
* The network request information.
*/
handleNetworkEvent: function WCF_handleNetworkEvent(aActor)
handleNetworkEvent: function(networkInfo)
{
let networkInfo = {
node: null,
actor: aActor.actor,
discardRequestBody: true,
discardResponseBody: true,
startedDateTime: aActor.startedDateTime,
request: {
url: aActor.url,
method: aActor.method,
},
isXHR: aActor.isXHR,
response: {},
timings: {},
updates: [], // track the list of network event updates
private: aActor.private,
};
this._networkRequests[aActor.actor] = networkInfo;
this.outputMessage(CATEGORY_NETWORK, this.logNetEvent, [aActor]);
this.outputMessage(CATEGORY_NETWORK, this.logNetEvent, [networkInfo]);
},
/**
* Handle network event updates coming from the server.
*
* @param string aActorId
* The network event actor ID.
* @param string aType
* Update type.
* @param object aPacket
* @param object networkInfo
* The network request information.
* @param object packet
* Update details.
*/
handleNetworkEventUpdate:
function WCF_handleNetworkEventUpdate(aActorId, aType, aPacket)
handleNetworkEventUpdate: function(networkInfo, packet)
{
let networkInfo = this._networkRequests[aActorId];
if (!networkInfo) {
return;
}
networkInfo.updates.push(aType);
switch (aType) {
case "requestHeaders":
networkInfo.request.headersSize = aPacket.headersSize;
break;
case "requestPostData":
networkInfo.discardRequestBody = aPacket.discardRequestBody;
networkInfo.request.bodySize = aPacket.dataSize;
break;
case "responseStart":
networkInfo.response.httpVersion = aPacket.response.httpVersion;
networkInfo.response.status = aPacket.response.status;
networkInfo.response.statusText = aPacket.response.statusText;
networkInfo.response.headersSize = aPacket.response.headersSize;
networkInfo.discardResponseBody = aPacket.response.discardResponseBody;
break;
case "responseContent":
networkInfo.response.content = {
mimeType: aPacket.mimeType,
};
networkInfo.response.bodySize = aPacket.contentSize;
networkInfo.discardResponseBody = aPacket.discardResponseBody;
break;
case "eventTimings":
networkInfo.totalTime = aPacket.totalTime;
break;
}
if (networkInfo.node && this._updateNetMessage(aActorId)) {
if (networkInfo.node && this._updateNetMessage(packet.from)) {
this.emit("new-messages", new Set([{
update: true,
node: networkInfo.node,
response: aPacket,
response: packet,
}]));
}
@ -1900,7 +1835,7 @@ WebConsoleFrame.prototype = {
*/
_updateNetMessage: function WCF__updateNetMessage(aActorId)
{
let networkInfo = this._networkRequests[aActorId];
let networkInfo = this.webConsoleClient.getNetworkRequest(aActorId);
if (!networkInfo || !networkInfo.node) {
return;
}
@ -2446,8 +2381,8 @@ WebConsoleFrame.prototype = {
else if (typeof methodOrNode != "function") {
connectionId = methodOrNode._connectionId;
}
if (connectionId && connectionId in this._networkRequests) {
delete this._networkRequests[connectionId];
if (connectionId && this.webConsoleClient.hasNetworkRequest(connectionId)) {
this.webConsoleClient.removeNetworkRequest(connectionId);
this._releaseObject(connectionId);
}
}
@ -2524,7 +2459,7 @@ WebConsoleFrame.prototype = {
}
else if (aNode._connectionId &&
aNode.category == CATEGORY_NETWORK) {
delete this._networkRequests[aNode._connectionId];
this.webConsoleClient.removeNetworkRequest(aNode._connectionId);
this._releaseObject(aNode._connectionId);
}
else if (aNode.classList.contains("inlined-variables-view")) {
@ -3018,7 +2953,7 @@ WebConsoleFrame.prototype = {
this._itemDestroyQueue.forEach(this._destroyItem, this);
this._itemDestroyQueue = [];
this._pruneCategoriesQueue = {};
this._networkRequests = {};
this.webConsoleClient.clearNetworkRequests();
if (this._outputTimerInitialized) {
this._outputTimerInitialized = false;
@ -3972,7 +3907,7 @@ JSTerm.prototype = {
hud.groupDepth = 0;
hud._outputQueue.forEach(hud._destroyItem, hud);
hud._outputQueue = [];
hud._networkRequests = {};
this.webConsoleClient.clearNetworkRequests();
hud._repeatNodes = {};
if (aClearStorage) {
@ -5114,8 +5049,6 @@ WebConsoleConnectionProxy.prototype = {
client.addListener("logMessage", this._onLogMessage);
client.addListener("pageError", this._onPageError);
client.addListener("consoleAPICall", this._onConsoleAPICall);
client.addListener("networkEvent", this._onNetworkEvent);
client.addListener("networkEventUpdate", this._onNetworkEventUpdate);
client.addListener("fileActivity", this._onFileActivity);
client.addListener("reflowActivity", this._onReflowActivity);
client.addListener("lastPrivateContextExited", this._onLastPrivateContextExited);
@ -5180,6 +5113,8 @@ WebConsoleConnectionProxy.prototype = {
this.webConsoleClient = aWebConsoleClient;
this._hasNativeConsoleAPI = aResponse.nativeConsoleAPI;
this.webConsoleClient.on("networkEvent", this._onNetworkEvent);
this.webConsoleClient.on("networkEventUpdate", this._onNetworkEventUpdate);
let msgs = ["PageError", "ConsoleAPI"];
this.webConsoleClient.getCachedMessages(msgs, this._onCachedMessages);
@ -5209,7 +5144,10 @@ WebConsoleConnectionProxy.prototype = {
Cu.reportError("Web Console getCachedMessages error: invalid state.");
}
this.owner.displayCachedMessages(aResponse.messages);
let messages = aResponse.messages.concat(...this.webConsoleClient.getNetworkEvents());
messages.sort((a, b) => a.timeStamp - b.timeStamp);
this.owner.displayCachedMessages(messages);
if (!this._hasNativeConsoleAPI) {
this.owner.logWarningAboutReplacedAPI();
@ -5275,15 +5213,15 @@ WebConsoleConnectionProxy.prototype = {
* the UI for displaying.
*
* @private
* @param string aType
* @param string type
* Message type.
* @param object aPacket
* The message received from the server.
* @param object networkInfo
* The network request information.
*/
_onNetworkEvent: function WCCP__onNetworkEvent(aType, aPacket)
_onNetworkEvent: function(type, networkInfo)
{
if (this.owner && aPacket.from == this._consoleActor) {
this.owner.handleNetworkEvent(aPacket.eventActor);
if (this.owner) {
this.owner.handleNetworkEvent(networkInfo);
}
},
@ -5292,16 +5230,17 @@ WebConsoleConnectionProxy.prototype = {
* the UI for displaying.
*
* @private
* @param string aType
* @param string type
* Message type.
* @param object aPacket
* @param object packet
* The message received from the server.
* @param object networkInfo
* The network request information.
*/
_onNetworkEventUpdate: function WCCP__onNetworkEvenUpdatet(aType, aPacket)
_onNetworkEventUpdate: function(type, { packet, networkInfo })
{
if (this.owner) {
this.owner.handleNetworkEventUpdate(aPacket.from, aPacket.updateType,
aPacket);
this.owner.handleNetworkEventUpdate(networkInfo, packet);
}
},
@ -5401,11 +5340,11 @@ WebConsoleConnectionProxy.prototype = {
this.client.removeListener("logMessage", this._onLogMessage);
this.client.removeListener("pageError", this._onPageError);
this.client.removeListener("consoleAPICall", this._onConsoleAPICall);
this.client.removeListener("networkEvent", this._onNetworkEvent);
this.client.removeListener("networkEventUpdate", this._onNetworkEventUpdate);
this.client.removeListener("fileActivity", this._onFileActivity);
this.client.removeListener("reflowActivity", this._onReflowActivity);
this.client.removeListener("lastPrivateContextExited", this._onLastPrivateContextExited);
this.webConsoleClient.off("networkEvent", this._onNetworkEvent);
this.webConsoleClient.off("networkEventUpdate", this._onNetworkEventUpdate);
this.target.off("will-navigate", this._onTabNavigated);
this.target.off("navigate", this._onTabNavigated);

View File

@ -254,10 +254,7 @@ Section "-InstallStartCleanup"
${CleanUpdateDirectories} "Mozilla\Firefox" "Mozilla\updates"
${RemoveDeprecatedFiles}
StrCpy $R2 "false"
StrCpy $R3 "false"
${RemovePrecompleteEntries} "$R2" "$R3"
${RemovePrecompleteEntries} "false"
${If} ${FileExists} "$INSTDIR\defaults\pref\channel-prefs.js"
Delete "$INSTDIR\defaults\pref\channel-prefs.js"

View File

@ -71,6 +71,8 @@ Var CanSetAsDefault
Var InstallCounterStep
Var InstallStepSize
Var InstallTotalSteps
Var ProgressCompleted
Var ProgressTotal
Var TmpVal
Var ExitCode
@ -258,6 +260,7 @@ Var ControlRightPX
!insertmacro IsUserAdmin
!insertmacro RemovePrecompleteEntries
!insertmacro SetBrandNameVars
!insertmacro ITBL3Create
!insertmacro UnloadUAC
VIAddVersionKey "FileDescription" "${BrandShortName} Stub Installer"
@ -1293,6 +1296,9 @@ Function createInstall
StrCpy $InstallTotalSteps ${InstallCleanTotalSteps}
${EndIf}
${ITBL3Create}
${ITBL3SetProgressState} "${TBPF_INDETERMINATE}"
${NSD_CreateTimer} StartDownload ${DownloadIntervalMS}
LockWindow off
@ -1315,6 +1321,21 @@ Function StartDownload
${EndIf}
FunctionEnd
Function SetProgressBars
SendMessage $Progressbar ${PBM_SETPOS} $ProgressCompleted 0
${ITBL3SetProgressValue} "$ProgressCompleted" "$ProgressTotal"
FunctionEnd
Function RemoveFileProgressCallback
IntOp $InstallCounterStep $InstallCounterStep + 2
System::Int64Op $ProgressCompleted + $InstallStepSize
Pop $ProgressCompleted
Call SetProgressBars
System::Int64Op $ProgressCompleted + $InstallStepSize
Pop $ProgressCompleted
Call SetProgressBars
FunctionEnd
Function OnDownload
InetBgDL::GetStats
# $0 = HTTP status code, 0=Completed
@ -1333,6 +1354,7 @@ Function OnDownload
${NSD_AddStyle} $Progressbar ${PBS_MARQUEE}
SendMessage $Progressbar ${PBM_SETMARQUEE} 1 \
$ProgressbarMarqueeIntervalMS ; start=1|stop=0 interval(ms)=+N
${ITBL3SetProgressState} "${TBPF_INDETERMINATE}"
${EndIf}
InetBgDL::Get /RESET /END
StrCpy $DownloadSizeBytes ""
@ -1390,8 +1412,9 @@ Function OnDownload
SendMessage $Progressbar ${PBM_SETMARQUEE} 0 0 ; start=1|stop=0 interval(ms)=+N
${RemoveStyle} $Progressbar ${PBS_MARQUEE}
System::Int64Op $HalfOfDownload + $DownloadSizeBytes
Pop $R9
SendMessage $Progressbar ${PBM_SETRANGE32} 0 $R9
Pop $ProgressTotal
StrCpy $ProgressCompleted 0
SendMessage $Progressbar ${PBM_SETRANGE32} $ProgressCompleted $ProgressTotal
${EndIf}
; Don't update the status until after the download starts
@ -1448,12 +1471,13 @@ Function OnDownload
LockWindow on
; Update the progress bars first in the UI change so they take affect
; before other UI changes.
SendMessage $Progressbar ${PBM_SETPOS} $DownloadSizeBytes 0
StrCpy $ProgressCompleted "$DownloadSizeBytes"
Call SetProgressBars
System::Int64Op $InstallStepSize * ${InstallProgressFirstStep}
Pop $R9
SendMessage $Progressbar ${PBM_SETSTEP} $R9 0
SendMessage $Progressbar ${PBM_STEPIT} 0 0
SendMessage $Progressbar ${PBM_SETSTEP} $InstallStepSize 0
System::Int64Op $ProgressCompleted + $R9
Pop $ProgressCompleted
Call SetProgressBars
ShowWindow $LabelDownloading ${SW_HIDE}
ShowWindow $LabelInstalling ${SW_SHOW}
ShowWindow $LabelBlurb2 ${SW_HIDE}
@ -1540,7 +1564,8 @@ Function OnDownload
WriteIniStr "$0" "TASKBAR" "Migrated" "true"
${EndIf}
${RemovePrecompleteEntries} $Progressbar $InstallCounterStep
GetFunctionAddress $0 RemoveFileProgressCallback
${RemovePrecompleteEntries} $0
; Delete the install.log and let the full installer create it. When the
; installer closes it we can detect that it has completed.
@ -1569,7 +1594,8 @@ Function OnDownload
LockWindow off
${EndIf}
StrCpy $DownloadedBytes "$3"
SendMessage $Progressbar ${PBM_SETPOS} $3 0
StrCpy $ProgressCompleted "$DownloadedBytes"
Call SetProgressBars
${EndIf}
${EndIf}
FunctionEnd
@ -1608,7 +1634,9 @@ Function CheckInstall
Return
${EndIf}
SendMessage $Progressbar ${PBM_STEPIT} 0 0
System::Int64Op $ProgressCompleted + $InstallStepSize
Pop $ProgressCompleted
Call SetProgressBars
${If} ${FileExists} "$INSTDIR\install.log"
Delete "$INSTDIR\install.tmp"
@ -1632,7 +1660,6 @@ Function CheckInstall
Pop $EndInstallPhaseTickCount
System::Int64Op $InstallStepSize * ${InstallProgressFinishStep}
Pop $InstallStepSize
SendMessage $Progressbar ${PBM_SETSTEP} $InstallStepSize 0
${NSD_CreateTimer} FinishInstall ${InstallIntervalMS}
${EndUnless}
${EndIf}
@ -1647,14 +1674,16 @@ Function FinishInstall
${EndIf}
${If} $InstallTotalSteps != $InstallCounterStep
SendMessage $Progressbar ${PBM_STEPIT} 0 0
System::Int64Op $ProgressCompleted + $InstallStepSize
Pop $ProgressCompleted
Call SetProgressBars
Return
${EndIf}
${NSD_KillTimer} FinishInstall
SendMessage $Progressbar ${PBM_GETRANGE} 0 0 $R9
SendMessage $Progressbar ${PBM_SETPOS} $R9 0
StrCpy $ProgressCompleted "$ProgressTotal"
Call SetProgressBars
${If} "$CheckboxSetAsDefault" == "1"
${GetParameters} $0
@ -1935,6 +1964,10 @@ FunctionEnd
Function DisplayDownloadError
${NSD_KillTimer} DisplayDownloadError
; To better display the error state on the taskbar set the progress completed
; value to the total value.
${ITBL3SetProgressValue} "100" "100"
${ITBL3SetProgressState} "${TBPF_ERROR}"
MessageBox MB_OKCANCEL|MB_ICONSTOP "$(ERROR_DOWNLOAD)" IDCANCEL +2 IDOK +1
StrCpy $OpenedDownloadPage "1" ; Already initialized to 0

View File

@ -397,9 +397,7 @@ Section "Uninstall"
${UnregisterDLL} "$INSTDIR\AccessibleMarshal.dll"
${EndIf}
StrCpy $R2 "false"
StrCpy $R3 "false"
${un.RemovePrecompleteEntries} "$R2" "$R3"
${un.RemovePrecompleteEntries} "false"
${If} ${FileExists} "$INSTDIR\defaults\pref\channel-prefs.js"
Delete /REBOOTOK "$INSTDIR\defaults\pref\channel-prefs.js"

View File

@ -37,17 +37,26 @@ xpinstallDisabledButton.accesskey=n
addonDownloadingAndVerifying=Downloading and verifying add-on…;Downloading and verifying #1 add-ons…
addonDownloadVerifying=Verifying
addonInstall.unsigned=(Unverified)
addonInstall.cancelButton.label=Cancel
addonInstall.cancelButton.accesskey=C
addonInstall.acceptButton.label=Install
addonInstall.acceptButton.accesskey=I
# LOCALIZATION NOTE (addonConfirmInstallMessage):
# LOCALIZATION NOTE (addonConfirmInstallMessage,addonConfirmInstallUnsigned):
# Semicolon-separated list of plural forms. See:
# http://developer.mozilla.org/en/docs/Localization_and_Plurals
# #1 is brandShortName
# #2 is the number of add-ons being installed
addonConfirmInstall.message=This site would like to install an add-on in #1:;This site would like to install #2 add-ons in #1:
addonConfirmInstallUnsigned.message=Caution: This site would like to install an unverified add-on in #1. Proceed at your own risk.;Caution: This site would like to install #2 unverified add-ons in #1. Proceed at your own risk.
# LOCALIZATION NOTE (addonConfirmInstallSomeUnsigned.message):
# Semicolon-separated list of plural forms. See:
# http://developer.mozilla.org/en/docs/Localization_and_Plurals
# #1 is brandShortName
# #2 is the total number of add-ons being installed (at least 2)
addonConfirmInstallSomeUnsigned.message=;Caution: This site would like to install #2 add-ons in #1, some of which are unverified. Proceed at your own risk.
addonwatch.slow=%1$S might be making %2$S run slowly
addonwatch.disable.label=Disable %S

View File

@ -340,3 +340,7 @@ context_edit_activate_label=Talk about "{{title}}"
context_edit_name_placeholder=Conversation Name
context_edit_comments_placeholder=Comments
context_add_some_label=Add some context
context_edit_tooltip=Edit Context
context_hide_tooltip=Hide Context
context_show_tooltip=Show Context
context_save_label=Save Context

View File

@ -1862,6 +1862,7 @@ toolbarbutton.chevron > .toolbarbutton-icon {
%include ../shared/devtools/commandline.inc.css
%include ../shared/plugin-doorhanger.inc.css
%include ../shared/badcontent-doorhanger.inc.css
%include ../shared/login-doorhanger.inc.css
%include downloads/indicator.css

View File

@ -1,11 +0,0 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
@import url("chrome://global/skin/inContentUI.css");
#downloadsListEmptyDescription {
margin: 1em;
text-align: center;
color: GrayText;
}

View File

@ -134,7 +134,7 @@ browser.jar:
skin/classic/browser/customizableui/whimsy-bw@2x.png (../shared/customizableui/whimsy-bw@2x.png)
skin/classic/browser/downloads/allDownloadsViewOverlay.css (downloads/allDownloadsViewOverlay.css)
skin/classic/browser/downloads/buttons.png (downloads/buttons.png)
skin/classic/browser/downloads/contentAreaDownloadsView.css (downloads/contentAreaDownloadsView.css)
skin/classic/browser/downloads/contentAreaDownloadsView.css (../shared/downloads/contentAreaDownloadsView.css)
skin/classic/browser/downloads/download-glow-menuPanel.png (downloads/download-glow-menuPanel.png)
skin/classic/browser/downloads/download-notification-finish.png (downloads/download-notification-finish.png)
skin/classic/browser/downloads/download-notification-start.png (downloads/download-notification-start.png)

View File

@ -3895,6 +3895,7 @@ notification[value="loop-sharing-notification"] .messageImage {
%include ../shared/devtools/commandline.inc.css
%include ../shared/plugin-doorhanger.inc.css
%include ../shared/badcontent-doorhanger.inc.css
%include ../shared/login-doorhanger.inc.css
%include downloads/indicator.css

View File

@ -215,7 +215,7 @@ browser.jar:
skin/classic/browser/downloads/allDownloadsViewOverlay.css (downloads/allDownloadsViewOverlay.css)
skin/classic/browser/downloads/buttons.png (downloads/buttons.png)
skin/classic/browser/downloads/buttons@2x.png (downloads/buttons@2x.png)
skin/classic/browser/downloads/contentAreaDownloadsView.css (downloads/contentAreaDownloadsView.css)
skin/classic/browser/downloads/contentAreaDownloadsView.css (../shared/downloads/contentAreaDownloadsView.css)
skin/classic/browser/downloads/download-glow-menuPanel.png (downloads/download-glow-menuPanel.png)
skin/classic/browser/downloads/download-glow-menuPanel@2x.png (downloads/download-glow-menuPanel@2x.png)
skin/classic/browser/downloads/download-notification-finish.png (downloads/download-notification-finish.png)

View File

@ -256,6 +256,11 @@ panelview:not([mainview]) .toolbarbutton-text,
padding: 4px 0;
}
.cui-widget-panel[viewId="PanelUI-pocketView"] > .panel-arrowcontainer > .panel-arrowcontent {
padding-top: 0;
padding-bottom: 0;
}
.cui-widget-panel.cui-widget-panelWithFooter > .panel-arrowcontainer > .panel-arrowcontent {
padding-bottom: 0;
}

View File

@ -2,17 +2,25 @@
* 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/. */
@import url("chrome://global/skin/inContentUI.css");
@import url("chrome://global/skin/in-content/common.css");
.downloadButton {
box-shadow: none;
#contentAreaDownloadsView {
padding: 18px;
}
#downloadsRichListBox:not(:-moz-focusring) {
border-color: transparent;
}
.downloadButton:not([disabled="true"]):hover,
.downloadButton:not([disabled="true"]):hover:active,
.downloadButton:not([disabled]):hover:active {
background: transparent;
border: none;
box-shadow: none;
}
.downloadButton > .button-box {
padding-bottom: 0;
}
#downloadsListEmptyDescription {

View File

@ -0,0 +1,19 @@
#login-fill-testing {
color: #b33;
font-weight: bold;
}
#login-fill-list {
border: 1px solid black;
max-height: 20em;
}
.login-hostname {
margin: 4px;
font-weight: bold;
}
.login-username {
margin: 4px;
color: #888;
}

View File

@ -2456,6 +2456,7 @@ notification[value="loop-sharing-notification"] .messageImage {
%include ../shared/devtools/commandline.inc.css
%include ../shared/plugin-doorhanger.inc.css
%include ../shared/badcontent-doorhanger.inc.css
%include ../shared/login-doorhanger.inc.css
%include downloads/indicator.css

View File

@ -1,22 +0,0 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
@import url("chrome://global/skin/inContentUI.css");
.downloadButton {
box-shadow: none;
}
.downloadButton:not([disabled="true"]):hover:active,
.downloadButton:not([disabled]):hover:active {
background: transparent;
border: none;
box-shadow: none;
}
#downloadsListEmptyDescription {
margin: 1em;
text-align: center;
color: GrayText;
}

View File

@ -173,7 +173,7 @@ browser.jar:
skin/classic/browser/downloads/allDownloadsViewOverlay.css (downloads/allDownloadsViewOverlay.css)
skin/classic/browser/downloads/buttons.png (downloads/buttons.png)
skin/classic/browser/downloads/buttons-XP.png (downloads/buttons-XP.png)
skin/classic/browser/downloads/contentAreaDownloadsView.css (downloads/contentAreaDownloadsView.css)
skin/classic/browser/downloads/contentAreaDownloadsView.css (../shared/downloads/contentAreaDownloadsView.css)
skin/classic/browser/downloads/download-glow-menuPanel-XPVista7.png (downloads/download-glow-menuPanel-XPVista7.png)
skin/classic/browser/downloads/download-notification-finish.png (downloads/download-notification-finish.png)
skin/classic/browser/downloads/download-notification-start.png (downloads/download-notification-start.png)

View File

@ -3519,10 +3519,10 @@ AC_SUBST(NIGHTLY_BUILD)
AC_SUBST(RELEASE_BUILD)
dnl ========================================================
dnl Multiprocess Firefox Nightly Testing UI
dnl Multiprocess Firefox Testing UI - Nightly and Aurora
dnl To be removed in Bug 1003313
dnl ========================================================
if test -n "$NIGHTLY_BUILD"; then
if test -z "$RELEASE_BUILD"; then
E10S_TESTING_ONLY=1
AC_DEFINE(E10S_TESTING_ONLY)
fi

View File

@ -2918,7 +2918,16 @@ ContentChild::RecvInvokeDragSession(nsTArray<IPCDataTransfer>&& aTransfers,
} else if (item.data().type() == IPCDataTransferData::TPBlobChild) {
BlobChild* blob = static_cast<BlobChild*>(item.data().get_PBlobChild());
nsRefPtr<FileImpl> fileImpl = blob->GetBlobImpl();
variant->SetAsISupports(fileImpl);
nsString path;
ErrorResult result;
fileImpl->GetMozFullPathInternal(path, result);
if (result.Failed()) {
variant->SetAsISupports(fileImpl);
} else {
nsCOMPtr<nsIFile> file;
NS_NewNativeLocalFile(NS_ConvertUTF16toUTF8(path), true, getter_AddRefs(file));
variant->SetAsISupports(file);
}
} else {
continue;
}

View File

@ -18,7 +18,7 @@ import com.nineoldandroids.animation.ValueAnimator;
* After constructing an instance, animations can be queued up sequentially with the
* {@link #queue(Attributes) queue} method.
*/
public class BounceAnimator extends ValueAnimator {
public class BounceAnimatorBuilder extends ValueAnimator {
public static final class Attributes {
public final float value;
@ -34,7 +34,7 @@ public class BounceAnimator extends ValueAnimator {
private final String mPropertyName;
private final List<Animator> animatorChain = new LinkedList<Animator>();
public BounceAnimator(View view, String property) {
public BounceAnimatorBuilder(View view, String property) {
mView = view;
mPropertyName = property;
}
@ -46,10 +46,9 @@ public class BounceAnimator extends ValueAnimator {
animatorChain.add(animator);
}
@Override
public void start() {
public AnimatorSet build(){
AnimatorSet animatorSet = new AnimatorSet();
animatorSet.playSequentially(animatorChain);
animatorSet.start();
return animatorSet;
}
}

View File

@ -6,8 +6,8 @@
package org.mozilla.gecko.home;
import org.mozilla.gecko.R;
import org.mozilla.gecko.animation.BounceAnimator;
import org.mozilla.gecko.animation.BounceAnimator.Attributes;
import org.mozilla.gecko.animation.BounceAnimatorBuilder;
import org.mozilla.gecko.animation.BounceAnimatorBuilder.Attributes;
import org.mozilla.gecko.animation.TransitionsTracker;
import android.content.Context;
@ -33,12 +33,11 @@ import com.nineoldandroids.view.ViewHelper;
class HomePagerTabStrip extends PagerTabStrip {
private static final String LOGTAG = "PagerTabStrip";
private static final int ANIMATION_DELAY_MS = 250;
private static final int ANIMATION_DELAY_MS = 50;
private static final int ALPHA_MS = 10;
private static final int BOUNCE1_MS = 350;
private static final int BOUNCE2_MS = 200;
private static final int BOUNCE3_MS = 100;
private static final int BOUNCE4_MS = 100;
private static final int INIT_OFFSET = 100;
private final Paint shadowPaint;
@ -103,30 +102,33 @@ class HomePagerTabStrip extends PagerTabStrip {
final AnimatorSet alphaAnimatorSet = new AnimatorSet();
alphaAnimatorSet.playTogether(alpha1, alpha2);
alphaAnimatorSet.setStartDelay(ANIMATION_DELAY_MS);
alphaAnimatorSet.setDuration(ALPHA_MS);
alphaAnimatorSet.setStartDelay(ANIMATION_DELAY_MS);
// Bounce animation.
final float bounceDistance = getWidth()/100f; // Hack: TextFields still have 0 width here.
final BounceAnimator prevBounceAnimator = new BounceAnimator(prevTextView, "translationX");
prevBounceAnimator.queue(new Attributes(bounceDistance, BOUNCE1_MS));
prevBounceAnimator.queue(new Attributes(-bounceDistance/4, BOUNCE2_MS));
prevBounceAnimator.queue(new Attributes(0, BOUNCE4_MS));
prevBounceAnimator.setStartDelay(ANIMATION_DELAY_MS);
final BounceAnimatorBuilder prevBounceAnimatorBuilder = new BounceAnimatorBuilder(prevTextView, "translationX");
prevBounceAnimatorBuilder.queue(new Attributes(bounceDistance, BOUNCE1_MS));
prevBounceAnimatorBuilder.queue(new Attributes(-bounceDistance/4, BOUNCE2_MS));
prevBounceAnimatorBuilder.queue(new Attributes(0, BOUNCE3_MS));
final BounceAnimator nextBounceAnimator = new BounceAnimator(nextTextView, "translationX");
nextBounceAnimator.queue(new Attributes(-bounceDistance, BOUNCE1_MS));
nextBounceAnimator.queue(new Attributes(bounceDistance/4, BOUNCE2_MS));
nextBounceAnimator.queue(new Attributes(0, BOUNCE4_MS));
nextBounceAnimator.setStartDelay(ANIMATION_DELAY_MS);
final BounceAnimatorBuilder nextBounceAnimatorBuilder = new BounceAnimatorBuilder(nextTextView, "translationX");
nextBounceAnimatorBuilder.queue(new Attributes(-bounceDistance, BOUNCE1_MS));
nextBounceAnimatorBuilder.queue(new Attributes(bounceDistance/4, BOUNCE2_MS));
nextBounceAnimatorBuilder.queue(new Attributes(0, BOUNCE3_MS));
TransitionsTracker.track(nextBounceAnimator);
final AnimatorSet bounceAnimatorSet = new AnimatorSet();
bounceAnimatorSet.playTogether(prevBounceAnimatorBuilder.build(), nextBounceAnimatorBuilder.build());
TransitionsTracker.track(nextBounceAnimatorBuilder);
final AnimatorSet titlesAnimatorSet = new AnimatorSet();
titlesAnimatorSet.playTogether(alphaAnimatorSet, bounceAnimatorSet);
titlesAnimatorSet.setStartDelay(ANIMATION_DELAY_MS);
// Start animations.
alphaAnimatorSet.start();
prevBounceAnimator.start();
nextBounceAnimator.start();
titlesAnimatorSet.start();
}
private class PreDrawListener implements ViewTreeObserver.OnPreDrawListener {

View File

@ -144,7 +144,7 @@ gbjar.sources += [
'AndroidGamepadManager.java',
'animation/AnimationUtils.java',
'animation/AnimatorProxy.java',
'animation/BounceAnimator.java',
'animation/BounceAnimatorBuilder.java',
'animation/HeightChangeAnimation.java',
'animation/PropertyAnimator.java',
'animation/Rotate3DAnimation.java',

View File

@ -216,7 +216,7 @@ public class PromptInput {
input.setCurrentMinute(calendar.get(GregorianCalendar.MINUTE));
mView = (View)input;
} else if (mType.equals("datetime-local") || mType.equals("datetime")) {
DateTimePicker input = new DateTimePicker(context, "yyyy-MM-dd HH:mm", mValue,
DateTimePicker input = new DateTimePicker(context, "yyyy-MM-dd HH:mm", mValue.replace("T"," ").replace("Z", ""),
DateTimePicker.PickersState.DATETIME);
input.toggleCalendar(true);
mView = (View)input;
@ -255,11 +255,11 @@ public class PromptInput {
} else if (mType.equals("week")) {
return formatDateString("yyyy-'W'ww",calendar);
} else if (mType.equals("datetime-local")) {
return formatDateString("yyyy-MM-dd HH:mm",calendar);
return formatDateString("yyyy-MM-dd'T'HH:mm",calendar);
} else if (mType.equals("datetime")) {
calendar.set(GregorianCalendar.ZONE_OFFSET,0);
calendar.setTimeInMillis(dp.getTimeInMillis());
return formatDateString("yyyy-MM-dd HH:mm",calendar);
return formatDateString("yyyy-MM-dd'T'HH:mm'Z'",calendar);
} else if (mType.equals("month")) {
return formatDateString("yyyy-MM",calendar);
}

View File

@ -173,7 +173,7 @@ var gMessageIds = [];
* - text (string): Text to show as banner message
* - url (string): URL to open when banner is clicked
* - icon (data URI): Icon to appear in banner
* - target_geo (string): Country code for where this message should be shown (e.g. "US")
* - countries (list of strings): Country codes for where this message should be shown (e.g. ["US", "GR"])
*/
function updateBanner(messages) {
// Remove the current messages, if there are any.
@ -194,9 +194,10 @@ function updateBanner(messages) {
messages.forEach(function(message) {
// Don't add this message to the banner if it's not supposed to be shown in this country.
if ("target_geo" in message && message.target_geo != gCountryCode) {
if ("countries" in message && message.countries.indexOf(gCountryCode) === -1) {
return;
}
let id = Home.banner.add({
text: message.text,
icon: message.icon,

View File

@ -533,6 +533,9 @@ extract_signature(const char *src, uint32_t sigIndex, const char * dest)
/* Skip to the correct signature */
for (i = 0; i <= sigIndex; i++) {
/* Avoid leaking while skipping signatures */
free(extractedSignature);
/* skip past the signature algorithm ID */
if (fseeko(fpSrc, sizeof(uint32_t), SEEK_CUR)) {
fprintf(stderr, "ERROR: Could not seek past sig algorithm ID.\n");

View File

@ -51,9 +51,10 @@ mar_read_entire_file(const char * filePath, uint32_t maxSize,
}
}
}
fclose(f);
}
fclose(f);
return result;
}

View File

@ -20,6 +20,7 @@
#include "nsIFileStreams.h"
#include "nsIInputStream.h"
#include "nsIStringEnumerator.h"
#include "nsIDirectoryEnumerator.h"
#include "nsIZipReader.h"
#include "nsNetUtil.h"
#include "nsNSSCertificate.h"
@ -98,7 +99,7 @@ ReadStream(const nsCOMPtr<nsIInputStream>& stream, /*out*/ SECItem& buf)
return NS_OK;
}
// Finds exactly one (signature metadata) entry that matches the given
// Finds exactly one (signature metadata) JAR entry that matches the given
// search pattern, and then load it. Fails if there are no matches or if
// there is more than one match. If bugDigest is not null then on success
// bufDigest will contain the SHA-1 digeset of the entry.
@ -153,27 +154,22 @@ FindAndLoadOneEntry(nsIZipReader * zip,
// at once, which would require memory in proportion to the size of the largest
// entry. Instead, we require only a small, fixed amount of memory.
//
// @param stream an input stream from a JAR entry or file depending on whether
// it is from a signed archive or unpacked into a directory
// @param digestFromManifest The digest that we're supposed to check the file's
// contents against, from the manifest
// @param buf A scratch buffer that we use for doing the I/O, which must have
// already been allocated. The size of this buffer is the unit
// size of our I/O.
nsresult
VerifyEntryContentDigest(nsIZipReader * zip, const nsACString & aFilename,
const SECItem & digestFromManifest, SECItem & buf)
VerifyStreamContentDigest(nsIInputStream* stream,
const SECItem& digestFromManifest, SECItem& buf)
{
MOZ_ASSERT(buf.len > 0);
if (digestFromManifest.len != SHA1_LENGTH)
return NS_ERROR_SIGNED_JAR_MANIFEST_INVALID;
nsresult rv;
nsCOMPtr<nsIInputStream> stream;
rv = zip->GetInputStream(aFilename, getter_AddRefs(stream));
if (NS_FAILED(rv)) {
return NS_ERROR_SIGNED_JAR_ENTRY_MISSING;
}
uint64_t len64;
rv = stream->Available(&len64);
NS_ENSURE_SUCCESS(rv, rv);
@ -226,6 +222,81 @@ VerifyEntryContentDigest(nsIZipReader * zip, const nsACString & aFilename,
return NS_OK;
}
nsresult
VerifyEntryContentDigest(nsIZipReader* zip, const nsACString& aFilename,
const SECItem& digestFromManifest, SECItem& buf)
{
nsCOMPtr<nsIInputStream> stream;
nsresult rv = zip->GetInputStream(aFilename, getter_AddRefs(stream));
if (NS_FAILED(rv)) {
return NS_ERROR_SIGNED_JAR_ENTRY_MISSING;
}
return VerifyStreamContentDigest(stream, digestFromManifest, buf);
}
// @oaram aDir directory containing the unpacked signed archive
// @param aFilename path of the target file relative to aDir
// @param digestFromManifest The digest that we're supposed to check the file's
// contents against, from the manifest
// @param buf A scratch buffer that we use for doing the I/O
nsresult
VerifyFileContentDigest(nsIFile* aDir, const nsAString& aFilename,
const SECItem& digestFromManifest, SECItem& buf)
{
// Find the file corresponding to the manifest path
nsCOMPtr<nsIFile> file;
nsresult rv = aDir->Clone(getter_AddRefs(file));
if (NS_FAILED(rv)) {
return rv;
}
// We don't know how to handle JARs with signed directory entries.
// It's technically possible in the manifest but makes no sense on disk.
// Inside an archive we just ignore them, but here we have to treat it
// as an error because the signed bytes never got unpacked.
int32_t pos = 0;
int32_t slash;
int32_t namelen = aFilename.Length();
if (namelen == 0 || aFilename[namelen - 1] == '/') {
return NS_ERROR_SIGNED_JAR_ENTRY_INVALID;
}
// Append path segments one by one
do {
slash = aFilename.FindChar('/', pos);
int32_t segend = (slash == kNotFound) ? namelen : slash;
rv = file->Append(Substring(aFilename, pos, (segend - pos)));
if (NS_FAILED(rv)) {
return NS_ERROR_SIGNED_JAR_ENTRY_INVALID;
}
pos = slash + 1;
} while (pos < namelen && slash != kNotFound);
bool exists;
rv = file->Exists(&exists);
if (NS_FAILED(rv) || !exists) {
return NS_ERROR_SIGNED_JAR_ENTRY_MISSING;
}
bool isDir;
rv = file->IsDirectory(&isDir);
if (NS_FAILED(rv) || isDir) {
// We only support signed files, not directory entries
return NS_ERROR_SIGNED_JAR_ENTRY_INVALID;
}
// Open an input stream for that file and verify it.
nsCOMPtr<nsIInputStream> stream;
rv = NS_NewLocalFileInputStream(getter_AddRefs(stream), file, -1, -1,
nsIFileInputStream::CLOSE_ON_EOF);
if (NS_FAILED(rv) || !stream) {
return NS_ERROR_SIGNED_JAR_ENTRY_MISSING;
}
return VerifyStreamContentDigest(stream, digestFromManifest, buf);
}
// On input, nextLineStart is the start of the current line. On output,
// nextLineStart is the start of the next line.
nsresult
@ -286,6 +357,7 @@ ReadLine(/*in/out*/ const char* & nextLineStart, /*out*/ nsCString & line,
#define JAR_MF_SEARCH_STRING "(M|/M)ETA-INF/(M|m)(ANIFEST|anifest).(MF|mf)$"
#define JAR_SF_SEARCH_STRING "(M|/M)ETA-INF/*.(SF|sf)$"
#define JAR_RSA_SEARCH_STRING "(M|/M)ETA-INF/*.(RSA|rsa)$"
#define JAR_META_DIR "META-INF"
#define JAR_MF_HEADER "Manifest-Version: 1.0"
#define JAR_SF_HEADER "Signature-Version: 1.0"
@ -949,3 +1021,497 @@ nsNSSCertificateDB::VerifySignedManifestAsync(
aSignatureStream, aCallback));
return task->Dispatch("SignedManifest");
}
//
// Signature verification for archives unpacked into a file structure
//
// Finds the "*.rsa" signature file in the META-INF directory and returns
// the name. It is an error if there are none or more than one .rsa file
nsresult
FindSignatureFilename(nsIFile* aMetaDir,
/*out*/ nsAString& aFilename)
{
nsCOMPtr<nsISimpleEnumerator> entries;
nsresult rv = aMetaDir->GetDirectoryEntries(getter_AddRefs(entries));
nsCOMPtr<nsIDirectoryEnumerator> files = do_QueryInterface(entries);
if (NS_FAILED(rv) || !files) {
return NS_ERROR_SIGNED_JAR_NOT_SIGNED;
}
bool found = false;
nsCOMPtr<nsIFile> file;
rv = files->GetNextFile(getter_AddRefs(file));
while (NS_SUCCEEDED(rv) && file) {
nsAutoString leafname;
rv = file->GetLeafName(leafname);
if (NS_SUCCEEDED(rv)) {
if (StringEndsWith(leafname, NS_LITERAL_STRING(".rsa"))) {
if (!found) {
found = true;
aFilename = leafname;
} else {
// second signature file is an error
rv = NS_ERROR_SIGNED_JAR_MANIFEST_INVALID;
break;
}
}
rv = files->GetNextFile(getter_AddRefs(file));
}
}
if (!found) {
rv = NS_ERROR_SIGNED_JAR_NOT_SIGNED;
}
files->Close();
return rv;
}
// Loads the signature metadata file that matches the given filename in
// the passed-in Meta-inf directory. If bufDigest is not null then on
// success bufDigest will contain the SHA-1 digest of the entry.
nsresult
LoadOneMetafile(nsIFile* aMetaDir,
const nsAString& aFilename,
/*out*/ SECItem& aBuf,
/*optional, out*/ Digest* aBufDigest)
{
nsCOMPtr<nsIFile> metafile;
nsresult rv = aMetaDir->Clone(getter_AddRefs(metafile));
NS_ENSURE_SUCCESS(rv, rv);
rv = metafile->Append(aFilename);
NS_ENSURE_SUCCESS(rv, rv);
bool exists;
rv = metafile->Exists(&exists);
if (NS_FAILED(rv) || !exists) {
// we can call a missing .rsa file "unsigned" but FindSignatureFilename()
// already found one: missing other metadata files means a broken signature.
return NS_ERROR_SIGNED_JAR_MANIFEST_INVALID;
}
nsCOMPtr<nsIInputStream> stream;
rv = NS_NewLocalFileInputStream(getter_AddRefs(stream), metafile);
NS_ENSURE_SUCCESS(rv, rv);
rv = ReadStream(stream, aBuf);
stream->Close();
NS_ENSURE_SUCCESS(rv, rv);
if (aBufDigest) {
rv = aBufDigest->DigestBuf(SEC_OID_SHA1, aBuf.data, aBuf.len - 1);
NS_ENSURE_SUCCESS(rv, rv);
}
return NS_OK;
}
// Parses MANIFEST.MF and verifies the contents of the unpacked files
// listed in the manifest.
// The filenames of all entries will be returned in aMfItems. aBuf must
// be a pre-allocated scratch buffer that is used for doing I/O.
nsresult
ParseMFUnpacked(const char* aFilebuf, nsIFile* aDir,
/*out*/ nsTHashtable<nsStringHashKey>& aMfItems,
ScopedAutoSECItem& aBuf)
{
nsresult rv;
const char* nextLineStart = aFilebuf;
rv = CheckManifestVersion(nextLineStart, NS_LITERAL_CSTRING(JAR_MF_HEADER));
if (NS_FAILED(rv)) {
return rv;
}
// Skip the rest of the header section, which ends with a blank line.
{
nsAutoCString line;
do {
rv = ReadLine(nextLineStart, line);
if (NS_FAILED(rv)) {
return rv;
}
} while (line.Length() > 0);
// Manifest containing no file entries is OK, though useless.
if (*nextLineStart == '\0') {
return NS_OK;
}
}
nsAutoString curItemName;
ScopedAutoSECItem digest;
for (;;) {
nsAutoCString curLine;
rv = ReadLine(nextLineStart, curLine);
if (NS_FAILED(rv)) {
return rv;
}
if (curLine.Length() == 0) {
// end of section (blank line or end-of-file)
if (curItemName.Length() == 0) {
// '...Each section must start with an attribute with the name as
// "Name",...', so every section must have a Name attribute.
return NS_ERROR_SIGNED_JAR_MANIFEST_INVALID;
}
if (digest.len == 0) {
// We require every entry to have a digest, since we require every
// entry to be signed and we don't allow duplicate entries.
return NS_ERROR_SIGNED_JAR_MANIFEST_INVALID;
}
if (aMfItems.Contains(curItemName)) {
// Duplicate entry
return NS_ERROR_SIGNED_JAR_MANIFEST_INVALID;
}
// Verify that the file's content digest matches the digest from this
// MF section.
rv = VerifyFileContentDigest(aDir, curItemName, digest, aBuf);
if (NS_FAILED(rv)) {
return rv;
}
aMfItems.PutEntry(curItemName);
if (*nextLineStart == '\0') {
// end-of-file
break;
}
// reset so we know we haven't encountered either of these for the next
// item yet.
curItemName.Truncate();
digest.reset();
continue; // skip the rest of the loop below
}
nsAutoCString attrName;
nsAutoCString attrValue;
rv = ParseAttribute(curLine, attrName, attrValue);
if (NS_FAILED(rv)) {
return rv;
}
// Lines to look for:
// (1) Digest:
if (attrName.LowerCaseEqualsLiteral("sha1-digest")) {
if (digest.len > 0) {
// multiple SHA1 digests in section
return NS_ERROR_SIGNED_JAR_MANIFEST_INVALID;
}
rv = MapSECStatus(ATOB_ConvertAsciiToItem(&digest, attrValue.get()));
if (NS_FAILED(rv)) {
return NS_ERROR_SIGNED_JAR_MANIFEST_INVALID;
}
continue;
}
// (2) Name: associates this manifest section with a file in the jar.
if (attrName.LowerCaseEqualsLiteral("name")) {
if (MOZ_UNLIKELY(curItemName.Length() > 0)) {
// multiple names in section
return NS_ERROR_SIGNED_JAR_MANIFEST_INVALID;
}
if (MOZ_UNLIKELY(attrValue.Length() == 0)) {
return NS_ERROR_SIGNED_JAR_MANIFEST_INVALID;
}
curItemName = NS_ConvertUTF8toUTF16(attrValue);
continue;
}
// (3) Magic: the only other must-understand attribute
if (attrName.LowerCaseEqualsLiteral("magic")) {
// We don't understand any magic, so we can't verify an entry that
// requires magic. Since we require every entry to have a valid
// signature, we have no choice but to reject the entry.
return NS_ERROR_SIGNED_JAR_MANIFEST_INVALID;
}
// unrecognized attributes must be ignored
}
return NS_OK;
}
// recursively check a directory tree for files not in the list of
// verified files we found in the manifest. For each file we find
// Check it against the files found in the manifest. If the file wasn't
// in the manifest then it's unsigned and we can stop looking. Otherwise
// remove it from the collection so we can check leftovers later.
//
// @param aDir Directory to check
// @param aPath Relative path to that directory (to check against aItems)
// @param aItems All the files found
// @param *Filename signature files that won't be in the manifest
nsresult
CheckDirForUnsignedFiles(nsIFile* aDir,
const nsString& aPath,
/* in/out */ nsTHashtable<nsStringHashKey>& aItems,
const nsAString& sigFilename,
const nsAString& sfFilename,
const nsAString& mfFilename)
{
nsCOMPtr<nsISimpleEnumerator> entries;
nsresult rv = aDir->GetDirectoryEntries(getter_AddRefs(entries));
nsCOMPtr<nsIDirectoryEnumerator> files = do_QueryInterface(entries);
if (NS_FAILED(rv) || !files) {
return NS_ERROR_SIGNED_JAR_ENTRY_MISSING;
}
bool inMeta = StringBeginsWith(aPath, NS_LITERAL_STRING(JAR_META_DIR));
while (NS_SUCCEEDED(rv)) {
nsCOMPtr<nsIFile> file;
rv = files->GetNextFile(getter_AddRefs(file));
if (NS_FAILED(rv) || !file) {
break;
}
nsAutoString leafname;
rv = file->GetLeafName(leafname);
if (NS_FAILED(rv)) {
return rv;
}
nsAutoString curName(aPath + leafname);
bool isDir;
rv = file->IsDirectory(&isDir);
if (NS_FAILED(rv)) {
return rv;
}
// if it's a directory we need to recurse
if (isDir) {
curName.Append(NS_LITERAL_STRING("/"));
rv = CheckDirForUnsignedFiles(file, curName, aItems,
sigFilename, sfFilename, mfFilename);
} else {
// The files that comprise the signature mechanism are not covered by the
// signature.
//
// XXX: This is OK for a single signature, but doesn't work for
// multiple signatures because the metadata for the other signatures
// is not signed either.
if (inMeta && ( leafname == sigFilename ||
leafname == sfFilename ||
leafname == mfFilename )) {
continue;
}
// make sure the current file was found in the manifest
nsStringHashKey* item = aItems.GetEntry(curName);
if (!item) {
return NS_ERROR_SIGNED_JAR_UNSIGNED_ENTRY;
}
// Remove the item so we can check for leftover items later
aItems.RemoveEntry(curName);
}
}
files->Close();
return rv;
}
/*
* Verify the signature of a directory structure as if it were a
* signed JAR file (used for unpacked JARs)
*/
nsresult
VerifySignedDirectory(AppTrustedRoot aTrustedRoot,
nsIFile* aDirectory,
/*out, optional */ nsIX509Cert** aSignerCert)
{
NS_ENSURE_ARG_POINTER(aDirectory);
if (aSignerCert) {
*aSignerCert = nullptr;
}
// Make sure there's a META-INF directory
nsCOMPtr<nsIFile> metaDir;
nsresult rv = aDirectory->Clone(getter_AddRefs(metaDir));
if (NS_FAILED(rv)) {
return rv;
}
rv = metaDir->Append(NS_LITERAL_STRING(JAR_META_DIR));
if (NS_FAILED(rv)) {
return rv;
}
bool exists;
rv = metaDir->Exists(&exists);
if (NS_FAILED(rv) || !exists) {
return NS_ERROR_SIGNED_JAR_NOT_SIGNED;
}
bool isDirectory;
rv = metaDir->IsDirectory(&isDirectory);
if (NS_FAILED(rv) || !isDirectory) {
return NS_ERROR_SIGNED_JAR_NOT_SIGNED;
}
// Find and load the Signature (RSA) file
nsAutoString sigFilename;
rv = FindSignatureFilename(metaDir, sigFilename);
if (NS_FAILED(rv)) {
return rv;
}
ScopedAutoSECItem sigBuffer;
rv = LoadOneMetafile(metaDir, sigFilename, sigBuffer, nullptr);
if (NS_FAILED(rv)) {
return NS_ERROR_SIGNED_JAR_NOT_SIGNED;
}
// Load the signature (SF) file and verify the signature.
// The .sf and .rsa files must have the same name apart from the extension.
nsAutoString sfFilename(Substring(sigFilename, 0, sigFilename.Length() - 3)
+ NS_LITERAL_STRING("sf"));
ScopedAutoSECItem sfBuffer;
Digest sfCalculatedDigest;
rv = LoadOneMetafile(metaDir, sfFilename, sfBuffer, &sfCalculatedDigest);
if (NS_FAILED(rv)) {
return NS_ERROR_SIGNED_JAR_MANIFEST_INVALID;
}
sigBuffer.type = siBuffer;
ScopedCERTCertList builtChain;
rv = VerifySignature(aTrustedRoot, sigBuffer, sfCalculatedDigest.get(),
builtChain);
if (NS_FAILED(rv)) {
return NS_ERROR_SIGNED_JAR_MANIFEST_INVALID;
}
// Get the expected manifest hash from the signed .sf file
ScopedAutoSECItem mfDigest;
rv = ParseSF(char_ptr_cast(sfBuffer.data), mfDigest);
if (NS_FAILED(rv)) {
return NS_ERROR_SIGNED_JAR_MANIFEST_INVALID;
}
// Load manifest (MF) file and verify signature
nsAutoString mfFilename(NS_LITERAL_STRING("manifest.mf"));
ScopedAutoSECItem manifestBuffer;
Digest mfCalculatedDigest;
rv = LoadOneMetafile(metaDir, mfFilename, manifestBuffer, &mfCalculatedDigest);
if (NS_FAILED(rv)) {
return NS_ERROR_SIGNED_JAR_MANIFEST_INVALID;
}
if (SECITEM_CompareItem(&mfDigest, &mfCalculatedDigest.get()) != SECEqual) {
return NS_ERROR_SIGNED_JAR_MANIFEST_INVALID;
}
// Parse manifest and verify signed hash of all listed files
// Allocate the I/O buffer only once per JAR, instead of once per entry, in
// order to minimize malloc/free calls and in order to avoid fragmenting
// memory.
ScopedAutoSECItem buf(128 * 1024);
nsTHashtable<nsStringHashKey> items;
rv = ParseMFUnpacked(char_ptr_cast(manifestBuffer.data),
aDirectory, items, buf);
if (NS_FAILED(rv)){
return rv;
}
// We've checked that everything listed in the manifest exists and is signed
// correctly. Now check on disk for extra (unsigned) files.
// Deletes found entries from items as it goes.
rv = CheckDirForUnsignedFiles(aDirectory, EmptyString(), items,
sigFilename, sfFilename, mfFilename);
if (NS_FAILED(rv)) {
return rv;
}
// We verified that every entry that we require to be signed is signed. But,
// were there any missing entries--that is, entries that are mentioned in the
// manifest but missing from the directory tree? (There shouldn't be given
// ParseMFUnpacked() checking them all, but it's a cheap sanity check.)
if (items.Count() != 0) {
return NS_ERROR_SIGNED_JAR_ENTRY_MISSING;
}
// Return the signer's certificate to the reader if they want it.
// XXX: We should return an nsIX509CertList with the whole validated chain.
if (aSignerCert) {
MOZ_ASSERT(CERT_LIST_HEAD(builtChain));
nsCOMPtr<nsIX509Cert> signerCert =
nsNSSCertificate::Create(CERT_LIST_HEAD(builtChain)->cert);
NS_ENSURE_TRUE(signerCert, NS_ERROR_OUT_OF_MEMORY);
signerCert.forget(aSignerCert);
}
return NS_OK;
}
class VerifySignedDirectoryTask final : public CryptoTask
{
public:
VerifySignedDirectoryTask(AppTrustedRoot aTrustedRoot, nsIFile* aUnpackedJar,
nsIVerifySignedDirectoryCallback* aCallback)
: mTrustedRoot(aTrustedRoot)
, mDirectory(aUnpackedJar)
, mCallback(new nsMainThreadPtrHolder<nsIVerifySignedDirectoryCallback>(aCallback))
{
}
private:
virtual nsresult CalculateResult() override
{
return VerifySignedDirectory(mTrustedRoot,
mDirectory,
getter_AddRefs(mSignerCert));
}
// This class doesn't directly hold NSS resources so there's nothing that
// needs to be released
virtual void ReleaseNSSResources() override { }
virtual void CallCallback(nsresult rv) override
{
(void) mCallback->VerifySignedDirectoryFinished(rv, mSignerCert);
}
const AppTrustedRoot mTrustedRoot;
const nsCOMPtr<nsIFile> mDirectory;
nsMainThreadPtrHandle<nsIVerifySignedDirectoryCallback> mCallback;
nsCOMPtr<nsIX509Cert> mSignerCert; // out
};
NS_IMETHODIMP
nsNSSCertificateDB::VerifySignedDirectoryAsync(
AppTrustedRoot aTrustedRoot, nsIFile* aUnpackedJar,
nsIVerifySignedDirectoryCallback* aCallback)
{
NS_ENSURE_ARG_POINTER(aUnpackedJar);
NS_ENSURE_ARG_POINTER(aCallback);
RefPtr<VerifySignedDirectoryTask> task(new VerifySignedDirectoryTask(aTrustedRoot,
aUnpackedJar,
aCallback));
return task->Dispatch("UnpackedJar");
}

View File

@ -28,6 +28,13 @@ interface nsIOpenSignedAppFileCallback : nsISupports
in nsIX509Cert aSignerCert);
};
[scriptable, function, uuid(d5f97827-622a-488f-be08-d850432ac8ec)]
interface nsIVerifySignedDirectoryCallback : nsISupports
{
void verifySignedDirectoryFinished(in nsresult rv,
in nsIX509Cert aSignerCert);
};
[scriptable, function, uuid(3d6a9c87-5c5f-46fc-9410-96da6092f0f2)]
interface nsIVerifySignedManifestCallback : nsISupports
{
@ -39,7 +46,7 @@ interface nsIVerifySignedManifestCallback : nsISupports
* This represents a service to access and manipulate
* X.509 certificates stored in a database.
*/
[scriptable, uuid(560bc9ac-3e71-472e-9b08-2270d0c71878)]
[scriptable, uuid(6d27211b-7119-4777-9c62-f29310eeb262)]
interface nsIX509CertDB : nsISupports {
/**
@ -317,6 +324,22 @@ interface nsIX509CertDB : nsISupports {
in nsIFile aJarFile,
in nsIOpenSignedAppFileCallback callback);
/**
* Verifies the signature on a directory representing an unpacked signed
* JAR file. To be considered valid, there must be exactly one signature
* on the directory structure and that signature must have signed every
* entry. Further, the signature must come from a certificate that
* is trusted for code signing.
*
* On success NS_OK and the trusted certificate that signed the
* unpacked JAR are returned.
*
* On failure, an error code is returned.
*/
void verifySignedDirectoryAsync(in AppTrustedRoot trustedRoot,
in nsIFile aUnpackedDir,
in nsIVerifySignedDirectoryCallback callback);
/**
* Given streams containing a signature and a manifest file, verifies
* that the signature is valid for the manifest. The signature must

View File

@ -0,0 +1,173 @@
"use strict";
Components.utils.import("resource://gre/modules/ZipUtils.jsm");
do_get_profile(); // must be called before getting nsIX509CertDB
const certdb = Cc["@mozilla.org/security/x509certdb;1"].getService(Ci.nsIX509CertDB);
var gSignedXPI = do_get_file("test_signed_apps/sslcontrol.xpi", false);
var gTarget = FileUtils.getDir("TmpD", ["test_signed_dir"]);
gTarget.createUnique(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
// tamper data structure, each element is optional
// { copy: [[path,newname], [path2,newname2], ...],
// delete: [path, path2, ...],
// corrupt: [path, path2, ...]
// }
function prepare(tamper) {
ZipUtils.extractFiles(gSignedXPI, gTarget);
// copy files
if (tamper.copy) {
tamper.copy.forEach(i => {
let f = gTarget.clone();
i[0].split('/').forEach(seg => {f.append(seg);});
f.copyTo(null, i[1]);
});
}
// delete files
if (tamper.delete) {
tamper.delete.forEach(i => {
let f = gTarget.clone();
i.split('/').forEach(seg => {f.append(seg);});
f.remove(true);
});
}
// corrupt files
if (tamper.corrupt) {
tamper.corrupt.forEach(i => {
let f = gTarget.clone();
i.split('/').forEach(seg => {f.append(seg);});
let s = FileUtils.openFileOutputStream(f, FileUtils.MODE_WRONLY);
const str = "Kilroy was here";
s.write(str, str.length);
s.close();
});
}
return gTarget;
}
function check_result(name, expectedRv, dir) {
return function verifySignedDirCallback(rv, aSignerCert) {
equal(rv, expectedRv, name + " rv:");
equal(aSignerCert != null, Components.isSuccessCode(expectedRv),
"expecting certificate:");
// cleanup and kick off next test
dir.remove(true);
run_next_test();
};
}
function verifyDirAsync(name, expectedRv, tamper) {
let targetDir = prepare(tamper);
certdb.verifySignedDirectoryAsync(
Ci.nsIX509CertDB.AddonsPublicRoot, targetDir,
check_result(name, expectedRv, targetDir));
}
//
// the tests
//
add_test(function() {
verifyDirAsync("'valid'", Cr.NS_OK, {} /* no tampering */ );
});
add_test(function() {
verifyDirAsync("'no meta dir'", Cr.NS_ERROR_SIGNED_JAR_NOT_SIGNED,
{delete: ["META-INF"]});
});
add_test(function() {
verifyDirAsync("'empty meta dir'", Cr.NS_ERROR_SIGNED_JAR_NOT_SIGNED,
{delete: ["META-INF/mozilla.rsa",
"META-INF/mozilla.sf",
"META-INF/manifest.mf"]});
});
add_test(function() {
verifyDirAsync("'two rsa files'", Cr.NS_ERROR_SIGNED_JAR_MANIFEST_INVALID,
{copy: [["META-INF/mozilla.rsa","extra.rsa"]]});
});
add_test(function() {
verifyDirAsync("'corrupt rsa file'", Cr.NS_ERROR_SIGNED_JAR_MANIFEST_INVALID,
{corrupt: ["META-INF/mozilla.rsa"]});
});
add_test(function() {
verifyDirAsync("'missing sf file'", Cr.NS_ERROR_SIGNED_JAR_MANIFEST_INVALID,
{delete: ["META-INF/mozilla.sf"]});
});
add_test(function() {
verifyDirAsync("'corrupt sf file'", Cr.NS_ERROR_SIGNED_JAR_MANIFEST_INVALID,
{corrupt: ["META-INF/mozilla.sf"]});
});
add_test(function() {
verifyDirAsync("'extra .sf file (invalid)'", Cr.NS_ERROR_SIGNED_JAR_UNSIGNED_ENTRY,
{copy: [["META-INF/mozilla.rsa","extra.sf"]]});
});
add_test(function() {
verifyDirAsync("'extra .sf file (valid)'", Cr.NS_ERROR_SIGNED_JAR_UNSIGNED_ENTRY,
{copy: [["META-INF/mozilla.sf","extra.sf"]]});
});
add_test(function() {
verifyDirAsync("'missing manifest'", Cr.NS_ERROR_SIGNED_JAR_MANIFEST_INVALID,
{delete: ["META-INF/manifest.mf"]});
});
add_test(function() {
verifyDirAsync("'corrupt manifest'", Cr.NS_ERROR_SIGNED_JAR_MANIFEST_INVALID,
{corrupt: ["META-INF/manifest.mf"]});
});
add_test(function() {
verifyDirAsync("'missing file'", Cr.NS_ERROR_SIGNED_JAR_ENTRY_MISSING,
{delete: ["bootstrap.js"]});
});
add_test(function() {
verifyDirAsync("'corrupt file'", Cr.NS_ERROR_SIGNED_JAR_MODIFIED_ENTRY,
{corrupt: ["bootstrap.js"]});
});
add_test(function() {
verifyDirAsync("'extra file'", Cr.NS_ERROR_SIGNED_JAR_UNSIGNED_ENTRY,
{copy: [["bootstrap.js","extra"]]});
});
add_test(function() {
verifyDirAsync("'missing file in dir'", Cr.NS_ERROR_SIGNED_JAR_ENTRY_MISSING,
{delete: ["content/options.xul"]});
});
add_test(function() {
verifyDirAsync("'corrupt file in dir'", Cr.NS_ERROR_SIGNED_JAR_MODIFIED_ENTRY,
{corrupt: ["content/options.xul"]});
});
add_test(function() {
verifyDirAsync("'extra file in dir'", Cr.NS_ERROR_SIGNED_JAR_UNSIGNED_ENTRY,
{copy: [["content/options.xul","extra"]]});
});
do_register_cleanup(function() {
if (gTarget.exists()) {
gTarget.remove(true);
}
});
function run_test() {
run_next_test();
}

View File

@ -70,6 +70,7 @@ run-sequentially = hardcoded ports
[test_cert_version.js]
[test_signed_apps.js]
[test_signed_apps-marketplace.js]
[test_signed_dir.js]
[test_cert_eku-CA_EP.js]
[test_cert_eku-CA_EP_NS_OS_SA_TS.js]

View File

@ -19,7 +19,26 @@ function promiseNotification(topic) {
});
}
// Just enough mocks so we can avoid hawk etc.
// Just enough mocks so we can avoid hawk and storage.
let MockStorage = function() {
this.data = null;
};
MockStorage.prototype = Object.freeze({
set: function (contents) {
this.data = contents;
return Promise.resolve(null);
},
get: function () {
return Promise.resolve(this.data);
},
getOAuthTokens() {
return Promise.resolve(null);
},
setOAuthTokens(contents) {
return Promise.resolve();
},
});
function MockFxAccountsClient() {
this._email = "nobody@example.com";
this._verified = false;
@ -43,6 +62,7 @@ function MockFxAccounts(mockGrantClient) {
return new FxAccounts({
fxAccountsClient: new MockFxAccountsClient(),
getAssertion: () => Promise.resolve("assertion"),
signedInUserStorage: new MockStorage(),
_destroyOAuthToken: function(tokenData) {
// somewhat sad duplication of _destroyOAuthToken, but hard to avoid.
return mockGrantClient.destroyToken(tokenData.token).then( () => {

View File

@ -248,9 +248,40 @@
var testWin = windowMediator.getMostRecentWindow(winType);
setStatus("Running...");
testWin.focus();
var Tester = new testWin.Tester(links, gDumper, testsFinished);
Tester.start();
// It's possible that the test harness window is not yet focused when this
// function runs (in which case testWin is already focused, and focusing it
// will be a no-op, and then the test harness window will steal focus later,
// which will mess up tests). So wait for the test harness window to be
// focused before trying to focus testWin.
waitForFocus(() => {
// Focus the test window and start tests.
waitForFocus(() => {
var Tester = new testWin.Tester(links, gDumper, testsFinished);
Tester.start();
}, testWin);
}, window);
}
function executeSoon(callback) {
let tm = Cc["@mozilla.org/thread-manager;1"].getService(Ci.nsIThreadManager);
tm.mainThread.dispatch(callback, Ci.nsIThread.DISPATCH_NORMAL);
}
function waitForFocus(callback, win) {
// If "win" is already focused, just call the callback.
let fm = Cc["@mozilla.org/focus-manager;1"].getService(Ci.nsIFocusManager);
if (fm.focusedWindow == win) {
executeSoon(callback);
return;
}
// Otherwise focus it, and wait for the focus event.
win.addEventListener("focus", function listener() {
win.removeEventListener("focus", listener, true);
executeSoon(callback);
}, true);
win.focus();
}
function sum(a, b) {

View File

@ -0,0 +1,244 @@
/* 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";
this.EXPORTED_SYMBOLS = [
"LoginDoorhangers",
];
const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
Cu.import("resource://gre/modules/Services.jsm");
const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
this.LoginDoorhangers = {};
/**
* Doorhanger for selecting and filling logins.
*
* @param {Object} properties
* Properties from this object will be applied to the new instance.
*/
this.LoginDoorhangers.FillDoorhanger = function (properties) {
this.onFilterInput = this.onFilterInput.bind(this);
this.onListDblClick = this.onListDblClick.bind(this);
this.onListKeyPress = this.onListKeyPress.bind(this);
this.filterString = properties.filterString;
if (properties.browser) {
this.browser = properties.browser;
}
};
this.LoginDoorhangers.FillDoorhanger.prototype = {
/**
* Whether the elements for this doorhanger are currently in the document.
*/
bound: false,
/**
* Associates the doorhanger with its browser. When the tab associated to this
* browser is selected, the anchor icon for the doorhanger will appear.
*
* The browser may change during the lifetime of the doorhanger, in case the
* web page is moved to a different chrome window by the swapDocShells method.
*/
set browser(browser) {
this._browser = browser;
let doorhanger = this;
let PopupNotifications = this.chomeDocument.defaultView.PopupNotifications;
let notification = PopupNotifications.show(
browser,
"password-fill",
"",
"password-notification-icon",
null,
null,
{
dismissed: true,
persistWhileVisible: true,
eventCallback: function (topic, otherBrowser) {
switch (topic) {
case "shown":
// Since we specified the "dismissed" option, this event will only
// be called after the "show" method returns, so the reference to
// "this.notification" will be available at this point.
doorhanger.bound = true;
doorhanger.bind();
break;
case "dismissed":
case "removed":
if (doorhanger.bound) {
doorhanger.unbind();
}
break;
case "swapping":
this._browser = otherBrowser;
return true;
}
return false;
},
}
);
this.notification = notification;
notification.doorhanger = this;
},
get browser() {
return this._browser;
},
_browser: null,
/**
* DOM document to which the doorhanger is currently associated.
*
* This may change during the lifetime of the doorhanger, in case the web page
* is moved to a different chrome window by the swapDocShells method.
*/
get chomeDocument() {
return this.browser.ownerDocument;
},
/**
* Hides this notification, if the notification panel is currently open.
*/
hide() {
let PopupNotifications = this.chomeDocument.defaultView.PopupNotifications;
if (PopupNotifications.isPanelOpen) {
PopupNotifications.panel.hidePopup();
}
},
/**
* Removes the doorhanger from the browser.
*/
remove() {
this.notification.remove();
},
/**
* Binds this doorhanger to its UI controls.
*/
bind() {
this.element = this.chomeDocument.getElementById("login-fill-doorhanger");
this.list = this.chomeDocument.getElementById("login-fill-list");
this.filter = this.chomeDocument.getElementById("login-fill-filter");
this.filter.setAttribute("value", this.filterString);
this.refreshList();
this.filter.addEventListener("input", this.onFilterInput);
this.list.addEventListener("dblclick", this.onListDblClick);
this.list.addEventListener("keypress", this.onListKeyPress);
// Move the main element to the notification panel for displaying.
this.notification.owner.panel.firstElementChild.appendChild(this.element);
this.element.hidden = false;
},
/**
* Unbinds this doorhanger from its UI controls.
*/
unbind() {
this.filter.removeEventListener("input", this.onFilterInput);
this.list.removeEventListener("dblclick", this.onListDblClick);
this.list.removeEventListener("keypress", this.onListKeyPress);
this.clearList();
// Place the element back in the document for the next time we need it.
this.element.hidden = true;
this.chomeDocument.getElementById("mainPopupSet").appendChild(this.element);
},
/**
* User-editable string used to filter the list of all logins.
*/
filterString: "",
/**
* Handles text changes in the filter textbox.
*/
onFilterInput() {
this.filterString = this.filter.value;
this.refreshList();
},
/**
* Rebuilds the list of logins.
*/
refreshList() {
this.clearList();
let formLogins = Services.logins.findLogins({}, "", "", null);
let filterToUse = this.filterString.trim().toLowerCase();
if (filterToUse) {
formLogins = formLogins.filter(login => {
return login.hostname.toLowerCase().indexOf(filterToUse) != -1 ||
login.username.toLowerCase().indexOf(filterToUse) != -1 ;
});
}
for (let { hostname, username } of formLogins) {
let item = this.chomeDocument.createElementNS(XUL_NS, "richlistitem");
item.classList.add("login-fill-item");
item.setAttribute("hostname", hostname);
item.setAttribute("username", username);
this.list.appendChild(item);
}
},
/**
* Clears the list of logins.
*/
clearList() {
while (this.list.firstChild) {
this.list.removeChild(this.list.firstChild);
}
},
/**
* Handles the action associated to a login item.
*/
onListDblClick(event) {
if (event.button != 0 || !this.list.selectedItem) {
return;
}
this.fillLogin();
},
onListKeyPress(event) {
if (event.keyCode != Ci.nsIDOMKeyEvent.DOM_VK_RETURN ||
!this.list.selectedItem) {
return;
}
this.fillLogin();
},
fillLogin() {
this.hide();
},
};
/**
* Retrieves an existing FillDoorhanger associated with a browser, or null if an
* associated doorhanger of that type cannot be found.
*
* @param An object with the following properties:
* {
* browser:
* The <browser> element to which the doorhanger is associated.
* }
*/
this.LoginDoorhangers.FillDoorhanger.find = function ({ browser }) {
let PopupNotifications = browser.ownerDocument.defaultView.PopupNotifications;
let notification = PopupNotifications.getNotification("password-fill",
browser);
return notification && notification.doorhanger;
};

View File

@ -0,0 +1,22 @@
<?xml version="1.0"?>
<!-- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<!DOCTYPE bindings SYSTEM "chrome://passwordmgr/locale/passwordManager.dtd">
<bindings id="login-bindings"
xmlns="http://www.mozilla.org/xbl"
xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
xmlns:xbl="http://www.mozilla.org/xbl">
<binding id="login"
extends="chrome://global/content/bindings/richlistbox.xml#richlistitem">
<content orient="vertical">
<xul:label class="login-hostname" crop="end"
xbl:inherits="value=hostname"/>
<xul:label class="login-username" crop="end"
xbl:inherits="value=username"/>
</content>
</binding>
</bindings>

View File

@ -4,6 +4,7 @@
toolkit.jar:
% content passwordmgr %content/passwordmgr/
content/passwordmgr/login.xml (content/login.xml)
* content/passwordmgr/passwordManager.xul (content/passwordManager.xul)
* content/passwordmgr/passwordManager.js (content/passwordManager.js)
* content/passwordmgr/passwordManagerExceptions.js (content/passwordManagerExceptions.js)

View File

@ -41,6 +41,7 @@ EXTRA_PP_JS_MODULES += [
EXTRA_JS_MODULES += [
'InsecurePasswordUtils.jsm',
'LoginDoorhangers.jsm',
'LoginHelper.jsm',
'LoginManagerContent.jsm',
'LoginRecipes.jsm',

View File

@ -1415,7 +1415,7 @@ interface nsINavHistoryService : nsISupports
/**
* @see runInBatchMode of nsINavHistoryService/nsINavBookmarksService
*/
[scriptable, uuid(5143f2bb-be0a-4faf-9acb-b0ed3f82952c)]
[scriptable, function, uuid(5a5a9154-95ac-4e3d-90df-558816297407)]
interface nsINavHistoryBatchCallback : nsISupports {
void runBatched(in nsISupports aUserData);
};

View File

@ -4186,10 +4186,13 @@ nsNavHistory::URIToResultNode(nsIURI* aURI,
true, tagsFragment);
// Should match kGetInfoIndex_*
nsCOMPtr<mozIStorageStatement> stmt = mDB->GetStatement(NS_LITERAL_CSTRING(
"SELECT h.id, :page_url, h.title, h.rev_host, h.visit_count, "
"h.last_visit_date, f.url, null, null, null, null, "
) + tagsFragment + NS_LITERAL_CSTRING(", h.frecency, h.hidden, h.guid "
"SELECT h.id, :page_url, COALESCE(b.title, h.title), "
"h.rev_host, h.visit_count, h.last_visit_date, f.url, "
"b.id, b.dateAdded, b.lastModified, b.parent, "
) + tagsFragment + NS_LITERAL_CSTRING(", h.frecency, h.hidden, h.guid, "
"b.guid, b.position, b.type, b.fk "
"FROM moz_places h "
"LEFT JOIN moz_bookmarks b ON b.fk = h.id "
"LEFT JOIN moz_favicons f ON h.favicon_id = f.id "
"WHERE h.url = :page_url ")
);

View File

@ -2767,7 +2767,7 @@ nsNavHistoryQueryResultNode::NotifyIfTagsChanged(nsIURI* aURI)
nsCOMArray<nsNavHistoryResultNode> matches;
RecursiveFindURIs(onlyOneEntry, this, spec, &matches);
if (matches.Count() == 0 && mHasSearchTerms && !mRemovingURI) {
if (matches.Count() == 0 && mHasSearchTerms) {
// A new tag has been added, it's possible it matches our query.
NS_ENSURE_TRUE(history, NS_ERROR_OUT_OF_MEMORY);
rv = history->URIToResultNode(aURI, mOptions, getter_AddRefs(node));
@ -2838,7 +2838,6 @@ nsNavHistoryQueryResultNode::OnItemRemoved(int64_t aItemId,
const nsACString& aGUID,
const nsACString& aParentGUID)
{
mRemovingURI = aURI;
if (aItemType == nsINavBookmarksService::TYPE_BOOKMARK &&
mLiveUpdate != QUERYUPDATE_SIMPLE && mLiveUpdate != QUERYUPDATE_TIME) {
nsresult rv = Refresh();

View File

@ -674,7 +674,6 @@ public:
virtual void RecursiveSort(const char* aData,
SortComparator aComparator) override;
nsCOMPtr<nsIURI> mRemovingURI;
nsresult NotifyIfTagsChanged(nsIURI* aURI);
uint32_t mBatchChanges;

View File

@ -107,30 +107,31 @@ TaggingService.prototype = {
* @throws Cr.NS_ERROR_INVALID_ARG if any element of the input array is not
* a valid tag.
*/
_convertInputMixedTagsArray: function TS__convertInputMixedTagsArray(aTags, trim=false)
{
return aTags.map(function (val)
{
let tag = { _self: this };
if (typeof(val) == "number" && this._tagFolders[val]) {
_convertInputMixedTagsArray(aTags, trim=false) {
// Handle sparse array with a .filter.
return aTags.filter(tag => tag !== undefined)
.map(idOrName => {
let tag = {};
if (typeof(idOrName) == "number" && this._tagFolders[idOrName]) {
// This is a tag folder id.
tag.id = val;
tag.id = idOrName;
// We can't know the name at this point, since a previous tag could
// want to change it.
tag.__defineGetter__("name", function () this._self._tagFolders[this.id]);
tag.__defineGetter__("name", () => this._tagFolders[tag.id]);
}
else if (typeof(val) == "string" && val.length > 0 && val.length <= Ci.nsITaggingService.MAX_TAG_LENGTH) {
else if (typeof(idOrName) == "string" && idOrName.length > 0 &&
idOrName.length <= Ci.nsITaggingService.MAX_TAG_LENGTH) {
// This is a tag name.
tag.name = trim ? val.trim() : val;
tag.name = trim ? idOrName.trim() : idOrName;
// We can't know the id at this point, since a previous tag could
// have created it.
tag.__defineGetter__("id", function () this._self._getItemIdForTag(this.name));
tag.__defineGetter__("id", () => this._getItemIdForTag(tag.name));
}
else {
throw Cr.NS_ERROR_INVALID_ARG;
}
return tag;
}, this);
});
},
// nsITaggingService
@ -143,35 +144,37 @@ TaggingService.prototype = {
// This also does some input validation.
let tags = this._convertInputMixedTagsArray(aTags, true);
let taggingService = this;
PlacesUtils.bookmarks.runInBatchMode({
runBatched: function (aUserData)
{
tags.forEach(function (tag)
{
if (tag.id == -1) {
// Tag does not exist yet, create it.
this._createTag(tag.name);
}
let taggingFunction = () => {
for (let tag of tags) {
if (tag.id == -1) {
// Tag does not exist yet, create it.
this._createTag(tag.name);
}
if (this._getItemIdForTaggedURI(aURI, tag.name) == -1) {
// The provided URI is not yet tagged, add a tag for it.
// Note that bookmarks under tag containers must have null titles.
PlacesUtils.bookmarks.insertBookmark(
tag.id, aURI, PlacesUtils.bookmarks.DEFAULT_INDEX, null
);
}
if (this._getItemIdForTaggedURI(aURI, tag.name) == -1) {
// The provided URI is not yet tagged, add a tag for it.
// Note that bookmarks under tag containers must have null titles.
PlacesUtils.bookmarks.insertBookmark(
tag.id, aURI, PlacesUtils.bookmarks.DEFAULT_INDEX, null
);
}
// Try to preserve user's tag name casing.
// Rename the tag container so the Places view matches the most-recent
// user-typed value.
if (PlacesUtils.bookmarks.getItemTitle(tag.id) != tag.name) {
// this._tagFolders is updated by the bookmarks observer.
PlacesUtils.bookmarks.setItemTitle(tag.id, tag.name);
}
}, taggingService);
// Try to preserve user's tag name casing.
// Rename the tag container so the Places view matches the most-recent
// user-typed value.
if (PlacesUtils.bookmarks.getItemTitle(tag.id) != tag.name) {
// this._tagFolders is updated by the bookmarks observer.
PlacesUtils.bookmarks.setItemTitle(tag.id, tag.name);
}
}
}, null);
};
// Use a batch only if creating more than 2 tags.
if (tags.length < 3) {
taggingFunction();
} else {
PlacesUtils.bookmarks.runInBatchMode(taggingFunction, null);
}
},
/**
@ -225,23 +228,25 @@ TaggingService.prototype = {
"https://bugzilla.mozilla.org/show_bug.cgi?id=967196");
}
let taggingService = this;
PlacesUtils.bookmarks.runInBatchMode({
runBatched: function (aUserData)
{
tags.forEach(function (tag)
{
if (tag.id != -1) {
// A tag could exist.
let itemId = this._getItemIdForTaggedURI(aURI, tag.name);
if (itemId != -1) {
// There is a tagged item.
PlacesUtils.bookmarks.removeItem(itemId);
}
let untaggingFunction = () => {
for (let tag of tags) {
if (tag.id != -1) {
// A tag could exist.
let itemId = this._getItemIdForTaggedURI(aURI, tag.name);
if (itemId != -1) {
// There is a tagged item.
PlacesUtils.bookmarks.removeItem(itemId);
}
}, taggingService);
}
}
}, null);
};
// Use a batch only if creating more than 2 tags.
if (tags.length < 3) {
untaggingFunction();
} else {
PlacesUtils.bookmarks.runInBatchMode(untaggingFunction, null);
}
},
// nsITaggingService

View File

@ -143,8 +143,6 @@ add_test(function onItemChanged_tags_bookmark() {
const TITLE = "New title";
const TAG = "tag"
gBookmarksObserver.expected = [
{ name: "onBeginUpdateBatch", // Tag addition uses a batch.
args: [] },
{ name: "onItemAdded", // This is the tag folder.
args: [
{ name: "itemId", check: function (v) typeof(v) == "number" && v > 0 },
@ -181,10 +179,6 @@ add_test(function onItemChanged_tags_bookmark() {
{ name: "guid", check: function (v) typeof(v) == "string" && /^[a-zA-Z0-9\-_]{12}$/.test(v) },
{ name: "parentGuid", check: function (v) typeof(v) == "string" && /^[a-zA-Z0-9\-_]{12}$/.test(v) },
] },
{ name: "onEndUpdateBatch",
args: [] },
{ name: "onBeginUpdateBatch", // Tag removal uses a batch.
args: [] },
{ name: "onItemRemoved", // This is the tag.
args: [
{ name: "itemId", check: function (v) typeof(v) == "number" && v > 0 },
@ -217,8 +211,6 @@ add_test(function onItemChanged_tags_bookmark() {
{ name: "guid", check: function (v) typeof(v) == "string" && /^[a-zA-Z0-9\-_]{12}$/.test(v) },
{ name: "parentGuid", check: function (v) typeof(v) == "string" && /^[a-zA-Z0-9\-_]{12}$/.test(v) },
] },
{ name: "onEndUpdateBatch",
args: [] },
];
PlacesUtils.tagging.tagURI(uri, [TAG]);
PlacesUtils.tagging.untagURI(uri, [TAG]);

View File

@ -160,6 +160,10 @@ add_task(function pages_searchterm_is_tag_query()
compareArrayToResult([], root);
gTestData.forEach(function (data) {
let uri = NetUtil.newURI(data.uri);
PlacesUtils.bookmarks.insertBookmark(PlacesUtils.unfiledBookmarksFolderId,
uri,
PlacesUtils.bookmarks.DEFAULT_INDEX,
data.title);
PlacesUtils.tagging.tagURI(uri, ["test-tag"]);
compareArrayToResult([data], root);
PlacesUtils.tagging.untagURI(uri, ["test-tag"]);
@ -177,6 +181,10 @@ add_task(function visits_searchterm_is_tag_query()
compareArrayToResult([], root);
gTestData.forEach(function (data) {
let uri = NetUtil.newURI(data.uri);
PlacesUtils.bookmarks.insertBookmark(PlacesUtils.unfiledBookmarksFolderId,
uri,
PlacesUtils.bookmarks.DEFAULT_INDEX,
data.title);
PlacesUtils.tagging.tagURI(uri, ["test-tag"]);
compareArrayToResult([data], root);
PlacesUtils.tagging.untagURI(uri, ["test-tag"]);

View File

@ -283,6 +283,7 @@
"meta": true,
"param": true,
"source": true,
"wbr": true
};
var whitespace = [" ", "\t", "\n", "\r"];

View File

@ -109,12 +109,12 @@ Readability.prototype = {
DEFAULT_MAX_PAGES: 5,
// Element tags to score by default.
DEFAULT_TAGS_TO_SCORE: ["SECTION", "P", "TD", "PRE"],
DEFAULT_TAGS_TO_SCORE: "section,h2,h3,h4,h5,h6,p,td,pre".toUpperCase().split(","),
// All of the regular expressions in use within readability.
// Defined up here so we don't instantiate them repeatedly in loops.
REGEXPS: {
unlikelyCandidates: /banner|combx|comment|community|disqus|extra|foot|header|menu|remark|rss|share|shoutbox|sidebar|skyscraper|sponsor|ad-break|agegate|pagination|pager|popup/i,
unlikelyCandidates: /banner|combx|comment|community|disqus|extra|foot|header|menu|related|remark|rss|share|shoutbox|sidebar|skyscraper|sponsor|ad-break|agegate|pagination|pager|popup/i,
okMaybeItsACandidate: /and|article|body|column|main|shadow/i,
positive: /article|body|content|entry|hentry|main|page|pagination|post|text|blog|story/i,
negative: /hidden|banner|combx|comment|com-|contact|foot|footer|footnote|masthead|media|meta|outbrain|promo|related|scroll|share|shoutbox|sidebar|skyscraper|sponsor|shopping|tags|tool|widget/i,
@ -122,7 +122,7 @@ Readability.prototype = {
byline: /byline|author|dateline|writtenby/i,
replaceFonts: /<(\/?)font[^>]*>/gi,
normalize: /\s{2,}/g,
videos: /https?:\/\/(www\.)?(dailymotion|youtube|youtube-nocookie|player\.vimeo)\.com/i,
videos: /\/\/(www\.)?(dailymotion|youtube|youtube-nocookie|player\.vimeo)\.com/i,
nextLink: /(next|weiter|continue|>([^\|]|$)|»([^\|]|$))/i,
prevLink: /(prev|earl|old|new|<|«)/i,
whitespace: /^\s*$/,
@ -739,7 +739,12 @@ Readability.prototype = {
candidates.push(ancestor);
}
ancestor.readability.contentScore += contentScore / (level === 0 ? 1 : level * 2);
// Node score divider:
// - parent: 1 (no division)
// - grandparent: 2
// - great grandparent+: ancestor level * 3
var scoreDivider = level === 0 ? 1 : level === 1 ? 2 : level * 3;
ancestor.readability.contentScore += contentScore / scoreDivider;
});
});

View File

@ -8,6 +8,16 @@ this.EXPORTED_SYMBOLS = ["ReaderMode"];
const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
// Constants for telemetry.
const DOWNLOAD_SUCCESS = 0;
const DOWNLOAD_ERROR_XHR = 1;
const DOWNLOAD_ERROR_NO_DOC = 2;
const PARSE_SUCCESS = 0;
const PARSE_ERROR_TOO_MANY_ELEMENTS = 1;
const PARSE_ERROR_WORKER = 2;
const PARSE_ERROR_NO_ARTICLE = 3;
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
@ -17,6 +27,7 @@ XPCOMUtils.defineLazyModuleGetter(this, "CommonUtils", "resource://services-comm
XPCOMUtils.defineLazyModuleGetter(this, "OS", "resource://gre/modules/osfile.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "ReaderWorker", "resource://gre/modules/reader/ReaderWorker.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Task", "resource://gre/modules/Task.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "TelemetryStopwatch", "resource://gre/modules/TelemetryStopwatch.jsm");
XPCOMUtils.defineLazyGetter(this, "Readability", function() {
let scope = {};
@ -169,11 +180,17 @@ this.ReaderMode = {
*/
downloadAndParseDocument: Task.async(function* (url) {
let uri = Services.io.newURI(url, null, null);
let doc = yield this._downloadDocument(url);
TelemetryStopwatch.start("READER_MODE_DOWNLOAD_MS");
let doc = yield this._downloadDocument(url).catch(e => {
TelemetryStopwatch.finish("READER_MODE_DOWNLOAD_MS");
throw e;
});
TelemetryStopwatch.finish("READER_MODE_DOWNLOAD_MS");
return yield this._readerParse(uri, doc);
}),
_downloadDocument: function (url) {
let histogram = Services.telemetry.getHistogramById("READER_MODE_DOWNLOAD_RESULT");
return new Promise((resolve, reject) => {
let xhr = new XMLHttpRequest();
xhr.open("GET", url, true);
@ -182,12 +199,14 @@ this.ReaderMode = {
xhr.onload = evt => {
if (xhr.status !== 200) {
reject("Reader mode XHR failed with status: " + xhr.status);
histogram.add(DOWNLOAD_ERROR_XHR);
return;
}
let doc = xhr.responseXML;
if (!doc) {
reject("Reader mode XHR didn't return a document");
histogram.add(DOWNLOAD_ERROR_NO_DOC);
return;
}
@ -205,6 +224,7 @@ this.ReaderMode = {
}
}
resolve(doc);
histogram.add(DOWNLOAD_SUCCESS);
}
xhr.send();
});
@ -292,10 +312,12 @@ this.ReaderMode = {
* @resolves JS object representing the article, or null if no article is found.
*/
_readerParse: Task.async(function* (uri, doc) {
let histogram = Services.telemetry.getHistogramById("READER_MODE_PARSE_RESULT");
if (this.parseNodeLimit) {
let numTags = doc.getElementsByTagName("*").length;
if (numTags > this.parseNodeLimit) {
this.log("Aborting parse for " + uri.spec + "; " + numTags + " elements found");
histogram.add(PARSE_ERROR_TOO_MANY_ELEMENTS);
return null;
}
}
@ -308,19 +330,25 @@ this.ReaderMode = {
pathBase: Services.io.newURI(".", null, uri).spec
};
TelemetryStopwatch.start("READER_MODE_SERIALIZE_DOM_MS");
let serializer = Cc["@mozilla.org/xmlextras/xmlserializer;1"].
createInstance(Ci.nsIDOMSerializer);
let serializedDoc = yield Promise.resolve(serializer.serializeToString(doc));
let serializedDoc = serializer.serializeToString(doc);
TelemetryStopwatch.finish("READER_MOD_SERIALIZE_DOM_MS");
TelemetryStopwatch.start("READER_MODE_WORKER_PARSE_MS");
let article = null;
try {
article = yield ReaderWorker.post("parseDocument", [uriParam, serializedDoc]);
} catch (e) {
Cu.reportError("Error in ReaderWorker: " + e);
histogram.add(PARSE_ERROR_WORKER);
}
TelemetryStopwatch.finish("READER_MODE_WORKER_PARSE_MS");
if (!article) {
this.log("Worker did not return an article");
histogram.add(PARSE_ERROR_NO_ARTICLE);
return null;
}
@ -331,6 +359,8 @@ this.ReaderMode = {
let flags = Ci.nsIDocumentEncoder.OutputSelectionOnly | Ci.nsIDocumentEncoder.OutputAbsoluteLinks;
article.title = Cc["@mozilla.org/parserutils;1"].getService(Ci.nsIParserUtils)
.convertToPlainText(article.title, flags, 0);
histogram.add(PARSE_SUCCESS);
return article;
}),

View File

@ -7793,5 +7793,39 @@
"expires_in_version": "50",
"kind": "count",
"description": "Tracking whether a ServiceWorker spawn gets queued due to hitting max workers per domain limit"
},
"READER_MODE_SERIALIZE_DOM_MS": {
"expires_in_version": "42",
"kind": "exponential",
"high": "5000",
"n_buckets": 15,
"description": "Time (ms) to serialize a DOM to send to the reader worker"
},
"READER_MODE_WORKER_PARSE_MS": {
"expires_in_version": "42",
"kind": "exponential",
"high": "10000",
"n_buckets": 30,
"description": "Time (ms) for the reader worker to parse a document"
},
"READER_MODE_DOWNLOAD_MS": {
"expires_in_version": "42",
"kind": "exponential",
"low": 50,
"high": "40000",
"n_buckets": 60,
"description": "Time (ms) to download a document to show in reader mode"
},
"READER_MODE_PARSE_RESULT" : {
"expires_in_version": "42",
"kind": "enumerated",
"n_values": 5,
"description": "The result of trying to parse a document to show in reader view (0=Success, 1=Error too many elements, 2=Error in worker, 3=Error no article)"
},
"READER_MODE_DOWNLOAD_RESULT" : {
"expires_in_version": "42",
"kind": "enumerated",
"n_values": 5,
"description": "The result of trying to download a document to show in reader view (0=Success, 1=Error XHR, 2=Error no document)"
}
}

View File

@ -0,0 +1,335 @@
/* 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";
let { Ci, Cu } = require("chrome");
let Services = require("Services");
let { ActorPool } = require("devtools/server/actors/common");
let { TabSources } = require("./utils/TabSources");
let makeDebugger = require("./utils/make-debugger");
let { ConsoleAPIListener } = require("devtools/toolkit/webconsole/utils");
let DevToolsUtils = require("devtools/toolkit/DevToolsUtils");
let { dbg_assert, update } = DevToolsUtils;
loader.lazyRequireGetter(this, "AddonThreadActor", "devtools/server/actors/script", true);
loader.lazyRequireGetter(this, "unwrapDebuggerObjectGlobal", "devtools/server/actors/script", true);
loader.lazyRequireGetter(this, "mapURIToAddonID", "devtools/server/actors/utils/map-uri-to-addon-id");
loader.lazyRequireGetter(this, "WebConsoleActor", "devtools/server/actors/webconsole", true);
loader.lazyImporter(this, "AddonManager", "resource://gre/modules/AddonManager.jsm");
function BrowserAddonActor(aConnection, aAddon) {
this.conn = aConnection;
this._addon = aAddon;
this._contextPool = new ActorPool(this.conn);
this.conn.addActorPool(this._contextPool);
this._threadActor = null;
this._global = null;
this._shouldAddNewGlobalAsDebuggee = this._shouldAddNewGlobalAsDebuggee.bind(this);
this.makeDebugger = makeDebugger.bind(null, {
findDebuggees: this._findDebuggees.bind(this),
shouldAddNewGlobalAsDebuggee: this._shouldAddNewGlobalAsDebuggee
});
AddonManager.addAddonListener(this);
}
exports.BrowserAddonActor = BrowserAddonActor;
BrowserAddonActor.prototype = {
actorPrefix: "addon",
get exited() {
return !this._addon;
},
get id() {
return this._addon.id;
},
get url() {
return this._addon.sourceURI ? this._addon.sourceURI.spec : undefined;
},
get attached() {
return this._threadActor;
},
get global() {
return this._global;
},
get sources() {
if (!this._sources) {
dbg_assert(this.threadActor, "threadActor should exist when creating sources.");
this._sources = new TabSources(this._threadActor, this._allowSource);
}
return this._sources;
},
form: function BAA_form() {
dbg_assert(this.actorID, "addon should have an actorID.");
if (!this._consoleActor) {
this._consoleActor = new AddonConsoleActor(this._addon, this.conn, this);
this._contextPool.addActor(this._consoleActor);
}
return {
actor: this.actorID,
id: this.id,
name: this._addon.name,
url: this.url,
debuggable: this._addon.isDebuggable,
consoleActor: this._consoleActor.actorID,
traits: {
highlightable: false,
networkMonitor: false,
},
};
},
disconnect: function BAA_disconnect() {
this.conn.removeActorPool(this._contextPool);
this._contextPool = null;
this._consoleActor = null;
this._addon = null;
this._global = null;
AddonManager.removeAddonListener(this);
},
setOptions: function BAA_setOptions(aOptions) {
if ("global" in aOptions) {
this._global = aOptions.global;
}
},
onDisabled: function BAA_onDisabled(aAddon) {
if (aAddon != this._addon) {
return;
}
this._global = null;
},
onUninstalled: function BAA_onUninstalled(aAddon) {
if (aAddon != this._addon) {
return;
}
if (this.attached) {
this.onDetach();
this.conn.send({ from: this.actorID, type: "tabDetached" });
}
this.disconnect();
},
onAttach: function BAA_onAttach() {
if (this.exited) {
return { type: "exited" };
}
if (!this.attached) {
this._threadActor = new AddonThreadActor(this.conn, this);
this._contextPool.addActor(this._threadActor);
}
return { type: "tabAttached", threadActor: this._threadActor.actorID };
},
onDetach: function BAA_onDetach() {
if (!this.attached) {
return { error: "wrongState" };
}
this._contextPool.removeActor(this._threadActor);
this._threadActor = null;
this._sources = null;
return { type: "detached" };
},
preNest: function() {
let e = Services.wm.getEnumerator(null);
while (e.hasMoreElements()) {
let win = e.getNext();
let windowUtils = win.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIDOMWindowUtils);
windowUtils.suppressEventHandling(true);
windowUtils.suspendTimeouts();
}
},
postNest: function() {
let e = Services.wm.getEnumerator(null);
while (e.hasMoreElements()) {
let win = e.getNext();
let windowUtils = win.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIDOMWindowUtils);
windowUtils.resumeTimeouts();
windowUtils.suppressEventHandling(false);
}
},
/**
* Return true if the given global is associated with this addon and should be
* added as a debuggee, false otherwise.
*/
_shouldAddNewGlobalAsDebuggee: function (aGlobal) {
const global = unwrapDebuggerObjectGlobal(aGlobal);
try {
// This will fail for non-Sandbox objects, hence the try-catch block.
let metadata = Cu.getSandboxMetadata(global);
if (metadata) {
return metadata.addonID === this.id;
}
} catch (e) {}
if (global instanceof Ci.nsIDOMWindow) {
let id = {};
if (mapURIToAddonID(global.document.documentURIObject, id)) {
return id.value === this.id;
}
return false;
}
// Check the global for a __URI__ property and then try to map that to an
// add-on
let uridescriptor = aGlobal.getOwnPropertyDescriptor("__URI__");
if (uridescriptor && "value" in uridescriptor && uridescriptor.value) {
let uri;
try {
uri = Services.io.newURI(uridescriptor.value, null, null);
}
catch (e) {
DevToolsUtils.reportException(
"BrowserAddonActor.prototype._shouldAddNewGlobalAsDebuggee",
new Error("Invalid URI: " + uridescriptor.value)
);
return false;
}
let id = {};
if (mapURIToAddonID(uri, id)) {
return id.value === this.id;
}
}
return false;
},
/**
* Override the eligibility check for scripts and sources to make
* sure every script and source with a URL is stored when debugging
* add-ons.
*/
_allowSource: function(aSource) {
// XPIProvider.jsm evals some code in every add-on's bootstrap.js. Hide it.
if (aSource.url === "resource://gre/modules/addons/XPIProvider.jsm") {
return false;
}
return true;
},
/**
* Yield the current set of globals associated with this addon that should be
* added as debuggees.
*/
_findDebuggees: function (dbg) {
return dbg.findAllGlobals().filter(this._shouldAddNewGlobalAsDebuggee);
}
};
BrowserAddonActor.prototype.requestTypes = {
"attach": BrowserAddonActor.prototype.onAttach,
"detach": BrowserAddonActor.prototype.onDetach
};
/**
* The AddonConsoleActor implements capabilities needed for the add-on web
* console feature.
*
* @constructor
* @param object aAddon
* The add-on that this console watches.
* @param object aConnection
* The connection to the client, DebuggerServerConnection.
* @param object aParentActor
* The parent BrowserAddonActor actor.
*/
function AddonConsoleActor(aAddon, aConnection, aParentActor)
{
this.addon = aAddon;
WebConsoleActor.call(this, aConnection, aParentActor);
}
AddonConsoleActor.prototype = Object.create(WebConsoleActor.prototype);
update(AddonConsoleActor.prototype, {
constructor: AddonConsoleActor,
actorPrefix: "addonConsole",
/**
* The add-on that this console watches.
*/
addon: null,
/**
* The main add-on JS global
*/
get window() {
return this.parentActor.global;
},
/**
* Destroy the current AddonConsoleActor instance.
*/
disconnect: function ACA_disconnect()
{
WebConsoleActor.prototype.disconnect.call(this);
this.addon = null;
},
/**
* Handler for the "startListeners" request.
*
* @param object aRequest
* The JSON request object received from the Web Console client.
* @return object
* The response object which holds the startedListeners array.
*/
onStartListeners: function ACA_onStartListeners(aRequest)
{
let startedListeners = [];
while (aRequest.listeners.length > 0) {
let listener = aRequest.listeners.shift();
switch (listener) {
case "ConsoleAPI":
if (!this.consoleAPIListener) {
this.consoleAPIListener =
new ConsoleAPIListener(null, this, "addon/" + this.addon.id);
this.consoleAPIListener.init();
}
startedListeners.push(listener);
break;
}
}
return {
startedListeners: startedListeners,
nativeConsoleAPI: true,
traits: this.traits,
};
},
});
AddonConsoleActor.prototype.requestTypes = Object.create(WebConsoleActor.prototype.requestTypes);
AddonConsoleActor.prototype.requestTypes.startListeners = AddonConsoleActor.prototype.onStartListeners;

View File

@ -5303,3 +5303,32 @@ function setBreakpointAtEntryPoints(actor, entryPoints) {
}
}
}
/**
* Unwrap a global that is wrapped in a |Debugger.Object|, or if the global has
* become a dead object, return |undefined|.
*
* @param Debugger.Object wrappedGlobal
* The |Debugger.Object| which wraps a global.
*
* @returns {Object|undefined}
* Returns the unwrapped global object or |undefined| if unwrapping
* failed.
*/
exports.unwrapDebuggerObjectGlobal = wrappedGlobal => {
try {
// Because of bug 991399 we sometimes get nuked window references here. We
// just bail out in that case.
//
// Note that addon sandboxes have a DOMWindow as their prototype. So make
// sure that we can touch the prototype too (whatever it is), in case _it_
// is it a nuked window reference. We force stringification to make sure
// that any dead object proxies make themselves known.
let global = wrappedGlobal.unsafeDereference();
Object.getPrototypeOf(global) + "";
return global;
}
catch (e) {
return undefined;
}
};

View File

@ -13,15 +13,15 @@ let { ActorPool, createExtraActors, appendExtraActors } = require("devtools/serv
let { DebuggerServer } = require("devtools/server/main");
let DevToolsUtils = require("devtools/toolkit/DevToolsUtils");
let { dbg_assert } = DevToolsUtils;
let { TabSources, isHiddenSource } = require("./utils/TabSources");
let { TabSources } = require("./utils/TabSources");
let makeDebugger = require("./utils/make-debugger");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
loader.lazyRequireGetter(this, "RootActor", "devtools/server/actors/root", true);
loader.lazyRequireGetter(this, "AddonThreadActor", "devtools/server/actors/script", true);
loader.lazyRequireGetter(this, "ThreadActor", "devtools/server/actors/script", true);
loader.lazyRequireGetter(this, "mapURIToAddonID", "devtools/server/actors/utils/map-uri-to-addon-id");
loader.lazyRequireGetter(this, "unwrapDebuggerObjectGlobal", "devtools/server/actors/script", true);
loader.lazyRequireGetter(this, "BrowserAddonActor", "devtools/server/actors/addon", true);
loader.lazyImporter(this, "AddonManager", "resource://gre/modules/AddonManager.jsm");
// Assumptions on events module:
@ -111,35 +111,6 @@ function sendShutdownEvent() {
exports.sendShutdownEvent = sendShutdownEvent;
/**
* Unwrap a global that is wrapped in a |Debugger.Object|, or if the global has
* become a dead object, return |undefined|.
*
* @param Debugger.Object wrappedGlobal
* The |Debugger.Object| which wraps a global.
*
* @returns {Object|undefined}
* Returns the unwrapped global object or |undefined| if unwrapping
* failed.
*/
const unwrapDebuggerObjectGlobal = wrappedGlobal => {
try {
// Because of bug 991399 we sometimes get nuked window references here. We
// just bail out in that case.
//
// Note that addon sandboxes have a DOMWindow as their prototype. So make
// sure that we can touch the prototype too (whatever it is), in case _it_
// is it a nuked window reference. We force stringification to make sure
// that any dead object proxies make themselves known.
let global = wrappedGlobal.unsafeDereference();
Object.getPrototypeOf(global) + "";
return global;
}
catch (e) {
return undefined;
}
};
/**
* Construct a root actor appropriate for use in a server running in a
* browser. The returned root actor:
@ -2005,238 +1976,6 @@ BrowserAddonList.prototype.onUninstalled = function (aAddon) {
exports.BrowserAddonList = BrowserAddonList;
function BrowserAddonActor(aConnection, aAddon) {
this.conn = aConnection;
this._addon = aAddon;
this._contextPool = new ActorPool(this.conn);
this.conn.addActorPool(this._contextPool);
this._threadActor = null;
this._global = null;
this._shouldAddNewGlobalAsDebuggee = this._shouldAddNewGlobalAsDebuggee.bind(this);
this.makeDebugger = makeDebugger.bind(null, {
findDebuggees: this._findDebuggees.bind(this),
shouldAddNewGlobalAsDebuggee: this._shouldAddNewGlobalAsDebuggee
});
AddonManager.addAddonListener(this);
}
BrowserAddonActor.prototype = {
actorPrefix: "addon",
get exited() {
return !this._addon;
},
get id() {
return this._addon.id;
},
get url() {
return this._addon.sourceURI ? this._addon.sourceURI.spec : undefined;
},
get attached() {
return this._threadActor;
},
get global() {
return this._global;
},
get sources() {
if (!this._sources) {
dbg_assert(this.threadActor, "threadActor should exist when creating sources.");
this._sources = new TabSources(this._threadActor, this._allowSource);
}
return this._sources;
},
form: function BAA_form() {
dbg_assert(this.actorID, "addon should have an actorID.");
if (!this._consoleActor) {
let {AddonConsoleActor} = require("devtools/server/actors/webconsole");
this._consoleActor = new AddonConsoleActor(this._addon, this.conn, this);
this._contextPool.addActor(this._consoleActor);
}
return {
actor: this.actorID,
id: this.id,
name: this._addon.name,
url: this.url,
debuggable: this._addon.isDebuggable,
consoleActor: this._consoleActor.actorID,
traits: {
highlightable: false,
networkMonitor: false,
},
};
},
disconnect: function BAA_disconnect() {
this.conn.removeActorPool(this._contextPool);
this._contextPool = null;
this._consoleActor = null;
this._addon = null;
this._global = null;
AddonManager.removeAddonListener(this);
},
setOptions: function BAA_setOptions(aOptions) {
if ("global" in aOptions) {
this._global = aOptions.global;
}
},
onDisabled: function BAA_onDisabled(aAddon) {
if (aAddon != this._addon) {
return;
}
this._global = null;
},
onUninstalled: function BAA_onUninstalled(aAddon) {
if (aAddon != this._addon) {
return;
}
if (this.attached) {
this.onDetach();
this.conn.send({ from: this.actorID, type: "tabDetached" });
}
this.disconnect();
},
onAttach: function BAA_onAttach() {
if (this.exited) {
return { type: "exited" };
}
if (!this.attached) {
this._threadActor = new AddonThreadActor(this.conn, this);
this._contextPool.addActor(this._threadActor);
}
return { type: "tabAttached", threadActor: this._threadActor.actorID };
},
onDetach: function BAA_onDetach() {
if (!this.attached) {
return { error: "wrongState" };
}
this._contextPool.removeActor(this._threadActor);
this._threadActor = null;
this._sources = null;
return { type: "detached" };
},
preNest: function() {
let e = Services.wm.getEnumerator(null);
while (e.hasMoreElements()) {
let win = e.getNext();
let windowUtils = win.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIDOMWindowUtils);
windowUtils.suppressEventHandling(true);
windowUtils.suspendTimeouts();
}
},
postNest: function() {
let e = Services.wm.getEnumerator(null);
while (e.hasMoreElements()) {
let win = e.getNext();
let windowUtils = win.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIDOMWindowUtils);
windowUtils.resumeTimeouts();
windowUtils.suppressEventHandling(false);
}
},
/**
* Return true if the given global is associated with this addon and should be
* added as a debuggee, false otherwise.
*/
_shouldAddNewGlobalAsDebuggee: function (aGlobal) {
const global = unwrapDebuggerObjectGlobal(aGlobal);
try {
// This will fail for non-Sandbox objects, hence the try-catch block.
let metadata = Cu.getSandboxMetadata(global);
if (metadata) {
return metadata.addonID === this.id;
}
} catch (e) {}
if (global instanceof Ci.nsIDOMWindow) {
let id = {};
if (mapURIToAddonID(global.document.documentURIObject, id)) {
return id.value === this.id;
}
return false;
}
// Check the global for a __URI__ property and then try to map that to an
// add-on
let uridescriptor = aGlobal.getOwnPropertyDescriptor("__URI__");
if (uridescriptor && "value" in uridescriptor && uridescriptor.value) {
let uri;
try {
uri = Services.io.newURI(uridescriptor.value, null, null);
}
catch (e) {
DevToolsUtils.reportException(
"BrowserAddonActor.prototype._shouldAddNewGlobalAsDebuggee",
new Error("Invalid URI: " + uridescriptor.value)
);
return false;
}
let id = {};
if (mapURIToAddonID(uri, id)) {
return id.value === this.id;
}
}
return false;
},
/**
* Override the eligibility check for scripts and sources to make
* sure every script and source with a URL is stored when debugging
* add-ons.
*/
_allowSource: function(aSource) {
// XPIProvider.jsm evals some code in every add-on's bootstrap.js. Hide it.
if (aSource.url === "resource://gre/modules/addons/XPIProvider.jsm") {
return false;
}
return true;
},
/**
* Yield the current set of globals associated with this addon that should be
* added as debuggees.
*/
_findDebuggees: function (dbg) {
return dbg.findAllGlobals().filter(this._shouldAddNewGlobalAsDebuggee);
}
};
BrowserAddonActor.prototype.requestTypes = {
"attach": BrowserAddonActor.prototype.onAttach,
"detach": BrowserAddonActor.prototype.onDetach
};
/**
* The DebuggerProgressListener object is an nsIWebProgressListener which
* handles onStateChange events for the inspected browser. If the user tries to

View File

@ -9,7 +9,6 @@
const { Cc, Ci, Cu } = require("chrome");
const { DebuggerServer, ActorPool } = require("devtools/server/main");
const { EnvironmentActor, LongStringActor, ObjectActor, ThreadActor } = require("devtools/server/actors/script");
const { update } = require("devtools/toolkit/DevToolsUtils");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
@ -735,8 +734,6 @@ WebConsoleActor.prototype =
}
}
messages.sort(function(a, b) { return a.timeStamp - b.timeStamp; });
return {
from: this.actorID,
messages: messages,
@ -1553,91 +1550,6 @@ WebConsoleActor.prototype.requestTypes =
exports.WebConsoleActor = WebConsoleActor;
/**
* The AddonConsoleActor implements capabilities needed for the add-on web
* console feature.
*
* @constructor
* @param object aAddon
* The add-on that this console watches.
* @param object aConnection
* The connection to the client, DebuggerServerConnection.
* @param object aParentActor
* The parent BrowserAddonActor actor.
*/
function AddonConsoleActor(aAddon, aConnection, aParentActor)
{
this.addon = aAddon;
WebConsoleActor.call(this, aConnection, aParentActor);
}
AddonConsoleActor.prototype = Object.create(WebConsoleActor.prototype);
update(AddonConsoleActor.prototype, {
constructor: AddonConsoleActor,
actorPrefix: "addonConsole",
/**
* The add-on that this console watches.
*/
addon: null,
/**
* The main add-on JS global
*/
get window() {
return this.parentActor.global;
},
/**
* Destroy the current AddonConsoleActor instance.
*/
disconnect: function ACA_disconnect()
{
WebConsoleActor.prototype.disconnect.call(this);
this.addon = null;
},
/**
* Handler for the "startListeners" request.
*
* @param object aRequest
* The JSON request object received from the Web Console client.
* @return object
* The response object which holds the startedListeners array.
*/
onStartListeners: function ACA_onStartListeners(aRequest)
{
let startedListeners = [];
while (aRequest.listeners.length > 0) {
let listener = aRequest.listeners.shift();
switch (listener) {
case "ConsoleAPI":
if (!this.consoleAPIListener) {
this.consoleAPIListener =
new ConsoleAPIListener(null, this, "addon/" + this.addon.id);
this.consoleAPIListener.init();
}
startedListeners.push(listener);
break;
}
}
return {
startedListeners: startedListeners,
nativeConsoleAPI: true,
traits: this.traits,
};
},
});
AddonConsoleActor.prototype.requestTypes = Object.create(WebConsoleActor.prototype.requestTypes);
AddonConsoleActor.prototype.requestTypes.startListeners = AddonConsoleActor.prototype.onStartListeners;
exports.AddonConsoleActor = AddonConsoleActor;
/**
* Creates an actor for a network event.
*
@ -1692,6 +1604,7 @@ NetworkEventActor.prototype =
return {
actor: this.actorID,
startedDateTime: this._startedDateTime,
timeStamp: Date.parse(this._startedDateTime),
url: this._request.url,
method: this._request.method,
isXHR: this._isXHR,

View File

@ -34,6 +34,7 @@ EXTRA_JS_MODULES.devtools.server += [
EXTRA_JS_MODULES.devtools.server.actors += [
'actors/actor-registry.js',
'actors/addon.js',
'actors/animation.js',
'actors/call-watcher.js',
'actors/canvas.js',

View File

@ -8,6 +8,7 @@
const {Cc, Ci, Cu} = require("chrome");
const DevToolsUtils = require("devtools/toolkit/DevToolsUtils");
const EventEmitter = require("devtools/toolkit/event-emitter");
loader.lazyImporter(this, "LongStringClient", "resource://gre/modules/devtools/dbg-client.jsm");
@ -28,11 +29,17 @@ function WebConsoleClient(aDebuggerClient, aResponse)
this._longStrings = {};
this.traits = aResponse.traits || {};
this.events = [];
this._networkRequests = new Map();
this.pendingEvaluationResults = new Map();
this.onEvaluationResult = this.onEvaluationResult.bind(this);
this.onNetworkEvent = this._onNetworkEvent.bind(this);
this.onNetworkEventUpdate = this._onNetworkEventUpdate.bind(this);
this._client.addListener("evaluationResult", this.onEvaluationResult);
this._client.addListener("networkEvent", this.onNetworkEvent);
this._client.addListener("networkEventUpdate", this.onNetworkEventUpdate);
EventEmitter.decorate(this);
}
exports.WebConsoleClient = WebConsoleClient;
@ -41,24 +48,145 @@ WebConsoleClient.prototype = {
_longStrings: null,
traits: null,
/**
* Holds the network requests currently displayed by the Web Console. Each key
* represents the connection ID and the value is network request information.
* @private
* @type object
*/
_networkRequests: null,
getNetworkRequest(actorId) {
return this._networkRequests.get(actorId);
},
hasNetworkRequest(actorId) {
return this._networkRequests.has(actorId);
},
removeNetworkRequest(actorId) {
this._networkRequests.delete(actorId);
},
getNetworkEvents() {
return this._networkRequests.values();
},
get actor() { return this._actor; },
/**
* The "networkEvent" message type handler. We redirect any message to
* the UI for displaying.
*
* @private
* @param string type
* Message type.
* @param object packet
* The message received from the server.
*/
_onNetworkEvent: function (type, packet)
{
if (packet.from == this._actor) {
let actor = packet.eventActor;
let networkInfo = {
_type: "NetworkEvent",
timeStamp: actor.timeStamp,
node: null,
actor: actor.actor,
discardRequestBody: true,
discardResponseBody: true,
startedDateTime: actor.startedDateTime,
request: {
url: actor.url,
method: actor.method,
},
isXHR: actor.isXHR,
response: {},
timings: {},
updates: [], // track the list of network event updates
private: actor.private,
fromCache: actor.fromCache
};
this._networkRequests.set(actor.actor, networkInfo);
this.emit("networkEvent", networkInfo);
}
},
/**
* The "networkEventUpdate" message type handler. We redirect any message to
* the UI for displaying.
*
* @private
* @param string type
* Message type.
* @param object packet
* The message received from the server.
*/
_onNetworkEventUpdate: function (type, packet)
{
let networkInfo = this.getNetworkRequest(packet.from);
if (!networkInfo) {
return;
}
networkInfo.updates.push(packet.updateType);
switch (packet.updateType) {
case "requestHeaders":
networkInfo.request.headersSize = packet.headersSize;
break;
case "requestPostData":
networkInfo.discardRequestBody = packet.discardRequestBody;
networkInfo.request.bodySize = packet.dataSize;
break;
case "responseStart":
networkInfo.response.httpVersion = packet.response.httpVersion;
networkInfo.response.status = packet.response.status;
networkInfo.response.statusText = packet.response.statusText;
networkInfo.response.headersSize = packet.response.headersSize;
networkInfo.response.remoteAddress = packet.response.remoteAddress;
networkInfo.response.remotePort = packet.response.remotePort;
networkInfo.discardResponseBody = packet.response.discardResponseBody;
break;
case "responseContent":
networkInfo.response.content = {
mimeType: packet.mimeType,
};
networkInfo.response.bodySize = packet.contentSize;
networkInfo.response.transferredSize = packet.transferredSize;
networkInfo.discardResponseBody = packet.discardResponseBody;
break;
case "eventTimings":
networkInfo.totalTime = packet.totalTime;
break;
case "securityInfo":
networkInfo.securityInfo = packet.state;
break;
}
this.emit("networkEventUpdate", {
packet: packet,
networkInfo
});
},
/**
* Retrieve the cached messages from the server.
*
* @see this.CACHED_MESSAGES
* @param array aTypes
* @param array types
* The array of message types you want from the server. See
* this.CACHED_MESSAGES for known types.
* @param function aOnResponse
* The function invoked when the response is received.
*/
getCachedMessages: function WCC_getCachedMessages(aTypes, aOnResponse)
getCachedMessages: function WCC_getCachedMessages(types, aOnResponse)
{
let packet = {
to: this._actor,
type: "getCachedMessages",
messageTypes: aTypes,
messageTypes: types,
};
this._client.request(packet, aOnResponse);
},
@ -473,10 +601,18 @@ WebConsoleClient.prototype = {
detach: function WCC_detach(aOnResponse)
{
this._client.removeListener("evaluationResult", this.onEvaluationResult);
this._client.removeListener("networkEvent", this.onNetworkEvent);
this._client.removeListener("networkEventUpdate", this.onNetworkEventUpdate);
this.stopListeners(null, aOnResponse);
this._longStrings = null;
this._client = null;
this.pendingEvaluationResults.clear();
this.pendingEvaluationResults = null;
this.clearNetworkRequests();
this._networkRequests = null;
},
clearNetworkRequests: function () {
this._networkRequests.clear();
}
};

View File

@ -1496,13 +1496,6 @@ ConsoleAPIListener.prototype =
messages = messages.filter((m) => m.consoleID == this.consoleID);
}
// ConsoleAPIStorage gives up messages sorted, but we ask for different
// blocks of events and we must sort them again in order to show them in the
// proper order.
messages = messages.sort(function(a, b) {
return a.timeStamp - b.timeStamp;
});
if (aIncludePrivate) {
return messages;
}

View File

@ -1357,8 +1357,18 @@ function verifyDirSignedState(aDir, aAddon) {
if (!SIGNED_TYPES.has(aAddon.type))
return Promise.resolve(undefined);
// TODO: Get the certificate for an unpacked add-on (bug 1038072)
return Promise.resolve(AddonManager.SIGNEDSTATE_MISSING);
let certDB = Cc["@mozilla.org/security/x509certdb;1"]
.getService(Ci.nsIX509CertDB);
let root = Ci.nsIX509CertDB.AddonsPublicRoot;
if (!REQUIRE_SIGNING && Preferences.get(PREF_XPI_SIGNATURES_DEV_ROOT, false))
root = Ci.nsIX509CertDB.AddonsStageRoot;
return new Promise(resolve => {
certDB.verifySignedDirectoryAsync(root, aDir, (aRv, aCert) => {
resolve(getSignedStatus(aRv, aCert, aAddon.id));
});
});
}
/**

View File

@ -236,6 +236,12 @@ fail-if = buildapp == "mulet" || os == "android"
[test_pref_properties.js]
[test_registry.js]
[test_safemode.js]
[test_signed_verify.js]
[test_signed_inject.js]
skip-if = true
[test_signed_install.js]
run-sequentially = Uses hardcoded ports in xpi files.
[test_signed_migrate.js]
[test_startup.js]
# Bug 676992: test consistently fails on Android
fail-if = os == "android"

View File

@ -22,12 +22,6 @@ skip-if = appname != "firefox"
[test_provider_unsafe_access_shutdown.js]
[test_provider_unsafe_access_startup.js]
[test_shutdown.js]
[test_signed_verify.js]
[test_signed_inject.js]
skip-if = true
[test_signed_install.js]
run-sequentially = Uses hardcoded ports in xpi files.
[test_signed_migrate.js]
[test_XPIcancel.js]
[test_XPIStates.js]

View File

@ -1641,8 +1641,8 @@
!macroend
!define RegisterDLL `!insertmacro RegisterDLL`
!define UnregisterDLL `!insertmacro UnregisterDLL`
!define RegisterDLL "!insertmacro RegisterDLL"
!define UnregisterDLL "!insertmacro UnregisterDLL"
################################################################################
@ -4332,24 +4332,21 @@
!macroend
/**
* Parses the precomplete file to remove an installation's files and directories.
* Parses the precomplete file to remove an installation's files and
* directories.
*
* @param _PROGRESSBAR
* The progress bar to update using PBM_STEPIT. Can also be "false" if
* updating a progressbar isn't needed.
* @param _INSTALL_STEP_COUNTER
* The install step counter to increment. The variable specified in
* this parameter is also updated. Can also be "false" if a counter
* isn't needed.
* $R2 = false if all files were deleted or moved to the tobedeleted directory.
* @param _CALLBACK
* The function address of a callback function for progress or "false"
* if there is no callback function.
*
* $R3 = false if all files were deleted or moved to the tobedeleted directory.
* true if file(s) could not be moved to the tobedeleted directory.
* $R3 = Path to temporary precomplete file.
* $R4 = File handle for the temporary precomplete file.
* $R5 = String returned from FileRead.
* $R6 = First seven characters of the string returned from FileRead.
* $R7 = Temporary file path used to rename files that are in use.
* $R8 = _PROGRESSBAR
* $R9 = _INSTALL_STEP_COUNTER
* $R4 = Path to temporary precomplete file.
* $R5 = File handle for the temporary precomplete file.
* $R6 = String returned from FileRead.
* $R7 = First seven characters of the string returned from FileRead.
* $R8 = Temporary file path used to rename files that are in use.
* $R9 = _CALLBACK
*/
!macro RemovePrecompleteEntries
@ -4368,95 +4365,89 @@
Function ${_MOZFUNC_UN}RemovePrecompleteEntries
Exch $R9
Exch 1
Exch $R8
Push $R8
Push $R7
Push $R6
Push $R5
Push $R4
Push $R3
Push $R2
${If} ${FileExists} "$INSTDIR\precomplete"
StrCpy $R2 "false"
StrCpy $R3 "false"
RmDir /r "$INSTDIR\${TO_BE_DELETED}"
CreateDirectory "$INSTDIR\${TO_BE_DELETED}"
GetTempFileName $R3 "$INSTDIR\${TO_BE_DELETED}"
Delete "$R3"
Rename "$INSTDIR\precomplete" "$R3"
GetTempFileName $R4 "$INSTDIR\${TO_BE_DELETED}"
Delete "$R4"
Rename "$INSTDIR\precomplete" "$R4"
ClearErrors
; Rename and then remove files
FileOpen $R4 "$R3" r
FileOpen $R5 "$R4" r
${Do}
FileRead $R4 $R5
FileRead $R5 $R6
${If} ${Errors}
${Break}
${EndIf}
${${_MOZFUNC_UN}TrimNewLines} "$R5" $R5
${${_MOZFUNC_UN}TrimNewLines} "$R6" $R6
; Replace all occurrences of "/" with "\".
${${_MOZFUNC_UN}WordReplace} "$R5" "/" "\" "+" $R5
${${_MOZFUNC_UN}WordReplace} "$R6" "/" "\" "+" $R6
; Copy the first 7 chars
StrCpy $R6 "$R5" 7
${If} "$R6" == "remove "
StrCpy $R7 "$R6" 7
${If} "$R7" == "remove "
; Copy the string starting after the 8th char
StrCpy $R5 "$R5" "" 8
StrCpy $R6 "$R6" "" 8
; Copy all but the last char to remove the double quote.
StrCpy $R5 "$R5" -1
${If} ${FileExists} "$INSTDIR\$R5"
StrCpy $R6 "$R6" -1
${If} ${FileExists} "$INSTDIR\$R6"
${Unless} "$R9" == "false"
IntOp $R9 $R9 + 2
${EndUnless}
${Unless} "$R8" == "false"
SendMessage $R8 ${PBM_STEPIT} 0 0
SendMessage $R8 ${PBM_STEPIT} 0 0
Call $R9
${EndUnless}
ClearErrors
Delete "$INSTDIR\$R5"
Delete "$INSTDIR\$R6"
${If} ${Errors}
GetTempFileName $R7 "$INSTDIR\${TO_BE_DELETED}"
Delete "$R7"
GetTempFileName $R8 "$INSTDIR\${TO_BE_DELETED}"
Delete "$R8"
ClearErrors
Rename "$INSTDIR\$R5" "$R7"
Rename "$INSTDIR\$R6" "$R8"
${Unless} ${Errors}
Delete /REBOOTOK "$R7"
Delete /REBOOTOK "$R8"
ClearErrors
${EndUnless}
!ifdef __UNINSTALL__
${If} ${Errors}
Delete /REBOOTOK "$INSTDIR\$R5"
StrCpy $R2 "true"
Delete /REBOOTOK "$INSTDIR\$R6"
StrCpy $R3 "true"
ClearErrors
${EndIf}
!endif
${EndIf}
${EndIf}
${ElseIf} "$R6" == "rmdir $\""
${ElseIf} "$R7" == "rmdir $\""
; Copy the string starting after the 7th char.
StrCpy $R5 "$R5" "" 7
StrCpy $R6 "$R6" "" 7
; Copy all but the last two chars to remove the slash and the double quote.
StrCpy $R5 "$R5" -2
${If} ${FileExists} "$INSTDIR\$R5"
StrCpy $R6 "$R6" -2
${If} ${FileExists} "$INSTDIR\$R6"
; Ignore directory removal errors
RmDir "$INSTDIR\$R5"
RmDir "$INSTDIR\$R6"
ClearErrors
${EndIf}
${EndIf}
${Loop}
FileClose $R4
FileClose $R5
; Delete the temporary precomplete file
Delete /REBOOTOK "$R3"
Delete /REBOOTOK "$R4"
RmDir /r /REBOOTOK "$INSTDIR\${TO_BE_DELETED}"
${If} ${RebootFlag}
${AndIf} "$R2" == "false"
${AndIf} "$R3" == "false"
; Clear the reboot flag if all files were deleted or moved to the
; tobedeleted directory.
SetRebootFlag false
@ -4465,14 +4456,12 @@
ClearErrors
Pop $R2
Pop $R3
Pop $R4
Pop $R5
Pop $R6
Pop $R7
Exch $R8
Exch 1
Pop $R8
Exch $R9
FunctionEnd
@ -4480,24 +4469,19 @@
!endif
!macroend
!macro RemovePrecompleteEntriesCall _PROGRESSBAR _INSTALL_STEP_COUNTER
!macro RemovePrecompleteEntriesCall _CALLBACK
!verbose push
Push "${_PROGRESSBAR}"
Push "${_INSTALL_STEP_COUNTER}"
Push "${_CALLBACK}"
!verbose ${_MOZFUNC_VERBOSE}
Call RemovePrecompleteEntries
Pop ${_INSTALL_STEP_COUNTER}
!verbose pop
!macroend
!macro un.RemovePrecompleteEntriesCall _PROGRESSBAR _INSTALL_STEP_COUNTER
!macro un.RemovePrecompleteEntriesCall _CALLBACK
!verbose push
!verbose ${_MOZFUNC_VERBOSE}
Push "${_PROGRESSBAR}"
Push "${_INSTALL_STEP_COUNTER}"
Push "${_CALLBACK}"
Call un.RemovePrecompleteEntries
Pop ${_INSTALL_STEP_COUNTER}
Pop $0
!verbose pop
!macroend
@ -7328,6 +7312,155 @@
!endif
!macroend
################################################################################
# Helpers for taskbar progress
!ifndef CLSCTX_INPROC_SERVER
!define CLSCTX_INPROC_SERVER 1
!endif
!define CLSID_ITaskbarList {56fdf344-fd6d-11d0-958a-006097c9a090}
!define IID_ITaskbarList3 {ea1afb91-9e28-4b86-90e9-9e9f8a5eefaf}
!define ITaskbarList3->SetProgressValue $ITaskbarList3->9
!define ITaskbarList3->SetProgressState $ITaskbarList3->10
/**
* Creates a single uninitialized object of the ITaskbarList class with a
* reference to the ITaskbarList3 interface. This object can be used to set
* progress and state on the installer's taskbar icon using the helper macros
* in this section.
*/
!macro ITBL3Create
!ifndef ${_MOZFUNC_UN}ITBL3Create
Var ITaskbarList3
!verbose push
!verbose ${_MOZFUNC_VERBOSE}
!define ${_MOZFUNC_UN}ITBL3Create "!insertmacro ${_MOZFUNC_UN}ITBL3CreateCall"
Function ${_MOZFUNC_UN}ITBL3Create
; Setting to 0 allows the helper macros to detect when the object was not
; created.
StrCpy $ITaskbarList3 0
; Don't create when running silently.
${Unless} ${Silent}
; This is only supported on Win 7 and above.
${If} ${AtLeastWin7}
System::Call "ole32::CoCreateInstance(g '${CLSID_ITaskbarList}', \
i 0, \
i ${CLSCTX_INPROC_SERVER}, \
g '${IID_ITaskbarList3}', \
*i .s)"
Pop $ITaskbarList3
${EndIf}
${EndUnless}
FunctionEnd
!verbose pop
!endif
!macroend
!macro ITBL3CreateCall
!verbose push
!verbose ${_MOZFUNC_VERBOSE}
Call ITBL3Create
!verbose pop
!macroend
!macro un.ITBL3CreateCall _PATH_TO_IMAGE
!verbose push
!verbose ${_MOZFUNC_VERBOSE}
Call un.ITBL3Create
!verbose pop
!macroend
!macro un.ITBL3Create
!ifndef un.ITBL3Create
!verbose push
!verbose ${_MOZFUNC_VERBOSE}
!undef _MOZFUNC_UN
!define _MOZFUNC_UN "un."
!insertmacro ITBL3Create
!undef _MOZFUNC_UN
!define _MOZFUNC_UN
!verbose pop
!endif
!macroend
/**
* Sets the percentage completed on the taskbar process icon progress indicator.
*
* @param _COMPLETED
* The proportion of the operation that has been completed in relation
* to _TOTAL.
* @param _TOTAL
* The value _COMPLETED will have when the operation has completed.
*
* $R8 = _COMPLETED
* $R9 = _TOTAL
*/
!macro ITBL3SetProgressValueCall _COMPLETED _TOTAL
Push ${_COMPLETED}
Push ${_TOTAL}
${CallArtificialFunction} ITBL3SetProgressValue_
!macroend
!define ITBL3SetProgressValue "!insertmacro ITBL3SetProgressValueCall"
!define un.ITBL3SetProgressValue "!insertmacro ITBL3SetProgressValueCall"
!macro ITBL3SetProgressValue_
Exch $R9
Exch 1
Exch $R8
${If} ${AtLeastWin7}
${AndIf} $ITaskbarList3 <> 0
System::Call "${ITaskbarList3->SetProgressValue}(i$HWNDPARENT, l$R8, l$R9)"
${EndIf}
Exch $R8
Exch 1
Exch $R9
!macroend
; Normal state / no progress bar
!define TBPF_NOPROGRESS 0x00000000
; Marquee style progress bar
!define TBPF_INDETERMINATE 0x00000001
; Standard progress bar
!define TBPF_NORMAL 0x00000002
; Red taskbar button to indicate an error occurred
!define TBPF_ERROR 0x00000004
; Yellow taskbar button to indicate user attention (input) is required to
; resume progress
!define TBPF_PAUSED 0x00000008
/**
* Sets the state on the taskbar process icon progress indicator.
*
* @param _STATE
* The state to set on the taskbar icon progress indicator. Only one of
* the states defined above should be specified.
*
* $R9 = _STATE
*/
!macro ITBL3SetProgressStateCall _STATE
Push ${_STATE}
${CallArtificialFunction} ITBL3SetProgressState_
!macroend
!define ITBL3SetProgressState "!insertmacro ITBL3SetProgressStateCall"
!define un.ITBL3SetProgressState "!insertmacro ITBL3SetProgressStateCall"
!macro ITBL3SetProgressState_
Exch $R9
${If} ${AtLeastWin7}
${AndIf} $ITaskbarList3 <> 0
System::Call "${ITaskbarList3->SetProgressState}(i$HWNDPARENT, i$R9)"
${EndIf}
Exch $R9
!macroend
################################################################################
# Helpers for the new user interface
@ -7430,7 +7563,7 @@
Exch $0
Pop ${HANDLE}
!macroend
!define SetStretchedTransparentImage `!insertmacro __SetStretchedTransparentImage`
!define SetStretchedTransparentImage "!insertmacro __SetStretchedTransparentImage"
/**
* Removes a single style from a control.

View File

@ -497,7 +497,7 @@ function hasUpdateMutex() {
XPCOMUtils.defineLazyGetter(this, "gCanApplyUpdates", function aus_gCanApplyUpdates() {
let useService = false;
if (shouldUseService() && isServiceInstalled()) {
if (shouldUseService()) {
// No need to perform directory write checks, the maintenance service will
// be able to write to all directories.
LOG("gCanApplyUpdates - bypass the write checks because we'll use the service");
@ -648,8 +648,7 @@ function getCanStageUpdates() {
return false;
}
if (AppConstants.platform == "win" && isServiceInstalled() &&
shouldUseService()) {
if (AppConstants.platform == "win" && shouldUseService()) {
// No need to perform directory write checks, the maintenance service will
// be able to write to all directories.
LOG("getCanStageUpdates - able to stage updates using the service");
@ -1025,13 +1024,12 @@ function releaseSDCardMountLock() {
/**
* Determines if the service should be used to attempt an update
* or not. For now this is only when PREF_APP_UPDATE_SERVICE_ENABLED
* is true and we have Firefox.
* or not.
*
* @return true if the service should be used for updates.
*/
function shouldUseService() {
if (AppConstants.MOZ_MAINTENANCE_SERVICE) {
if (AppConstants.MOZ_MAINTENANCE_SERVICE && isServiceInstalled()) {
return getPref("getBoolPref",
PREF_APP_UPDATE_SERVICE_ENABLED, false);
}
@ -4214,7 +4212,7 @@ Downloader.prototype = {
if (maxProgress != this._patch.size) {
LOG("Downloader:onProgress - maxProgress: " + maxProgress +
" is not equal to expectd patch size: " + this._patch.size);
" is not equal to expected patch size: " + this._patch.size);
// It's important that we use a different code than
// NS_ERROR_CORRUPTED_CONTENT so that tests can verify the difference
// between a hash error and a wrong download error.