Issue #303 Part 1: Move basilisk files from /browser to /application/basilisk

This commit is contained in:
wolfbeast 2018-06-04 13:17:38 +02:00 • committed by Roy Tam
commit 177b28a898
1938 changed files with 0 additions and 0 deletions

View file

@ -0,0 +1,175 @@
/* 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";
var Cc = Components.classes;
var Ci = Components.interfaces;
var Cu = Components.utils;
this.EXPORTED_SYMBOLS = [ "AboutHomeUtils", "AboutHome" ];
Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
Components.utils.import("resource://gre/modules/Services.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "AppConstants",
"resource://gre/modules/AppConstants.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "AutoMigrate",
"resource:///modules/AutoMigrate.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "fxAccounts",
"resource://gre/modules/FxAccounts.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils",
"resource://gre/modules/PrivateBrowsingUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Promise",
"resource://gre/modules/Promise.jsm");
// Should be bumped up if any data content format changes.
const STARTPAGE_VERSION = 5;
this.AboutHomeUtils = {
/*
* showKnowYourRights - Determines if the user should be shown the
* about:rights notification. The notification should *not* be shown if
* we've already shown the current version, or if the override pref says to
* never show it. The notification *should* be shown if it's never been seen
* before, if a newer version is available, or if the override pref says to
* always show it.
*/
get showKnowYourRights() {
// Look for an unconditional override pref. If set, do what it says.
// (true --> never show, false --> always show)
try {
return !Services.prefs.getBoolPref("browser.rights.override");
} catch (e) { }
// Ditto, for the legacy EULA pref.
try {
return !Services.prefs.getBoolPref("browser.EULA.override");
} catch (e) { }
if (!AppConstants.MC_OFFICIAL) {
// Non-official builds shouldn't show the notification.
return false;
}
// Look to see if the user has seen the current version or not.
var currentVersion = Services.prefs.getIntPref("browser.rights.version");
try {
return !Services.prefs.getBoolPref("browser.rights." + currentVersion + ".shown");
} catch (e) { }
// Legacy: If the user accepted a EULA, we won't annoy them with the
// equivalent about:rights page until the version changes.
try {
return !Services.prefs.getBoolPref("browser.EULA." + currentVersion + ".accepted");
} catch (e) { }
// We haven't shown the notification before, so do so now.
return true;
}
};
/**
* This code provides services to the about:home page. Whenever
* about:home needs to do something chrome-privileged, it sends a
* message that's handled here.
*/
var AboutHome = {
MESSAGES: [
"AboutHome:RestorePreviousSession",
"AboutHome:Downloads",
"AboutHome:Bookmarks",
"AboutHome:History",
"AboutHome:Addons",
"AboutHome:Sync",
"AboutHome:Settings",
"AboutHome:RequestUpdate",
"AboutHome:MaybeShowAutoMigrationUndoNotification",
],
init: function() {
let mm = Cc["@mozilla.org/globalmessagemanager;1"].getService(Ci.nsIMessageListenerManager);
for (let msg of this.MESSAGES) {
mm.addMessageListener(msg, this);
}
},
receiveMessage: function(aMessage) {
let window = aMessage.target.ownerGlobal;
switch (aMessage.name) {
case "AboutHome:RestorePreviousSession":
let ss = Cc["@mozilla.org/browser/sessionstore;1"].
getService(Ci.nsISessionStore);
if (ss.canRestoreLastSession) {
ss.restoreLastSession();
}
break;
case "AboutHome:Downloads":
window.BrowserDownloadsUI();
break;
case "AboutHome:Bookmarks":
window.PlacesCommandHook.showPlacesOrganizer("UnfiledBookmarks");
break;
case "AboutHome:History":
window.PlacesCommandHook.showPlacesOrganizer("History");
break;
case "AboutHome:Addons":
window.BrowserOpenAddonsMgr();
break;
case "AboutHome:Sync":
window.openPreferences("paneSync", { urlParams: { entrypoint: "abouthome" } });
break;
case "AboutHome:Settings":
window.openPreferences();
break;
case "AboutHome:RequestUpdate":
this.sendAboutHomeData(aMessage.target);
break;
case "AboutHome:MaybeShowAutoMigrationUndoNotification":
AutoMigrate.maybeShowUndoNotification(aMessage.target);
break;
}
},
// Send all the chrome-privileged data needed by about:home. This
// gets re-sent when the search engine changes.
sendAboutHomeData: function(target) {
let wrapper = {};
Components.utils.import("resource:///modules/sessionstore/SessionStore.jsm",
wrapper);
let ss = wrapper.SessionStore;
ss.promiseInitialized.then(function() {
let data = {
showRestoreLastSession: ss.canRestoreLastSession,
showKnowYourRights: AboutHomeUtils.showKnowYourRights
};
if (AboutHomeUtils.showKnowYourRights) {
// Set pref to indicate we've shown the notification.
let currentVersion = Services.prefs.getIntPref("browser.rights.version");
Services.prefs.setBoolPref("browser.rights." + currentVersion + ".shown", true);
}
if (target && target.messageManager) {
target.messageManager.sendAsyncMessage("AboutHome:Update", data);
} else {
let mm = Cc["@mozilla.org/globalmessagemanager;1"].getService(Ci.nsIMessageListenerManager);
mm.broadcastAsyncMessage("AboutHome:Update", data);
}
}).then(null, function onError(x) {
Cu.reportError("Error in AboutHome.sendAboutHomeData: " + x);
});
},
};

View file

@ -0,0 +1,43 @@
/* 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";
var Cc = Components.classes;
var Ci = Components.interfaces;
var Cu = Components.utils;
this.EXPORTED_SYMBOLS = [ "AboutNewTab" ];
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/Services.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "AutoMigrate",
"resource:///modules/AutoMigrate.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "NewTabUtils",
"resource://gre/modules/NewTabUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "RemotePages",
"resource://gre/modules/RemotePageManager.jsm");
var AboutNewTab = {
pageListener: null,
init: function() {
this.pageListener = new RemotePages("about:newtab");
this.pageListener.addMessageListener("NewTab:Customize", this.customize.bind(this));
this.pageListener.addMessageListener("NewTab:MaybeShowAutoMigrationUndoNotification",
(msg) => AutoMigrate.maybeShowUndoNotification(msg.target.browser));
},
customize: function(message) {
NewTabUtils.allPages.enabled = message.data.enabled;
NewTabUtils.allPages.enhanced = message.data.enhanced;
},
uninit: function() {
this.pageListener.destroy();
this.pageListener = null;
},
};

View file

@ -0,0 +1,123 @@
/* 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 = ["AttributionCode"];
const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, 'AppConstants',
'resource://gre/modules/AppConstants.jsm');
XPCOMUtils.defineLazyModuleGetter(this, 'OS',
'resource://gre/modules/osfile.jsm');
XPCOMUtils.defineLazyModuleGetter(this, 'Services',
'resource://gre/modules/Services.jsm');
XPCOMUtils.defineLazyModuleGetter(this, 'Task',
'resource://gre/modules/Task.jsm');
const ATTR_CODE_MAX_LENGTH = 200;
const ATTR_CODE_KEYS_REGEX = /^source|medium|campaign|content$/;
const ATTR_CODE_VALUE_REGEX = /[a-zA-Z0-9_%\\-\\.\\(\\)]*/;
const ATTR_CODE_FIELD_SEPARATOR = "%26"; // URL-encoded &
const ATTR_CODE_KEY_VALUE_SEPARATOR = "%3D"; // URL-encoded =
let gCachedAttrData = null;
/**
* Returns an nsIFile for the file containing the attribution data.
*/
function getAttributionFile() {
let file = Services.dirsvc.get("LocalAppData", Ci.nsIFile);
// appinfo does not exist in xpcshell, so we need defaults.
file.append(Services.appinfo.vendor || "mozilla");
file.append(AppConstants.MOZ_APP_NAME);
file.append("postSigningData");
return file;
}
/**
* Returns an object containing a key-value pair for each piece of attribution
* data included in the passed-in attribution code string.
* If the string isn't a valid attribution code, returns an empty object.
*/
function parseAttributionCode(code) {
if (code.length > ATTR_CODE_MAX_LENGTH) {
return {};
}
let isValid = true;
let parsed = {};
for (let param of code.split(ATTR_CODE_FIELD_SEPARATOR)) {
let [key, value] = param.split(ATTR_CODE_KEY_VALUE_SEPARATOR, 2);
if (key && ATTR_CODE_KEYS_REGEX.test(key)) {
if (value && ATTR_CODE_VALUE_REGEX.test(value)) {
parsed[key] = value;
}
} else {
isValid = false;
break;
}
}
return isValid ? parsed : {};
}
var AttributionCode = {
/**
* Reads the attribution code, either from disk or a cached version.
* Returns a promise that fulfills with an object containing the parsed
* attribution data if the code could be read and is valid,
* or an empty object otherwise.
*/
getAttrDataAsync() {
return Task.spawn(function*() {
if (gCachedAttrData != null) {
return gCachedAttrData;
}
let code = "";
try {
let bytes = yield OS.File.read(getAttributionFile().path);
let decoder = new TextDecoder();
code = decoder.decode(bytes);
} catch (ex) {
// The attribution file may already have been deleted,
// or it may have never been installed at all;
// failure to open or read it isn't an error.
}
gCachedAttrData = parseAttributionCode(code);
return gCachedAttrData;
});
},
/**
* Deletes the attribution data file.
* Returns a promise that resolves when the file is deleted,
* or if the file couldn't be deleted (the promise is never rejected).
*/
deleteFileAsync() {
return Task.spawn(function*() {
try {
yield OS.File.remove(getAttributionFile().path);
} catch (ex) {
// The attribution file may already have been deleted,
// or it may have never been installed at all;
// failure to delete it isn't an error.
}
});
},
/**
* Clears the cached attribution code value, if any.
* Does nothing if called from outside of an xpcshell test.
*/
_clearCache() {
let env = Cc["@mozilla.org/process/environment;1"]
.getService(Ci.nsIEnvironment);
if (env.exists("XPCSHELL_TEST_PROFILE_DIR")) {
gCachedAttrData = null;
}
},
};

View file

@ -0,0 +1,888 @@
// 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 = ["BrowserUITelemetry"];
const {interfaces: Ci, utils: Cu} = Components;
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "UITelemetry",
"resource://gre/modules/UITelemetry.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "RecentWindow",
"resource:///modules/RecentWindow.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "CustomizableUI",
"resource:///modules/CustomizableUI.jsm");
XPCOMUtils.defineLazyGetter(this, "Timer", function() {
let timer = {};
Cu.import("resource://gre/modules/Timer.jsm", timer);
return timer;
});
const MS_SECOND = 1000;
const MS_MINUTE = MS_SECOND * 60;
const MS_HOUR = MS_MINUTE * 60;
XPCOMUtils.defineLazyGetter(this, "DEFAULT_AREA_PLACEMENTS", function() {
let result = {
"PanelUI-contents": [
"edit-controls",
"zoom-controls",
"new-window-button",
"privatebrowsing-button",
"save-page-button",
"print-button",
"history-panelmenu",
"fullscreen-button",
"find-button",
"preferences-button",
"add-ons-button",
"sync-button",
"developer-button",
],
"nav-bar": [
"urlbar-container",
"search-container",
"bookmarks-menu-button",
"pocket-button",
"downloads-button",
"home-button",
],
// It's true that toolbar-menubar is not visible
// on OS X, but the XUL node is definitely present
// in the document.
"toolbar-menubar": [
"menubar-items",
],
"TabsToolbar": [
"tabbrowser-tabs",
"new-tab-button",
"alltabs-button",
],
"PersonalToolbar": [
"personal-bookmarks",
],
};
let showCharacterEncoding = Services.prefs.getComplexValue(
"browser.menu.showCharacterEncoding",
Ci.nsIPrefLocalizedString
).data;
if (showCharacterEncoding == "true") {
result["PanelUI-contents"].push("characterencoding-button");
}
return result;
});
XPCOMUtils.defineLazyGetter(this, "DEFAULT_AREAS", function() {
return Object.keys(DEFAULT_AREA_PLACEMENTS);
});
XPCOMUtils.defineLazyGetter(this, "PALETTE_ITEMS", function() {
let result = [
"open-file-button",
"developer-button",
"feed-button",
"email-link-button",
"containers-panelmenu",
];
let panelPlacements = DEFAULT_AREA_PLACEMENTS["PanelUI-contents"];
if (panelPlacements.indexOf("characterencoding-button") == -1) {
result.push("characterencoding-button");
}
if (Services.prefs.getBoolPref("privacy.panicButton.enabled")) {
result.push("panic-button");
}
return result;
});
XPCOMUtils.defineLazyGetter(this, "DEFAULT_ITEMS", function() {
let result = [];
for (let [, buttons] of Object.entries(DEFAULT_AREA_PLACEMENTS)) {
result = result.concat(buttons);
}
return result;
});
XPCOMUtils.defineLazyGetter(this, "ALL_BUILTIN_ITEMS", function() {
// These special cases are for click events on built-in items that are
// contained within customizable items (like the navigation widget).
const SPECIAL_CASES = [
"back-button",
"forward-button",
"urlbar-stop-button",
"urlbar-go-button",
"urlbar-reload-button",
"searchbar",
"cut-button",
"copy-button",
"paste-button",
"zoom-out-button",
"zoom-reset-button",
"zoom-in-button",
"BMB_bookmarksPopup",
"BMB_unsortedBookmarksPopup",
"BMB_bookmarksToolbarPopup",
"search-go-button",
"soundplaying-icon",
]
return DEFAULT_ITEMS.concat(PALETTE_ITEMS)
.concat(SPECIAL_CASES);
});
const OTHER_MOUSEUP_MONITORED_ITEMS = [
"PlacesChevron",
"PlacesToolbarItems",
"menubar-items",
];
// Items that open arrow panels will often be overlapped by
// the panel that they're opening by the time the mouseup
// event is fired, so for these items, we monitor mousedown.
const MOUSEDOWN_MONITORED_ITEMS = [
"PanelUI-menu-button",
];
// Weakly maps browser windows to objects whose keys are relative
// timestamps for when some kind of session started. For example,
// when a customization session started. That way, when the window
// exits customization mode, we can determine how long the session
// lasted.
const WINDOW_DURATION_MAP = new WeakMap();
// Default bucket name, when no other bucket is active.
const BUCKET_DEFAULT = "__DEFAULT__";
// Bucket prefix, for named buckets.
const BUCKET_PREFIX = "bucket_";
// Standard separator to use between different parts of a bucket name, such
// as primary name and the time step string.
const BUCKET_SEPARATOR = "|";
this.BrowserUITelemetry = {
init: function() {
UITelemetry.addSimpleMeasureFunction("toolbars",
this.getToolbarMeasures.bind(this));
UITelemetry.addSimpleMeasureFunction("contextmenu",
this.getContextMenuInfo.bind(this));
UITelemetry.addSimpleMeasureFunction("syncstate",
this.getSyncState.bind(this));
Services.obs.addObserver(this, "sessionstore-windows-restored", false);
Services.obs.addObserver(this, "browser-delayed-startup-finished", false);
Services.obs.addObserver(this, "autocomplete-did-enter-text", false);
CustomizableUI.addListener(this);
},
observe: function(aSubject, aTopic, aData) {
switch (aTopic) {
case "sessionstore-windows-restored":
this._gatherFirstWindowMeasurements();
break;
case "browser-delayed-startup-finished":
this._registerWindow(aSubject);
break;
case "autocomplete-did-enter-text":
let input = aSubject.QueryInterface(Ci.nsIAutoCompleteInput);
if (input && input.id == "urlbar" && !input.inPrivateContext &&
input.popup.selectedIndex != -1) {
this._logAwesomeBarSearchResult(input.textValue);
}
break;
}
},
/**
* For the _countableEvents object, constructs a chain of
* Javascript Objects with the keys in aKeys, with the final
* key getting the value in aEndWith. If the final key already
* exists in the final object, its value is not set. In either
* case, a reference to the second last object in the chain is
* returned.
*
* Example - suppose I want to store:
* _countableEvents: {
* a: {
* b: {
* c: 0
* }
* }
* }
*
* And then increment the "c" value by 1, you could call this
* function like this:
*
* let example = this._ensureObjectChain([a, b, c], 0);
* example["c"]++;
*
* Subsequent repetitions of these last two lines would
* simply result in the c value being incremented again
* and again.
*
* @param aKeys the Array of keys to chain Objects together with.
* @param aEndWith the value to assign to the last key.
* @param aRoot the root object onto which we create/get the object chain
* designated by aKeys.
* @returns a reference to the second last object in the chain -
* so in our example, that'd be "b".
*/
_ensureObjectChain: function(aKeys, aEndWith, aRoot) {
let current = aRoot;
let parent = null;
aKeys.unshift(this._bucket);
for (let [i, key] of aKeys.entries()) {
if (!(key in current)) {
if (i == aKeys.length - 1) {
current[key] = aEndWith;
} else {
current[key] = {};
}
}
parent = current;
current = current[key];
}
return parent;
},
_countableEvents: {},
_countEvent: function(aKeyArray, root=this._countableEvents) {
let countObject = this._ensureObjectChain(aKeyArray, 0, root);
let lastItemKey = aKeyArray[aKeyArray.length - 1];
countObject[lastItemKey]++;
},
_countMouseUpEvent: function(aCategory, aAction, aButton) {
const BUTTONS = ["left", "middle", "right"];
let buttonKey = BUTTONS[aButton];
if (buttonKey) {
this._countEvent([aCategory, aAction, buttonKey]);
}
},
_firstWindowMeasurements: null,
_gatherFirstWindowMeasurements: function() {
// We'll gather measurements as soon as the session has restored.
// We do this here instead of waiting for UITelemetry to ask for
// our measurements because at that point all browser windows have
// probably been closed, since the vast majority of saved-session
// pings are gathered during shutdown.
let win = RecentWindow.getMostRecentBrowserWindow({
private: false,
allowPopups: false,
});
Services.search.init(rv => {
// If there are no such windows (or we've just about found one
// but it's closed already), we're out of luck. :(
let hasWindow = win && !win.closed;
this._firstWindowMeasurements = hasWindow ? this._getWindowMeasurements(win, rv)
: {};
});
},
_registerWindow: function(aWindow) {
aWindow.addEventListener("unload", this);
let document = aWindow.document;
for (let areaID of CustomizableUI.areas) {
let areaNode = document.getElementById(areaID);
if (areaNode) {
(areaNode.customizationTarget || areaNode).addEventListener("mouseup", this);
}
}
for (let itemID of OTHER_MOUSEUP_MONITORED_ITEMS) {
let item = document.getElementById(itemID);
if (item) {
item.addEventListener("mouseup", this);
}
}
for (let itemID of MOUSEDOWN_MONITORED_ITEMS) {
let item = document.getElementById(itemID);
if (item) {
item.addEventListener("mousedown", this);
}
}
WINDOW_DURATION_MAP.set(aWindow, {});
},
_unregisterWindow: function(aWindow) {
aWindow.removeEventListener("unload", this);
let document = aWindow.document;
for (let areaID of CustomizableUI.areas) {
let areaNode = document.getElementById(areaID);
if (areaNode) {
(areaNode.customizationTarget || areaNode).removeEventListener("mouseup", this);
}
}
for (let itemID of OTHER_MOUSEUP_MONITORED_ITEMS) {
let item = document.getElementById(itemID);
if (item) {
item.removeEventListener("mouseup", this);
}
}
for (let itemID of MOUSEDOWN_MONITORED_ITEMS) {
let item = document.getElementById(itemID);
if (item) {
item.removeEventListener("mousedown", this);
}
}
},
handleEvent: function(aEvent) {
switch (aEvent.type) {
case "unload":
this._unregisterWindow(aEvent.currentTarget);
break;
case "mouseup":
this._handleMouseUp(aEvent);
break;
case "mousedown":
this._handleMouseDown(aEvent);
break;
}
},
_handleMouseUp: function(aEvent) {
let targetID = aEvent.currentTarget.id;
switch (targetID) {
case "PlacesToolbarItems":
this._PlacesToolbarItemsMouseUp(aEvent);
break;
case "PlacesChevron":
this._PlacesChevronMouseUp(aEvent);
break;
case "menubar-items":
this._menubarMouseUp(aEvent);
break;
default:
this._checkForBuiltinItem(aEvent);
}
},
_handleMouseDown: function(aEvent) {
if (aEvent.currentTarget.id == "PanelUI-menu-button") {
// _countMouseUpEvent expects a detail for the second argument,
// but we don't really have any details to give. Just passing in
// "button" is probably simpler than trying to modify
// _countMouseUpEvent for this particular case.
this._countMouseUpEvent("click-menu-button", "button", aEvent.button);
}
},
_PlacesChevronMouseUp: function(aEvent) {
let target = aEvent.originalTarget;
let result = target.id == "PlacesChevron" ? "chevron" : "overflowed-item";
this._countMouseUpEvent("click-bookmarks-bar", result, aEvent.button);
},
_PlacesToolbarItemsMouseUp: function(aEvent) {
let target = aEvent.originalTarget;
// If this isn't a bookmark-item, we don't care about it.
if (!target.classList.contains("bookmark-item")) {
return;
}
let result = target.hasAttribute("container") ? "container" : "item";
this._countMouseUpEvent("click-bookmarks-bar", result, aEvent.button);
},
_menubarMouseUp: function(aEvent) {
let target = aEvent.originalTarget;
let tag = target.localName
let result = (tag == "menu" || tag == "menuitem") ? tag : "other";
this._countMouseUpEvent("click-menubar", result, aEvent.button);
},
_bookmarksMenuButtonMouseUp: function(aEvent) {
let bookmarksWidget = CustomizableUI.getWidget("bookmarks-menu-button");
if (bookmarksWidget.areaType == CustomizableUI.TYPE_MENU_PANEL) {
// In the menu panel, only the star is visible, and that opens up the
// bookmarks subview.
this._countMouseUpEvent("click-bookmarks-menu-button", "in-panel",
aEvent.button);
} else {
let clickedItem = aEvent.originalTarget;
// Did we click on the star, or the dropmarker? The star
// has an anonid of "button". If we don't find that, we'll
// assume we clicked on the dropmarker.
let action = "menu";
if (clickedItem.getAttribute("anonid") == "button") {
// We clicked on the star - now we just need to record
// whether or not we're adding a bookmark or editing an
// existing one.
let bookmarksMenuNode =
bookmarksWidget.forWindow(aEvent.target.ownerGlobal).node;
action = bookmarksMenuNode.hasAttribute("starred") ? "edit" : "add";
}
this._countMouseUpEvent("click-bookmarks-menu-button", action,
aEvent.button);
}
},
_checkForBuiltinItem: function(aEvent) {
let item = aEvent.originalTarget;
// We don't want to count clicks on the private browsing
// button for privacy reasons. See bug 1176391.
if (item.id == "privatebrowsing-button") {
return;
}
// We special-case the bookmarks-menu-button, since we want to
// monitor more than just clicks on it.
if (item.id == "bookmarks-menu-button" ||
getIDBasedOnFirstIDedAncestor(item) == "bookmarks-menu-button") {
this._bookmarksMenuButtonMouseUp(aEvent);
return;
}
// Perhaps we're seeing one of the default toolbar items
// being clicked.
if (ALL_BUILTIN_ITEMS.indexOf(item.id) != -1) {
// Base case - we clicked directly on one of our built-in items,
// and we can go ahead and register that click.
this._countMouseUpEvent("click-builtin-item", item.id, aEvent.button);
return;
}
// If not, we need to check if the item's anonid is in our list
// of built-in items to check.
if (ALL_BUILTIN_ITEMS.indexOf(item.getAttribute("anonid")) != -1) {
this._countMouseUpEvent("click-builtin-item", item.getAttribute("anonid"), aEvent.button);
return;
}
// If not, we need to check if one of the ancestors of the clicked
// item is in our list of built-in items to check.
let candidate = getIDBasedOnFirstIDedAncestor(item);
if (ALL_BUILTIN_ITEMS.indexOf(candidate) != -1) {
this._countMouseUpEvent("click-builtin-item", candidate, aEvent.button);
}
},
_getWindowMeasurements: function(aWindow, searchResult) {
let document = aWindow.document;
let result = {};
// Determine if the window is in the maximized, normal or
// fullscreen state.
result.sizemode = document.documentElement.getAttribute("sizemode");
// Determine if the Bookmarks bar is currently visible
let bookmarksBar = document.getElementById("PersonalToolbar");
result.bookmarksBarEnabled = bookmarksBar && !bookmarksBar.collapsed;
// Determine if the menubar is currently visible. On OS X, the menubar
// is never shown, despite not having the collapsed attribute set.
let menuBar = document.getElementById("toolbar-menubar");
result.menuBarEnabled =
menuBar && Services.appinfo.OS != "Darwin"
&& menuBar.getAttribute("autohide") != "true";
// Determine if the titlebar is currently visible.
result.titleBarEnabled = !Services.prefs.getBoolPref("browser.tabs.drawInTitlebar");
// Examine all customizable areas and see what default items
// are present and missing.
let defaultKept = [];
let defaultMoved = [];
let nondefaultAdded = [];
for (let areaID of CustomizableUI.areas) {
let items = CustomizableUI.getWidgetIdsInArea(areaID);
for (let item of items) {
// Is this a default item?
if (DEFAULT_ITEMS.indexOf(item) != -1) {
// Ok, it's a default item - but is it in its default
// toolbar? We use Array.isArray instead of checking for
// toolbarID in DEFAULT_AREA_PLACEMENTS because an add-on might
// be clever and give itself the id of "toString" or something.
if (Array.isArray(DEFAULT_AREA_PLACEMENTS[areaID]) &&
DEFAULT_AREA_PLACEMENTS[areaID].indexOf(item) != -1) {
// The item is in its default toolbar
defaultKept.push(item);
} else {
defaultMoved.push(item);
}
} else if (PALETTE_ITEMS.indexOf(item) != -1) {
// It's a palette item that's been moved into a toolbar
nondefaultAdded.push(item);
}
// else, it's provided by an add-on, and we won't record it.
}
}
// Now go through the items in the palette to see what default
// items are in there.
let paletteItems =
CustomizableUI.getUnusedWidgets(aWindow.gNavToolbox.palette);
let defaultRemoved = [];
for (let item of paletteItems) {
if (DEFAULT_ITEMS.indexOf(item.id) != -1) {
defaultRemoved.push(item.id);
}
}
result.defaultKept = defaultKept;
result.defaultMoved = defaultMoved;
result.nondefaultAdded = nondefaultAdded;
result.defaultRemoved = defaultRemoved;
// Next, determine how many add-on provided toolbars exist.
let addonToolbars = 0;
let toolbars = document.querySelectorAll("toolbar[customizable=true]");
for (let toolbar of toolbars) {
if (DEFAULT_AREAS.indexOf(toolbar.id) == -1) {
addonToolbars++;
}
}
result.addonToolbars = addonToolbars;
// Find out how many open tabs we have in each window
let winEnumerator = Services.wm.getEnumerator("navigator:browser");
let visibleTabs = [];
let hiddenTabs = [];
while (winEnumerator.hasMoreElements()) {
let someWin = winEnumerator.getNext();
if (someWin.gBrowser) {
let visibleTabsNum = someWin.gBrowser.visibleTabs.length;
visibleTabs.push(visibleTabsNum);
hiddenTabs.push(someWin.gBrowser.tabs.length - visibleTabsNum);
}
}
result.visibleTabs = visibleTabs;
result.hiddenTabs = hiddenTabs;
if (Components.isSuccessCode(searchResult)) {
result.currentSearchEngine = Services.search.currentEngine.name;
}
return result;
},
getToolbarMeasures: function() {
let result = this._firstWindowMeasurements || {};
result.countableEvents = this._countableEvents;
result.durations = this._durations;
return result;
},
getSyncState: function() {
let result = {};
for (let sub of ["desktop", "mobile"]) {
let count = 0;
try {
count = Services.prefs.getIntPref("services.sync.clients.devices." + sub);
} catch (ex) {}
result[sub] = count;
}
return result;
},
countCustomizationEvent: function(aEventType) {
this._countEvent(["customize", aEventType]);
},
countSearchEvent: function(source, query, selection) {
this._countEvent(["search", source]);
if ((/^[a-zA-Z]+:[^\/\\]/).test(query)) {
this._countEvent(["search", "urlbar-keyword"]);
}
if (selection) {
this._countEvent(["search", "selection", source, selection.index, selection.kind]);
}
},
countOneoffSearchEvent: function(id, type, where) {
this._countEvent(["search-oneoff", id, type, where]);
},
countSearchSettingsEvent: function(source) {
this._countEvent(["click-builtin-item", source, "search-settings"]);
},
countPanicEvent: function(timeId) {
this._countEvent(["forget-button", timeId]);
},
countTabMutingEvent: function(action, reason) {
this._countEvent(["tab-audio-control", action, reason || "no reason given"]);
},
countSyncedTabEvent: function(what, where) {
// "what" will be, eg, "open"
// "where" will be "toolbarbutton-subview" or "sidebar"
this._countEvent(["synced-tabs", what, where]);
},
countSidebarEvent: function(sidebarID, action) {
// sidebarID is the ID of the sidebar (duh!)
// action will be "hide" or "show"
this._countEvent(["sidebar", sidebarID, action]);
},
_logAwesomeBarSearchResult: function (url) {
let spec = Services.search.parseSubmissionURL(url);
if (spec.engine) {
let matchedEngine = "default";
if (spec.engine.name !== Services.search.currentEngine.name) {
matchedEngine = "other";
}
this.countSearchEvent("autocomplete-" + matchedEngine);
}
},
_durations: {
customization: [],
},
onCustomizeStart: function(aWindow) {
this._countEvent(["customize", "start"]);
let durationMap = WINDOW_DURATION_MAP.get(aWindow);
if (!durationMap) {
durationMap = {};
WINDOW_DURATION_MAP.set(aWindow, durationMap);
}
durationMap.customization = {
start: aWindow.performance.now(),
bucket: this._bucket,
};
},
onCustomizeEnd: function(aWindow) {
let durationMap = WINDOW_DURATION_MAP.get(aWindow);
if (durationMap && "customization" in durationMap) {
let duration = aWindow.performance.now() - durationMap.customization.start;
this._durations.customization.push({
duration: duration,
bucket: durationMap.customization.bucket,
});
delete durationMap.customization;
}
},
_contextMenuItemWhitelist: new Set([
"close-without-interaction", // for closing the menu without clicking it.
"custom-page-item", // The ID we use for page-provided items
"unknown", // The bucket for stuff with no id.
// Everything we know of so far (which will exclude add-on items):
"navigation", "back", "forward", "reload", "stop", "bookmarkpage",
"spell-no-suggestions", "spell-add-to-dictionary",
"spell-undo-add-to-dictionary", "openlinkincurrent", "openlinkintab",
"openlink",
// "openlinkprivate" intentionally omitted for privacy reasons. See bug 1176391.
"bookmarklink", "savelink",
"marklinkMenu", "copyemail", "copylink", "media-play", "media-pause",
"media-mute", "media-unmute", "media-playbackrate",
"media-playbackrate-050x", "media-playbackrate-100x",
"media-playbackrate-125x", "media-playbackrate-150x", "media-playbackrate-200x",
"media-showcontrols", "media-hidecontrols",
"video-fullscreen", "leave-dom-fullscreen",
"reloadimage", "viewimage", "viewvideo", "copyimage-contents", "copyimage",
"copyvideourl", "copyaudiourl", "saveimage", "sendimage",
"setDesktopBackground", "viewimageinfo", "viewimagedesc", "savevideo",
"saveaudio", "video-saveimage", "sendvideo", "sendaudio",
"ctp-play", "ctp-hide", "savepage", "pocket", "markpageMenu",
"viewbgimage", "undo", "cut", "copy", "paste", "delete", "selectall",
"keywordfield", "searchselect", "frame", "showonlythisframe",
"openframeintab", "openframe", "reloadframe", "bookmarkframe", "saveframe",
"printframe", "viewframesource", "viewframeinfo",
"viewpartialsource-selection", "viewpartialsource-mathml",
"viewsource", "viewinfo", "spell-check-enabled",
"spell-add-dictionaries-main", "spell-dictionaries",
"spell-dictionaries-menu", "spell-add-dictionaries",
"bidi-text-direction-toggle", "bidi-page-direction-toggle", "inspect",
"media-eme-learn-more"
]),
_contextMenuInteractions: {},
registerContextMenuInteraction: function(keys, itemID) {
if (itemID) {
if (itemID == "openlinkprivate") {
// Don't record anything, not even an other-item count
// if the user chose to open in a private window. See
// bug 1176391.
return;
}
if (!this._contextMenuItemWhitelist.has(itemID)) {
itemID = "other-item";
}
keys.push(itemID);
}
this._countEvent(keys, this._contextMenuInteractions);
},
getContextMenuInfo: function() {
return this._contextMenuInteractions;
},
_bucket: BUCKET_DEFAULT,
_bucketTimer: null,
/**
* Default bucket name, when no other bucket is active.
*/
get BUCKET_DEFAULT() {
return BUCKET_DEFAULT;
},
/**
* Bucket prefix, for named buckets.
*/
get BUCKET_PREFIX() {
return BUCKET_PREFIX;
},
/**
* Standard separator to use between different parts of a bucket name, such
* as primary name and the time step string.
*/
get BUCKET_SEPARATOR() {
return BUCKET_SEPARATOR;
},
get currentBucket() {
return this._bucket;
},
/**
* Sets a named bucket for all countable events and select durections to be
* put into.
*
* @param aName Name of bucket, or null for default bucket name (__DEFAULT__)
*/
setBucket: function(aName) {
if (this._bucketTimer) {
Timer.clearTimeout(this._bucketTimer);
this._bucketTimer = null;
}
if (aName)
this._bucket = BUCKET_PREFIX + aName;
else
this._bucket = BUCKET_DEFAULT;
},
/**
* Sets a bucket that expires at the rate of a given series of time steps.
* Once the bucket expires, the current bucket will automatically revert to
* the default bucket. While the bucket is expiring, it's name is postfixed
* by '|' followed by a short string representation of the time step it's
* currently in.
* If any other bucket (expiring or normal) is set while an expiring bucket is
* still expiring, the old expiring bucket stops expiring and the new bucket
* immediately takes over.
*
* @param aName Name of bucket.
* @param aTimeSteps An array of times in milliseconds to count up to before
* reverting back to the default bucket. The array of times
* is expected to be pre-sorted in ascending order.
* For example, given a bucket name of 'bucket', the times:
* [60000, 300000, 600000]
* will result in the following buckets:
* * bucket|1m - for the first 1 minute
* * bucket|5m - for the following 4 minutes
* (until 5 minutes after the start)
* * bucket|10m - for the following 5 minutes
* (until 10 minutes after the start)
* * __DEFAULT__ - until a new bucket is set
* @param aTimeOffset Time offset, in milliseconds, from which to start
* counting. For example, if the first time step is 1000ms,
* and the time offset is 300ms, then the next time step
* will become active after 700ms. This affects all
* following time steps also, meaning they will also all be
* timed as though they started expiring 300ms before
* setExpiringBucket was called.
*/
setExpiringBucket: function(aName, aTimeSteps, aTimeOffset = 0) {
if (aTimeSteps.length === 0) {
this.setBucket(null);
return;
}
if (this._bucketTimer) {
Timer.clearTimeout(this._bucketTimer);
this._bucketTimer = null;
}
// Make a copy of the time steps array, so we can safely modify it without
// modifying the original array that external code has passed to us.
let steps = [...aTimeSteps];
let msec = steps.shift();
let postfix = this._toTimeStr(msec);
this.setBucket(aName + BUCKET_SEPARATOR + postfix);
this._bucketTimer = Timer.setTimeout(() => {
this._bucketTimer = null;
this.setExpiringBucket(aName, steps, aTimeOffset + msec);
}, msec - aTimeOffset);
},
/**
* Formats a time interval, in milliseconds, to a minimal non-localized string
* representation. Format is: 'h' for hours, 'm' for minutes, 's' for seconds,
* 'ms' for milliseconds.
* Examples:
* 65 => 65ms
* 1000 => 1s
* 60000 => 1m
* 61000 => 1m01s
*
* @param aTimeMS Time in milliseconds
*
* @return Minimal string representation.
*/
_toTimeStr: function(aTimeMS) {
let timeStr = "";
function reduce(aUnitLength, aSymbol) {
if (aTimeMS >= aUnitLength) {
let units = Math.floor(aTimeMS / aUnitLength);
aTimeMS = aTimeMS - (units * aUnitLength)
timeStr += units + aSymbol;
}
}
reduce(MS_HOUR, "h");
reduce(MS_MINUTE, "m");
reduce(MS_SECOND, "s");
reduce(1, "ms");
return timeStr;
},
};
/**
* Returns the id of the first ancestor of aNode that has an id. If aNode
* has no parent, or no ancestor has an id, returns null.
*
* @param aNode the node to find the first ID'd ancestor of
*/
function getIDBasedOnFirstIDedAncestor(aNode) {
while (!aNode.id) {
aNode = aNode.parentNode;
if (!aNode) {
return null;
}
}
return aNode.id;
}

View file

@ -0,0 +1,468 @@
/* -*- js-indent-level: 2; indent-tabs-mode: nil -*- */
/* 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 = ["BrowserUsageTelemetry"];
const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils",
"resource://gre/modules/PrivateBrowsingUtils.jsm");
// The upper bound for the count of the visited unique domain names.
const MAX_UNIQUE_VISITED_DOMAINS = 100;
// Observed topic names.
const WINDOWS_RESTORED_TOPIC = "sessionstore-windows-restored";
const TAB_RESTORING_TOPIC = "SSTabRestoring";
const TELEMETRY_SUBSESSIONSPLIT_TOPIC = "internal-telemetry-after-subsession-split";
const DOMWINDOW_OPENED_TOPIC = "domwindowopened";
// Probe names.
const MAX_TAB_COUNT_SCALAR_NAME = "browser.engagement.max_concurrent_tab_count";
const MAX_WINDOW_COUNT_SCALAR_NAME = "browser.engagement.max_concurrent_window_count";
const TAB_OPEN_EVENT_COUNT_SCALAR_NAME = "browser.engagement.tab_open_event_count";
const WINDOW_OPEN_EVENT_COUNT_SCALAR_NAME = "browser.engagement.window_open_event_count";
const UNIQUE_DOMAINS_COUNT_SCALAR_NAME = "browser.engagement.unique_domains_count";
const TOTAL_URI_COUNT_SCALAR_NAME = "browser.engagement.total_uri_count";
const UNFILTERED_URI_COUNT_SCALAR_NAME = "browser.engagement.unfiltered_uri_count";
// A list of known search origins.
const KNOWN_SEARCH_SOURCES = [
"abouthome",
"contextmenu",
"newtab",
"searchbar",
"urlbar",
];
const KNOWN_ONEOFF_SOURCES = [
"oneoff-urlbar",
"oneoff-searchbar",
"unknown", // Edge case: this is the searchbar (see bug 1195733 comment 7).
];
function getOpenTabsAndWinsCounts() {
let tabCount = 0;
let winCount = 0;
let browserEnum = Services.wm.getEnumerator("navigator:browser");
while (browserEnum.hasMoreElements()) {
let win = browserEnum.getNext();
winCount++;
tabCount += win.gBrowser.tabs.length;
}
return { tabCount, winCount };
}
function getSearchEngineId(engine) {
if (engine) {
if (engine.identifier) {
return engine.identifier;
}
// Due to bug 1222070, we can't directly check Services.telemetry.canRecordExtended
// here.
const extendedTelemetry = Services.prefs.getBoolPref("toolkit.telemetry.enabled");
if (engine.name && extendedTelemetry) {
// If it's a custom search engine only report the engine name
// if extended Telemetry is enabled.
return "other-" + engine.name;
}
}
return "other";
}
let URICountListener = {
// A set containing the visited domains, see bug 1271310.
_domainSet: new Set(),
// A map to keep track of the URIs loaded from the restored tabs.
_restoredURIsMap: new WeakMap(),
isHttpURI(uri) {
// Only consider http(s) schemas.
return uri.schemeIs("http") || uri.schemeIs("https");
},
addRestoredURI(browser, uri) {
if (!this.isHttpURI(uri)) {
return;
}
this._restoredURIsMap.set(browser, uri.spec);
},
onLocationChange(browser, webProgress, request, uri, flags) {
// Don't count this URI if it's an error page.
if (flags & Ci.nsIWebProgressListener.LOCATION_CHANGE_ERROR_PAGE) {
return;
}
// We only care about top level loads.
if (!webProgress.isTopLevel) {
return;
}
// The SessionStore sets the URI of a tab first, firing onLocationChange the
// first time, then manages content loading using its scheduler. Once content
// loads, we will hit onLocationChange again.
// We can catch the first case by checking for null requests: be advised that
// this can also happen when navigating page fragments, so account for it.
if (!request &&
!(flags & Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT)) {
return;
}
// Track URI loads, even if they're not http(s).
let uriSpec = null;
try {
uriSpec = uri.spec;
} catch (e) {
// If we have troubles parsing the spec, still count this as
// an unfiltered URI.
Services.telemetry.scalarAdd(UNFILTERED_URI_COUNT_SCALAR_NAME, 1);
return;
}
// Don't count about:blank and similar pages, as they would artificially
// inflate the counts.
if (browser.ownerDocument.defaultView.gInitialPages.includes(uriSpec)) {
return;
}
// If the URI we're loading is in the _restoredURIsMap, then it comes from a
// restored tab. If so, let's skip it and remove it from the map as we want to
// count page refreshes.
if (this._restoredURIsMap.get(browser) === uriSpec) {
this._restoredURIsMap.delete(browser);
return;
}
// The URI wasn't from a restored tab. Count it among the unfiltered URIs.
// If this is an http(s) URI, this also gets counted by the "total_uri_count"
// probe.
Services.telemetry.scalarAdd(UNFILTERED_URI_COUNT_SCALAR_NAME, 1);
if (!this.isHttpURI(uri)) {
return;
}
// Update the URI counts.
Services.telemetry.scalarAdd(TOTAL_URI_COUNT_SCALAR_NAME, 1);
// We only want to count the unique domains up to MAX_UNIQUE_VISITED_DOMAINS.
if (this._domainSet.size == MAX_UNIQUE_VISITED_DOMAINS) {
return;
}
// Unique domains should be aggregated by (eTLD + 1): x.test.com and y.test.com
// are counted once as test.com.
try {
// Even if only considering http(s) URIs, |getBaseDomain| could still throw
// due to the URI containing invalid characters or the domain actually being
// an ipv4 or ipv6 address.
this._domainSet.add(Services.eTLD.getBaseDomain(uri));
} catch (e) {
return;
}
Services.telemetry.scalarSet(UNIQUE_DOMAINS_COUNT_SCALAR_NAME, this._domainSet.size);
},
/**
* Reset the counts. This should be called when breaking a session in Telemetry.
*/
reset() {
this._domainSet.clear();
},
QueryInterface: XPCOMUtils.generateQI([Ci.nsIWebProgressListener,
Ci.nsISupportsWeakReference]),
};
let BrowserUsageTelemetry = {
init() {
Services.obs.addObserver(this, WINDOWS_RESTORED_TOPIC, false);
},
/**
* Handle subsession splits in the parent process.
*/
afterSubsessionSplit() {
// Scalars just got cleared due to a subsession split. We need to set the maximum
// concurrent tab and window counts so that they reflect the correct value for the
// new subsession.
const counts = getOpenTabsAndWinsCounts();
Services.telemetry.scalarSetMaximum(MAX_TAB_COUNT_SCALAR_NAME, counts.tabCount);
Services.telemetry.scalarSetMaximum(MAX_WINDOW_COUNT_SCALAR_NAME, counts.winCount);
// Reset the URI counter.
URICountListener.reset();
},
uninit() {
Services.obs.removeObserver(this, DOMWINDOW_OPENED_TOPIC, false);
Services.obs.removeObserver(this, TELEMETRY_SUBSESSIONSPLIT_TOPIC, false);
Services.obs.removeObserver(this, WINDOWS_RESTORED_TOPIC, false);
},
observe(subject, topic, data) {
switch (topic) {
case WINDOWS_RESTORED_TOPIC:
this._setupAfterRestore();
break;
case DOMWINDOW_OPENED_TOPIC:
this._onWindowOpen(subject);
break;
case TELEMETRY_SUBSESSIONSPLIT_TOPIC:
this.afterSubsessionSplit();
break;
}
},
handleEvent(event) {
switch (event.type) {
case "TabOpen":
this._onTabOpen();
break;
case "unload":
this._unregisterWindow(event.target);
break;
case TAB_RESTORING_TOPIC:
// We're restoring a new tab from a previous or crashed session.
// We don't want to track the URIs from these tabs, so let
// |URICountListener| know about them.
let browser = event.target.linkedBrowser;
URICountListener.addRestoredURI(browser, browser.currentURI);
break;
}
},
/**
* The main entry point for recording search related Telemetry. This includes
* search counts and engagement measurements.
*
* Telemetry records only search counts per engine and action origin, but
* nothing pertaining to the search contents themselves.
*
* @param {nsISearchEngine} engine
* The engine handling the search.
* @param {String} source
* Where the search originated from. See KNOWN_SEARCH_SOURCES for allowed
* values.
* @param {Object} [details] Options object.
* @param {Boolean} [details.isOneOff=false]
* true if this event was generated by a one-off search.
* @param {Boolean} [details.isSuggestion=false]
* true if this event was generated by a suggested search.
* @param {Boolean} [details.isAlias=false]
* true if this event was generated by a search using an alias.
* @param {Object} [details.type=null]
* The object describing the event that triggered the search.
* @throws if source is not in the known sources list.
*/
recordSearch(engine, source, details={}) {
const isOneOff = !!details.isOneOff;
const countId = getSearchEngineId(engine) + "." + source;
if (isOneOff) {
if (!KNOWN_ONEOFF_SOURCES.includes(source)) {
// Silently drop the error if this bogus call
// came from 'urlbar' or 'searchbar'. They're
// calling |recordSearch| twice from two different
// code paths because they want to record the search
// in SEARCH_COUNTS.
if (['urlbar', 'searchbar'].includes(source)) {
Services.telemetry.getKeyedHistogramById("SEARCH_COUNTS").add(countId);
return;
}
throw new Error("Unknown source for one-off search: " + source);
}
} else {
if (!KNOWN_SEARCH_SOURCES.includes(source)) {
throw new Error("Unknown source for search: " + source);
}
Services.telemetry.getKeyedHistogramById("SEARCH_COUNTS").add(countId);
}
// Dispatch the search signal to other handlers.
this._handleSearchAction(engine, source, details);
},
_recordSearch(engine, source, action = null) {
let scalarKey = action ? "search_" + action : "search";
Services.telemetry.keyedScalarAdd("browser.engagement.navigation." + source,
scalarKey, 1);
Services.telemetry.recordEvent("navigation", "search", source, action,
{ engine: getSearchEngineId(engine) });
},
_handleSearchAction(engine, source, details) {
switch (source) {
case "urlbar":
case "oneoff-urlbar":
case "searchbar":
case "oneoff-searchbar":
case "unknown": // Edge case: this is the searchbar (see bug 1195733 comment 7).
this._handleSearchAndUrlbar(engine, source, details);
break;
case "abouthome":
this._recordSearch(engine, "about_home", "enter");
break;
case "newtab":
this._recordSearch(engine, "about_newtab", "enter");
break;
case "contextmenu":
this._recordSearch(engine, "contextmenu");
break;
}
},
/**
* This function handles the "urlbar", "urlbar-oneoff", "searchbar" and
* "searchbar-oneoff" sources.
*/
_handleSearchAndUrlbar(engine, source, details) {
// We want "urlbar" and "urlbar-oneoff" (and similar cases) to go in the same
// scalar, but in a different key.
// When using one-offs in the searchbar we get an "unknown" source. See bug
// 1195733 comment 7 for the context. Fix-up the label here.
const sourceName =
(source === "unknown") ? "searchbar" : source.replace("oneoff-", "");
const isOneOff = !!details.isOneOff;
if (isOneOff) {
// We will receive a signal from the "urlbar"/"searchbar" even when the
// search came from "oneoff-urlbar". That's because both signals
// are propagated from search.xml. Skip it if that's the case.
// Moreover, we skip the "unknown" source that comes from the searchbar
// when performing searches from the default search engine. See bug 1195733
// comment 7 for context.
if (["urlbar", "searchbar", "unknown"].includes(source)) {
return;
}
// If that's a legit one-off search signal, record it using the relative key.
this._recordSearch(engine, sourceName, "oneoff");
return;
}
// The search was not a one-off. It was a search with the default search engine.
if (details.isSuggestion) {
// It came from a suggested search, so count it as such.
this._recordSearch(engine, sourceName, "suggestion");
return;
} else if (details.isAlias) {
// This one came from a search that used an alias.
this._recordSearch(engine, sourceName, "alias");
return;
}
// The search signal was generated by typing something and pressing enter.
this._recordSearch(engine, sourceName, "enter");
},
/**
* This gets called shortly after the SessionStore has finished restoring
* windows and tabs. It counts the open tabs and adds listeners to all the
* windows.
*/
_setupAfterRestore() {
// Make sure to catch new chrome windows and subsession splits.
Services.obs.addObserver(this, DOMWINDOW_OPENED_TOPIC, false);
Services.obs.addObserver(this, TELEMETRY_SUBSESSIONSPLIT_TOPIC, false);
// Attach the tabopen handlers to the existing Windows.
let browserEnum = Services.wm.getEnumerator("navigator:browser");
while (browserEnum.hasMoreElements()) {
this._registerWindow(browserEnum.getNext());
}
// Get the initial tab and windows max counts.
const counts = getOpenTabsAndWinsCounts();
Services.telemetry.scalarSetMaximum(MAX_TAB_COUNT_SCALAR_NAME, counts.tabCount);
Services.telemetry.scalarSetMaximum(MAX_WINDOW_COUNT_SCALAR_NAME, counts.winCount);
},
/**
* Adds listeners to a single chrome window.
*/
_registerWindow(win) {
win.addEventListener("unload", this);
win.addEventListener("TabOpen", this, true);
// Don't include URI and domain counts when in private mode.
if (PrivateBrowsingUtils.isWindowPrivate(win)) {
return;
}
win.gBrowser.tabContainer.addEventListener(TAB_RESTORING_TOPIC, this);
win.gBrowser.addTabsProgressListener(URICountListener);
},
/**
* Removes listeners from a single chrome window.
*/
_unregisterWindow(win) {
win.removeEventListener("unload", this);
win.removeEventListener("TabOpen", this, true);
// Don't include URI and domain counts when in private mode.
if (PrivateBrowsingUtils.isWindowPrivate(win.defaultView)) {
return;
}
win.defaultView.gBrowser.tabContainer.removeEventListener(TAB_RESTORING_TOPIC, this);
win.defaultView.gBrowser.removeTabsProgressListener(URICountListener);
},
/**
* Updates the tab counts.
* @param {Number} [newTabCount=0] The count of the opened tabs across all windows. This
* is computed manually if not provided.
*/
_onTabOpen(tabCount = 0) {
// Use the provided tab count if available. Otherwise, go on and compute it.
tabCount = tabCount || getOpenTabsAndWinsCounts().tabCount;
// Update the "tab opened" count and its maximum.
Services.telemetry.scalarAdd(TAB_OPEN_EVENT_COUNT_SCALAR_NAME, 1);
Services.telemetry.scalarSetMaximum(MAX_TAB_COUNT_SCALAR_NAME, tabCount);
},
/**
* Tracks the window count and registers the listeners for the tab count.
* @param{Object} win The window object.
*/
_onWindowOpen(win) {
// Make sure to have a |nsIDOMWindow|.
if (!(win instanceof Ci.nsIDOMWindow)) {
return;
}
let onLoad = () => {
win.removeEventListener("load", onLoad, false);
// Ignore non browser windows.
if (win.document.documentElement.getAttribute("windowtype") != "navigator:browser") {
return;
}
this._registerWindow(win);
// Track the window open event and check the maximum.
const counts = getOpenTabsAndWinsCounts();
Services.telemetry.scalarAdd(WINDOW_OPEN_EVENT_COUNT_SCALAR_NAME, 1);
Services.telemetry.scalarSetMaximum(MAX_WINDOW_COUNT_SCALAR_NAME, counts.winCount);
// We won't receive the "TabOpen" event for the first tab within a new window.
// Account for that.
this._onTabOpen(counts.tabCount);
};
win.addEventListener("load", onLoad, false);
},
};

View file

@ -0,0 +1,164 @@
// -*- Mode: js; tab-width: 2; indent-tabs-mode: nil; js2-basic-offset: 2; js2-skip-preprocessor-directives: t; -*-
/* 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 = ["CastingApps"];
const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/SimpleServiceDiscovery.jsm");
var CastingApps = {
_sendEventToVideo: function (element, data) {
let event = element.ownerDocument.createEvent("CustomEvent");
event.initCustomEvent("media-videoCasting", false, true, JSON.stringify(data));
element.dispatchEvent(event);
},
makeURI: function (url, charset, baseURI) {
return Services.io.newURI(url, charset, baseURI);
},
getVideo: function (element) {
if (!element) {
return null;
}
let extensions = SimpleServiceDiscovery.getSupportedExtensions();
let types = SimpleServiceDiscovery.getSupportedMimeTypes();
// Grab the poster attribute from the <video>
let posterURL = element.poster;
// First, look to see if the <video> has a src attribute
let sourceURL = element.src;
// If empty, try the currentSrc
if (!sourceURL) {
sourceURL = element.currentSrc;
}
if (sourceURL) {
// Use the file extension to guess the mime type
let sourceURI = this.makeURI(sourceURL, null, this.makeURI(element.baseURI));
if (this.allowableExtension(sourceURI, extensions)) {
return { element: element, source: sourceURI.spec, poster: posterURL, sourceURI: sourceURI};
}
}
// Next, look to see if there is a <source> child element that meets
// our needs
let sourceNodes = element.getElementsByTagName("source");
for (let sourceNode of sourceNodes) {
let sourceURI = this.makeURI(sourceNode.src, null, this.makeURI(sourceNode.baseURI));
// Using the type attribute is our ideal way to guess the mime type. Otherwise,
// fallback to using the file extension to guess the mime type
if (this.allowableMimeType(sourceNode.type, types) || this.allowableExtension(sourceURI, extensions)) {
return { element: element, source: sourceURI.spec, poster: posterURL, sourceURI: sourceURI, type: sourceNode.type };
}
}
return null;
},
sendVideoToService: function (videoElement, service) {
if (!service)
return;
let video = this.getVideo(videoElement);
if (!video) {
return;
}
// Make sure we have a player app for the given service
let app = SimpleServiceDiscovery.findAppForService(service);
if (!app)
return;
video.title = videoElement.ownerGlobal.top.document.title;
if (video.element) {
// If the video is currently playing on the device, pause it
if (!video.element.paused) {
video.element.pause();
}
}
app.stop(() => {
app.start(started => {
if (!started) {
Cu.reportError("CastingApps: Unable to start app");
return;
}
app.remoteMedia(remoteMedia => {
if (!remoteMedia) {
Cu.reportError("CastingApps: Failed to create remotemedia");
return;
}
this.session = {
service: service,
app: app,
remoteMedia: remoteMedia,
data: {
title: video.title,
source: video.source,
poster: video.poster
},
videoRef: Cu.getWeakReference(video.element)
};
}, this);
});
});
},
getServicesForVideo: function (videoElement) {
let video = this.getVideo(videoElement);
if (!video) {
return {};
}
let filteredServices = SimpleServiceDiscovery.services.filter(service => {
return this.allowableExtension(video.sourceURI, service.extensions) ||
this.allowableMimeType(video.type, service.types);
});
return filteredServices;
},
getServicesForMirroring: function () {
return SimpleServiceDiscovery.services.filter(service => service.mirror);
},
// RemoteMedia callback API methods
onRemoteMediaStart: function (remoteMedia) {
if (!this.session) {
return;
}
remoteMedia.load(this.session.data);
let video = this.session.videoRef.get();
if (video) {
this._sendEventToVideo(video, { active: true });
}
},
onRemoteMediaStop: function (remoteMedia) {
},
onRemoteMediaStatus: function (remoteMedia) {
},
allowableExtension: function (uri, extensions) {
return (uri instanceof Ci.nsIURL) && extensions.indexOf(uri.fileExtension) != -1;
},
allowableMimeType: function (type, types) {
return types.indexOf(type) != -1;
}
};

View file

@ -0,0 +1,98 @@
/* -*- mode: js; indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
var Cc = Components.classes;
var Ci = Components.interfaces;
var Cu = Components.utils;
this.EXPORTED_SYMBOLS = [ "ContentClick" ];
Cu.import("resource:///modules/PlacesUIUtils.jsm");
Cu.import("resource://gre/modules/PrivateBrowsingUtils.jsm");
Cu.import("resource://gre/modules/Services.jsm");
var ContentClick = {
init: function() {
let mm = Cc["@mozilla.org/globalmessagemanager;1"].getService(Ci.nsIMessageListenerManager);
mm.addMessageListener("Content:Click", this);
},
receiveMessage: function (message) {
switch (message.name) {
case "Content:Click":
this.contentAreaClick(message.json, message.target)
break;
}
},
contentAreaClick: function (json, browser) {
// This is heavily based on contentAreaClick from browser.js (Bug 903016)
// The json is set up in a way to look like an Event.
let window = browser.ownerGlobal;
if (!json.href) {
// Might be middle mouse navigation.
if (Services.prefs.getBoolPref("middlemouse.contentLoadURL") &&
!Services.prefs.getBoolPref("general.autoScroll")) {
window.middleMousePaste(json);
}
return;
}
if (json.bookmark) {
// This is the Opera convention for a special link that, when clicked,
// allows to add a sidebar panel. The link's title attribute contains
// the title that should be used for the sidebar panel.
PlacesUIUtils.showBookmarkDialog({ action: "add"
, type: "bookmark"
, uri: Services.io.newURI(json.href, null, null)
, title: json.title
, loadBookmarkInSidebar: true
, hiddenRows: [ "description"
, "location"
, "keyword" ]
}, window);
return;
}
// Note: We don't need the sidebar code here.
// Mark the page as a user followed link. This is done so that history can
// distinguish automatic embed visits from user activated ones. For example
// pages loaded in frames are embed visits and lost with the session, while
// visits across frames should be preserved.
try {
if (!PrivateBrowsingUtils.isWindowPrivate(window))
PlacesUIUtils.markPageAsFollowedLink(json.href);
} catch (ex) { /* Skip invalid URIs. */ }
// This part is based on handleLinkClick.
var where = window.whereToOpenLink(json);
if (where == "current")
return;
// Todo(903022): code for where == save
let params = {
charset: browser.characterSet,
referrerURI: browser.documentURI,
referrerPolicy: json.referrerPolicy,
noReferrer: json.noReferrer,
allowMixedContent: json.allowMixedContent,
isContentWindowPrivate: json.isContentWindowPrivate,
originPrincipal: json.originPrincipal,
triggeringPrincipal: json.triggeringPrincipal,
};
// The new tab/window must use the same userContextId.
if (json.originAttributes.userContextId) {
params.userContextId = json.originAttributes.userContextId;
}
window.openLinkIn(json.href, where, params);
}
};

View file

@ -0,0 +1,922 @@
/* 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";
var Cc = Components.classes;
var Ci = Components.interfaces;
var Cu = Components.utils;
var Cr = Components.results;
this.EXPORTED_SYMBOLS = [ "TabCrashHandler",
"PluginCrashReporter",
"UnsubmittedCrashHandler" ];
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/Services.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "CrashSubmit",
"resource://gre/modules/CrashSubmit.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "AppConstants",
"resource://gre/modules/AppConstants.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "RemotePages",
"resource://gre/modules/RemotePageManager.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "SessionStore",
"resource:///modules/sessionstore/SessionStore.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Task",
"resource://gre/modules/Task.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "RecentWindow",
"resource:///modules/RecentWindow.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "PluralForm",
"resource://gre/modules/PluralForm.jsm");
XPCOMUtils.defineLazyGetter(this, "gNavigatorBundle", function() {
const url = "chrome://browser/locale/browser.properties";
return Services.strings.createBundle(url);
});
// We don't process crash reports older than 28 days, so don't bother
// submitting them
const PENDING_CRASH_REPORT_DAYS = 28;
const DAY = 24 * 60 * 60 * 1000; // milliseconds
const DAYS_TO_SUPPRESS = 30;
const MAX_UNSEEN_CRASHED_CHILD_IDS = 20;
this.TabCrashHandler = {
_crashedTabCount: 0,
childMap: new Map(),
browserMap: new WeakMap(),
unseenCrashedChildIDs: [],
crashedBrowserQueues: new Map(),
get prefs() {
delete this.prefs;
return this.prefs = Services.prefs.getBranch("browser.tabs.crashReporting.");
},
init: function () {
if (this.initialized)
return;
this.initialized = true;
Services.obs.addObserver(this, "ipc:content-shutdown", false);
Services.obs.addObserver(this, "oop-frameloader-crashed", false);
this.pageListener = new RemotePages("about:tabcrashed");
// LOAD_BACKGROUND pages don't fire load events, so the about:tabcrashed
// content will fire up its own message when its initial scripts have
// finished running.
this.pageListener.addMessageListener("Load", this.receiveMessage.bind(this));
this.pageListener.addMessageListener("RemotePage:Unload", this.receiveMessage.bind(this));
this.pageListener.addMessageListener("closeTab", this.receiveMessage.bind(this));
this.pageListener.addMessageListener("restoreTab", this.receiveMessage.bind(this));
this.pageListener.addMessageListener("restoreAll", this.receiveMessage.bind(this));
},
observe: function (aSubject, aTopic, aData) {
switch (aTopic) {
case "ipc:content-shutdown": {
aSubject.QueryInterface(Ci.nsIPropertyBag2);
if (!aSubject.get("abnormal")) {
return;
}
let childID = aSubject.get("childID");
let dumpID = aSubject.get("dumpID");
if (!dumpID) {
Services.telemetry
.getHistogramById("FX_CONTENT_CRASH_DUMP_UNAVAILABLE")
.add(1);
}
if (!this.flushCrashedBrowserQueue(childID)) {
this.unseenCrashedChildIDs.push(childID);
// The elements in unseenCrashedChildIDs will only be removed if
// the tab crash page is shown. However, ipc:content-shutdown might
// be fired for processes for which we'll never show the tab crash
// page - for example, the thumbnailing process. Another case to
// consider is if the user is configured to submit backlogged crash
// reports automatically, and a background tab crashes. In that case,
// we will never show the tab crash page, and never remove the element
// from the list.
//
// Instead of trying to account for all of those cases, we prevent
// this list from getting too large by putting a reasonable upper
// limit on how many childIDs we track. It's unlikely that this
// array would ever get so large as to be unwieldy (that'd be a lot
// or crashes!), but a leak is a leak.
if (this.unseenCrashedChildIDs.length > MAX_UNSEEN_CRASHED_CHILD_IDS) {
this.unseenCrashedChildIDs.shift();
}
}
break;
}
case "oop-frameloader-crashed": {
aSubject.QueryInterface(Ci.nsIFrameLoader);
let browser = aSubject.ownerElement;
if (!browser) {
return;
}
this.browserMap.set(browser.permanentKey, aSubject.childID);
break;
}
}
},
receiveMessage: function(message) {
let browser = message.target.browser;
let gBrowser = browser.ownerGlobal.gBrowser;
let tab = gBrowser.getTabForBrowser(browser);
switch (message.name) {
case "Load": {
this.onAboutTabCrashedLoad(message);
break;
}
case "RemotePage:Unload": {
this.onAboutTabCrashedUnload(message);
break;
}
case "closeTab": {
this.maybeSendCrashReport(message);
gBrowser.removeTab(tab, { animate: true });
break;
}
case "restoreTab": {
this.maybeSendCrashReport(message);
SessionStore.reviveCrashedTab(tab);
break;
}
case "restoreAll": {
this.maybeSendCrashReport(message);
SessionStore.reviveAllCrashedTabs();
break;
}
}
},
/**
* This should be called once a content process has finished
* shutting down abnormally. Any tabbrowser browsers that were
* selected at the time of the crash will then be sent to
* the crashed tab page.
*
* @param childID (int)
* The childID of the content process that just crashed.
* @returns boolean
* True if one or more browsers were sent to the tab crashed
* page.
*/
flushCrashedBrowserQueue(childID) {
let browserQueue = this.crashedBrowserQueues.get(childID);
if (!browserQueue) {
return false;
}
this.crashedBrowserQueues.delete(childID);
let sentBrowser = false;
for (let weakBrowser of browserQueue) {
let browser = weakBrowser.get();
if (browser) {
this.sendToTabCrashedPage(browser);
sentBrowser = true;
}
}
return sentBrowser;
},
/**
* Called by a tabbrowser when it notices that its selected browser
* has crashed. This will queue the browser to show the tab crash
* page once the content process has finished tearing down.
*
* @param browser (<xul:browser>)
* The selected browser that just crashed.
*/
onSelectedBrowserCrash(browser) {
if (!browser.isRemoteBrowser) {
Cu.reportError("Selected crashed browser is not remote.")
return;
}
if (!browser.frameLoader) {
Cu.reportError("Selected crashed browser has no frameloader.");
return;
}
let childID = browser.frameLoader.childID;
let browserQueue = this.crashedBrowserQueues.get(childID);
if (!browserQueue) {
browserQueue = [];
this.crashedBrowserQueues.set(childID, browserQueue);
}
// It's probably unnecessary to store this browser as a
// weak reference, since the content process should complete
// its teardown in the same tick of the event loop, and then
// this queue will be flushed. The weak reference is to avoid
// leaking browsers in case anything goes wrong during this
// teardown process.
browserQueue.push(Cu.getWeakReference(browser));
},
/**
* This method is exposed for SessionStore to call if the user selects
* a tab which will restore on demand. It's possible that the tab
* is in this state because it recently crashed. If that's the case, then
* it's also possible that the user has not seen the tab crash page for
* that particular crash, in which case, we might show it to them instead
* of restoring the tab.
*
* @param browser (<xul:browser>)
* A browser from a browser tab that the user has just selected
* to restore on demand.
* @returns (boolean)
* True if TabCrashHandler will send the user to the tab crash
* page instead.
*/
willShowCrashedTab(browser) {
let childID = this.browserMap.get(browser.permanentKey);
// We will only show the tab crash page if:
// 1) We are aware that this browser crashed
// 2) We know we've never shown the tab crash page for the
// crash yet
// 3) The user is not configured to automatically submit backlogged
// crash reports. If they are, we'll send the crash report
// immediately.
if (childID &&
this.unseenCrashedChildIDs.indexOf(childID) != -1) {
if (UnsubmittedCrashHandler.autoSubmit) {
let dumpID = this.childMap.get(childID);
if (dumpID) {
UnsubmittedCrashHandler.submitReports([dumpID]);
}
} else {
this.sendToTabCrashedPage(browser);
return true;
}
}
return false;
},
/**
* We show a special page to users when a normal browser tab has crashed.
* This method should be called to send a browser to that page once the
* process has completely closed.
*
* @param browser (<xul:browser>)
* The browser that has recently crashed.
*/
sendToTabCrashedPage(browser) {
let title = browser.contentTitle;
let uri = browser.currentURI;
let gBrowser = browser.ownerGlobal.gBrowser;
let tab = gBrowser.getTabForBrowser(browser);
// The tab crashed page is non-remote by default.
gBrowser.updateBrowserRemoteness(browser, false);
browser.setAttribute("crashedPageTitle", title);
browser.docShell.displayLoadError(Cr.NS_ERROR_CONTENT_CRASHED, uri, null);
browser.removeAttribute("crashedPageTitle");
tab.setAttribute("crashed", true);
},
/**
* Submits a crash report from about:tabcrashed, if the crash
* reporter is enabled and a crash report can be found.
*/
maybeSendCrashReport(message) {
/*** STUB ***/
return;
},
removeSubmitCheckboxesForSameCrash: function(childID) {
let enumerator = Services.wm.getEnumerator("navigator:browser");
while (enumerator.hasMoreElements()) {
let window = enumerator.getNext();
if (!window.gMultiProcessBrowser)
continue;
for (let browser of window.gBrowser.browsers) {
if (browser.isRemoteBrowser)
continue;
let doc = browser.contentDocument;
if (!doc.documentURI.startsWith("about:tabcrashed"))
continue;
if (this.browserMap.get(browser.permanentKey) == childID) {
this.browserMap.delete(browser.permanentKey);
let ports = this.pageListener.portsForBrowser(browser);
if (ports.length) {
// For about:tabcrashed, we don't expect subframes. We can
// assume sending to the first port is sufficient.
ports[0].sendAsyncMessage("CrashReportSent");
}
}
}
}
},
onAboutTabCrashedLoad: function (message) {
this._crashedTabCount++;
// Broadcast to all about:tabcrashed pages a count of
// how many about:tabcrashed pages exist, so that they
// can decide whether or not to display the "Restore All
// Crashed Tabs" button.
this.pageListener.sendAsyncMessage("UpdateCount", {
count: this._crashedTabCount,
});
let browser = message.target.browser;
let childID = this.browserMap.get(browser.permanentKey);
let index = this.unseenCrashedChildIDs.indexOf(childID);
if (index != -1) {
this.unseenCrashedChildIDs.splice(index, 1);
}
let dumpID = this.getDumpID(browser);
if (!dumpID) {
message.target.sendAsyncMessage("SetCrashReportAvailable", {
hasReport: false,
});
return;
}
let requestAutoSubmit = !UnsubmittedCrashHandler.autoSubmit;
let requestEmail = this.prefs.getBoolPref("requestEmail");
let sendReport = this.prefs.getBoolPref("sendReport");
let includeURL = this.prefs.getBoolPref("includeURL");
let emailMe = this.prefs.getBoolPref("emailMe");
let data = {
hasReport: true,
sendReport,
includeURL,
emailMe,
requestAutoSubmit,
requestEmail,
};
if (emailMe) {
data.email = this.prefs.getCharPref("email", "");
}
// Make sure to only count once even if there are multiple windows
// that will all show about:tabcrashed.
if (this._crashedTabCount == 1) {
Services.telemetry.getHistogramById("FX_CONTENT_CRASH_PRESENTED").add(1);
}
message.target.sendAsyncMessage("SetCrashReportAvailable", data);
},
onAboutTabCrashedUnload(message) {
if (!this._crashedTabCount) {
Cu.reportError("Can not decrement crashed tab count to below 0");
return;
}
this._crashedTabCount--;
// Broadcast to all about:tabcrashed pages a count of
// how many about:tabcrashed pages exist, so that they
// can decide whether or not to display the "Restore All
// Crashed Tabs" button.
this.pageListener.sendAsyncMessage("UpdateCount", {
count: this._crashedTabCount,
});
let browser = message.target.browser;
let childID = this.browserMap.get(browser.permanentKey);
// Make sure to only count once even if there are multiple windows
// that will all show about:tabcrashed.
if (this._crashedTabCount == 0 && childID) {
Services.telemetry.getHistogramById("FX_CONTENT_CRASH_NOT_SUBMITTED").add(1);
}
},
/**
* For some <xul:browser>, return a crash report dump ID for that browser
* if we have been informed of one. Otherwise, return null.
*/
getDumpID(browser) {
/*** STUB ***/
return null;
},
}
/**
* This component is responsible for scanning the pending
* crash report directory for reports, and (if enabled), to
* prompt the user to submit those reports. It might also
* submit those reports automatically without prompting if
* the user has opted in.
*/
this.UnsubmittedCrashHandler = {
get prefs() {
delete this.prefs;
return this.prefs =
Services.prefs.getBranch("browser.crashReports.unsubmittedCheck.");
},
get enabled() {
return this.prefs.getBoolPref("enabled");
},
// showingNotification is set to true once a notification
// is successfully shown, and then set back to false if
// the notification is dismissed by an action by the user.
showingNotification: false,
// suppressed is true if we've determined that we've shown
// the notification too many times across too many days without
// user interaction, so we're suppressing the notification for
// some number of days. See the documentation for
// shouldShowPendingSubmissionsNotification().
suppressed: false,
init() {
if (this.initialized) {
return;
}
this.initialized = true;
// UnsubmittedCrashHandler can be initialized but still be disabled.
// This is intentional, as this makes simulating UnsubmittedCrashHandler's
// reactions to browser startup and shutdown easier in test automation.
//
// UnsubmittedCrashHandler, when initialized but not enabled, is inert.
if (this.enabled) {
if (this.prefs.prefHasUserValue("suppressUntilDate")) {
if (this.prefs.getCharPref("suppressUntilDate") > this.dateString()) {
// We'll be suppressing any notifications until after suppressedDate,
// so there's no need to do anything more.
this.suppressed = true;
return;
}
// We're done suppressing, so we don't need this pref anymore.
this.prefs.clearUserPref("suppressUntilDate");
}
Services.obs.addObserver(this, "browser-delayed-startup-finished",
false);
Services.obs.addObserver(this, "profile-before-change",
false);
}
},
uninit() {
if (!this.initialized) {
return;
}
this.initialized = false;
if (!this.enabled) {
return;
}
if (this.suppressed) {
this.suppressed = false;
// No need to do any more clean-up, since we were suppressed.
return;
}
if (this.showingNotification) {
this.prefs.setBoolPref("shutdownWhileShowing", true);
this.showingNotification = false;
}
try {
Services.obs.removeObserver(this, "browser-delayed-startup-finished");
} catch (e) {
// The browser-delayed-startup-finished observer might have already
// fired and removed itself, so if this fails, it's okay.
if (e.result != Cr.NS_ERROR_FAILURE) {
throw e;
}
}
Services.obs.removeObserver(this, "profile-before-change");
},
observe(subject, topic, data) {
switch (topic) {
case "browser-delayed-startup-finished": {
Services.obs.removeObserver(this, topic);
this.checkForUnsubmittedCrashReports();
break;
}
case "profile-before-change": {
this.uninit();
break;
}
}
},
/**
* Scans the profile directory for unsubmitted crash reports
* within the past PENDING_CRASH_REPORT_DAYS days. If it
* finds any, it will, if necessary, attempt to open a notification
* bar to prompt the user to submit them.
*
* @returns Promise
* Resolves with the <xul:notification> after it tries to
* show a notification on the most recent browser window.
* If a notification cannot be shown, will resolve with null.
*/
checkForUnsubmittedCrashReports: Task.async(function*() {
let dateLimit = new Date();
dateLimit.setDate(dateLimit.getDate() - PENDING_CRASH_REPORT_DAYS);
let reportIDs = [];
try {
reportIDs = yield CrashSubmit.pendingIDsAsync(dateLimit);
} catch (e) {
Cu.reportError(e);
return null;
}
if (reportIDs.length) {
if (this.autoSubmit) {
this.submitReports(reportIDs);
} else if (this.shouldShowPendingSubmissionsNotification()) {
return this.showPendingSubmissionsNotification(reportIDs);
}
}
return null;
}),
/**
* Returns true if the notification should be shown.
* shouldShowPendingSubmissionsNotification makes this decision
* by looking at whether or not the user has seen the notification
* over several days without ever interacting with it. If this occurs
* too many times, we suppress the notification for DAYS_TO_SUPPRESS
* days.
*
* @returns bool
*/
shouldShowPendingSubmissionsNotification() {
if (!this.prefs.prefHasUserValue("shutdownWhileShowing")) {
return true;
}
let shutdownWhileShowing = this.prefs.getBoolPref("shutdownWhileShowing");
this.prefs.clearUserPref("shutdownWhileShowing");
if (!this.prefs.prefHasUserValue("lastShownDate")) {
// This isn't expected, but we're being defensive here. We'll
// opt for showing the notification in this case.
return true;
}
let lastShownDate = this.prefs.getCharPref("lastShownDate");
if (this.dateString() > lastShownDate && shutdownWhileShowing) {
// We're on a newer day then when we last showed the
// notification without closing it. We don't want to do
// this too many times, so we'll decrement a counter for
// this situation. Too many of these, and we'll assume the
// user doesn't know or care about unsubmitted notifications,
// and we'll suppress the notification for a while.
let chances = this.prefs.getIntPref("chancesUntilSuppress");
if (--chances < 0) {
// We're out of chances!
this.prefs.clearUserPref("chancesUntilSuppress");
// We'll suppress for DAYS_TO_SUPPRESS days.
let suppressUntil =
this.dateString(new Date(Date.now() + (DAY * DAYS_TO_SUPPRESS)));
this.prefs.setCharPref("suppressUntilDate", suppressUntil);
return false;
}
this.prefs.setIntPref("chancesUntilSuppress", chances);
}
return true;
},
/**
* Given an array of unsubmitted crash report IDs, try to open
* up a notification asking the user to submit them.
*
* @param reportIDs (Array<string>)
* The Array of report IDs to offer the user to send.
* @returns The <xul:notification> if one is shown. null otherwise.
*/
showPendingSubmissionsNotification(reportIDs) {
let count = reportIDs.length;
if (!count) {
return null;
}
let messageTemplate =
gNavigatorBundle.GetStringFromName("pendingCrashReports2.label");
let message = PluralForm.get(count, messageTemplate).replace("#1", count);
let notification = this.show({
notificationID: "pending-crash-reports",
message,
reportIDs,
onAction: () => {
this.showingNotification = false;
},
});
if (notification) {
this.showingNotification = true;
this.prefs.setCharPref("lastShownDate", this.dateString());
}
return notification;
},
/**
* Returns a string representation of a Date in the format
* YYYYMMDD.
*
* @param someDate (Date, optional)
* The Date to convert to the string. If not provided,
* defaults to today's date.
* @returns String
*/
dateString(someDate = new Date()) {
let year = String(someDate.getFullYear()).padStart(4, "0");
let month = String(someDate.getMonth() + 1).padStart(2, "0");
let day = String(someDate.getDate()).padStart(2, "0");
return year + month + day;
},
/**
* Attempts to show a notification bar to the user in the most
* recent browser window asking them to submit some crash report
* IDs. If a notification cannot be shown (for example, there
* is no browser window), this method exits silently.
*
* The notification will allow the user to submit their crash
* reports. If the user dismissed the notification, the crash
* reports will be marked to be ignored (though they can
* still be manually submitted via about:crashes).
*
* @param JS Object
* An Object with the following properties:
*
* notificationID (string)
* The ID for the notification to be opened.
*
* message (string)
* The message to be displayed in the notification.
*
* reportIDs (Array<string>)
* The array of report IDs to offer to the user.
*
* onAction (function, optional)
* A callback to fire once the user performs an
* action on the notification bar (this includes
* dismissing the notification).
*
* @returns The <xul:notification> if one is shown. null otherwise.
*/
show({ notificationID, message, reportIDs, onAction }) {
let chromeWin = RecentWindow.getMostRecentBrowserWindow();
if (!chromeWin) {
// Can't show a notification in this case. We'll hopefully
// get another opportunity to have the user submit their
// crash reports later.
return null;
}
let nb = chromeWin.document.getElementById("global-notificationbox");
let notification = nb.getNotificationWithValue(notificationID);
if (notification) {
return null;
}
let buttons = [{
label: gNavigatorBundle.GetStringFromName("pendingCrashReports.send"),
callback: () => {
this.submitReports(reportIDs);
if (onAction) {
onAction();
}
},
},
{
label: gNavigatorBundle.GetStringFromName("pendingCrashReports.alwaysSend"),
callback: () => {
this.autoSubmit = true;
this.submitReports(reportIDs);
if (onAction) {
onAction();
}
},
},
{
label: gNavigatorBundle.GetStringFromName("pendingCrashReports.viewAll"),
callback: function() {
chromeWin.openUILinkIn("about:crashes", "tab");
return true;
},
}];
let eventCallback = (eventType) => {
if (eventType == "dismissed") {
// The user intentionally dismissed the notification,
// which we interpret as meaning that they don't care
// to submit the reports. We'll ignore these particular
// reports going forward.
reportIDs.forEach(function(reportID) {
CrashSubmit.ignore(reportID);
});
if (onAction) {
onAction();
}
}
};
return nb.appendNotification(message, notificationID,
"chrome://browser/skin/tab-crashed.svg",
nb.PRIORITY_INFO_HIGH, buttons,
eventCallback);
},
get autoSubmit() {
return Services.prefs
.getBoolPref("browser.crashReports.unsubmittedCheck.autoSubmit2");
},
set autoSubmit(val) {
Services.prefs.setBoolPref("browser.crashReports.unsubmittedCheck.autoSubmit2",
val);
},
/**
* Attempt to submit reports to the crash report server. Each
* report will have the "SubmittedFromInfobar" extra key set
* to true.
*
* @param reportIDs (Array<string>)
* The array of reportIDs to submit.
*/
submitReports(reportIDs) {
for (let reportID of reportIDs) {
CrashSubmit.submit(reportID, {
extraExtraKeyVals: {
"SubmittedFromInfobar": true,
},
});
}
},
};
this.PluginCrashReporter = {
/**
* Makes the PluginCrashReporter ready to hear about and
* submit crash reports.
*/
init() {
if (this.initialized) {
return;
}
this.initialized = true;
this.crashReports = new Map();
Services.obs.addObserver(this, "plugin-crashed", false);
Services.obs.addObserver(this, "gmp-plugin-crash", false);
Services.obs.addObserver(this, "profile-after-change", false);
},
uninit() {
Services.obs.removeObserver(this, "plugin-crashed", false);
Services.obs.removeObserver(this, "gmp-plugin-crash", false);
Services.obs.removeObserver(this, "profile-after-change", false);
this.initialized = false;
},
observe(subject, topic, data) {
switch (topic) {
case "plugin-crashed": {
let propertyBag = subject;
if (!(propertyBag instanceof Ci.nsIPropertyBag2) ||
!(propertyBag instanceof Ci.nsIWritablePropertyBag2) ||
!propertyBag.hasKey("runID") ||
!propertyBag.hasKey("pluginDumpID")) {
Cu.reportError("PluginCrashReporter can not read plugin information.");
return;
}
let runID = propertyBag.getPropertyAsUint32("runID");
let pluginDumpID = propertyBag.getPropertyAsAString("pluginDumpID");
let browserDumpID = propertyBag.getPropertyAsAString("browserDumpID");
if (pluginDumpID) {
this.crashReports.set(runID, { pluginDumpID, browserDumpID });
}
break;
}
case "gmp-plugin-crash": {
let propertyBag = subject;
if (!(propertyBag instanceof Ci.nsIWritablePropertyBag2) ||
!propertyBag.hasKey("pluginID") ||
!propertyBag.hasKey("pluginDumpID") ||
!propertyBag.hasKey("pluginName")) {
Cu.reportError("PluginCrashReporter can not read plugin information.");
return;
}
let pluginID = propertyBag.getPropertyAsUint32("pluginID");
let pluginDumpID = propertyBag.getPropertyAsAString("pluginDumpID");
if (pluginDumpID) {
this.crashReports.set(pluginID, { pluginDumpID });
}
// Only the parent process gets the gmp-plugin-crash observer
// notification, so we need to inform any content processes that
// the GMP has crashed.
if (Cc["@mozilla.org/parentprocessmessagemanager;1"]) {
let pluginName = propertyBag.getPropertyAsAString("pluginName");
let mm = Cc["@mozilla.org/parentprocessmessagemanager;1"]
.getService(Ci.nsIMessageListenerManager);
mm.broadcastAsyncMessage("gmp-plugin-crash",
{ pluginName, pluginID });
}
break;
}
case "profile-after-change":
this.uninit();
break;
}
},
/**
* Submit a crash report for a crashed NPAPI plugin.
*
* @param runID
* The runID of the plugin that crashed. A run ID is a unique
* identifier for a particular run of a plugin process - and is
* analogous to a process ID (though it is managed by Gecko instead
* of the operating system).
* @param keyVals
* An object whose key-value pairs will be merged
* with the ".extra" file submitted with the report.
* The properties of htis object will override properties
* of the same name in the .extra file.
*/
submitCrashReport(runID, keyVals) {
if (!this.crashReports.has(runID)) {
Cu.reportError(`Could not find plugin dump IDs for run ID ${runID}.` +
`It is possible that a report was already submitted.`);
return;
}
keyVals = keyVals || {};
let { pluginDumpID, browserDumpID } = this.crashReports.get(runID);
let submissionPromise = CrashSubmit.submit(pluginDumpID, {
recordSubmission: true,
extraExtraKeyVals: keyVals,
});
if (browserDumpID)
CrashSubmit.submit(browserDumpID);
this.broadcastState(runID, "submitting");
submissionPromise.then(() => {
this.broadcastState(runID, "success");
}, () => {
this.broadcastState(runID, "failed");
});
this.crashReports.delete(runID);
},
broadcastState(runID, state) {
let enumerator = Services.wm.getEnumerator("navigator:browser");
while (enumerator.hasMoreElements()) {
let window = enumerator.getNext();
let mm = window.messageManager;
mm.broadcastAsyncMessage("BrowserPlugins:CrashReportSubmitted",
{ runID, state });
}
},
hasCrashReport(runID) {
return this.crashReports.has(runID);
},
};

View file

@ -0,0 +1,147 @@
/* 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";
var Cc = Components.classes;
var Ci = Components.interfaces;
var Cu = Components.utils;
this.EXPORTED_SYMBOLS = [ "ContentLinkHandler" ];
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/NetUtil.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Feeds",
"resource:///modules/Feeds.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "BrowserUtils",
"resource://gre/modules/BrowserUtils.jsm");
const SIZES_TELEMETRY_ENUM = {
NO_SIZES: 0,
ANY: 1,
DIMENSION: 2,
INVALID: 3,
};
this.ContentLinkHandler = {
init: function(chromeGlobal) {
chromeGlobal.addEventListener("DOMLinkAdded", (event) => {
this.onLinkEvent(event, chromeGlobal);
}, false);
chromeGlobal.addEventListener("DOMLinkChanged", (event) => {
this.onLinkEvent(event, chromeGlobal);
}, false);
},
onLinkEvent: function(event, chromeGlobal) {
var link = event.originalTarget;
var rel = link.rel && link.rel.toLowerCase();
if (!link || !link.ownerDocument || !rel || !link.href)
return;
// Ignore sub-frames (bugs 305472, 479408).
let window = link.ownerGlobal;
if (window != window.top)
return;
var feedAdded = false;
var iconAdded = false;
var searchAdded = false;
var rels = {};
for (let relString of rel.split(/\s+/))
rels[relString] = true;
for (let relVal in rels) {
switch (relVal) {
case "feed":
case "alternate":
if (!feedAdded && event.type == "DOMLinkAdded") {
if (!rels.feed && rels.alternate && rels.stylesheet)
break;
if (Feeds.isValidFeed(link, link.ownerDocument.nodePrincipal, "feed" in rels)) {
chromeGlobal.sendAsyncMessage("Link:AddFeed",
{type: link.type,
href: link.href,
title: link.title});
feedAdded = true;
}
}
break;
case "icon":
if (iconAdded || !Services.prefs.getBoolPref("browser.chrome.site_icons"))
break;
var uri = this.getLinkIconURI(link);
if (!uri)
break;
// Telemetry probes for measuring the sizes attribute
// usage and available dimensions.
let sizeHistogramTypes = Services.telemetry.
getHistogramById("LINK_ICON_SIZES_ATTR_USAGE");
let sizeHistogramDimension = Services.telemetry.
getHistogramById("LINK_ICON_SIZES_ATTR_DIMENSION");
let sizesType;
if (link.sizes.length) {
for (let size of link.sizes) {
if (size.toLowerCase() == "any") {
sizesType = SIZES_TELEMETRY_ENUM.ANY;
break;
} else {
let re = /^([1-9][0-9]*)x[1-9][0-9]*$/i;
let values = re.exec(size);
if (values && values.length > 1) {
sizesType = SIZES_TELEMETRY_ENUM.DIMENSION;
sizeHistogramDimension.add(parseInt(values[1]));
} else {
sizesType = SIZES_TELEMETRY_ENUM.INVALID;
break;
}
}
}
} else {
sizesType = SIZES_TELEMETRY_ENUM.NO_SIZES;
}
sizeHistogramTypes.add(sizesType);
chromeGlobal.sendAsyncMessage(
"Link:SetIcon",
{url: uri.spec, loadingPrincipal: link.ownerDocument.nodePrincipal});
iconAdded = true;
break;
case "search":
if (!searchAdded && event.type == "DOMLinkAdded") {
var type = link.type && link.type.toLowerCase();
type = type.replace(/^\s+|\s*(?:;.*)?$/g, "");
let re = /^(?:https?|ftp):/i;
if (type == "application/opensearchdescription+xml" && link.title &&
re.test(link.href))
{
let engine = { title: link.title, href: link.href };
chromeGlobal.sendAsyncMessage("Link:AddSearch",
{engine: engine,
url: link.ownerDocument.documentURI});
searchAdded = true;
}
}
break;
}
}
},
getLinkIconURI: function(aLink) {
let targetDoc = aLink.ownerDocument;
var uri = BrowserUtils.makeURI(aLink.href, targetDoc.characterSet);
try {
uri.userPass = "";
} catch (e) {
// some URIs are immutable
}
return uri;
},
};

View file

@ -0,0 +1,55 @@
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
/**
* This module is for small observers that we want to register once per content
* process, usually in order to forward content-based observer service notifications
* to the chrome process through message passing. Using a JSM avoids having them
* in content.js and thereby registering N observers for N open tabs, which is bad
* for perf.
*/
"use strict";
this.EXPORTED_SYMBOLS = [];
const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
Cu.import("resource://gre/modules/Services.jsm");
var gEMEUIObserver = function(subject, topic, data) {
let win = subject.top;
let mm = getMessageManagerForWindow(win);
if (mm) {
mm.sendAsyncMessage("EMEVideo:ContentMediaKeysRequest", data);
}
};
var gDecoderDoctorObserver = function(subject, topic, data) {
let win = subject.top;
let mm = getMessageManagerForWindow(win);
if (mm) {
mm.sendAsyncMessage("DecoderDoctor:Notification", data);
}
};
function getMessageManagerForWindow(aContentWindow) {
let ir = aContentWindow.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIDocShell)
.sameTypeRootTreeItem
.QueryInterface(Ci.nsIInterfaceRequestor);
try {
// If e10s is disabled, this throws NS_NOINTERFACE for closed tabs.
return ir.getInterface(Ci.nsIContentFrameMessageManager);
} catch (e) {
if (e.result == Cr.NS_NOINTERFACE) {
return null;
}
throw e;
}
}
Services.obs.addObserver(gEMEUIObserver, "mediakeys-request", false);
Services.obs.addObserver(gDecoderDoctorObserver, "decoder-doctor-notification", false);

View file

@ -0,0 +1,566 @@
/* 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/. */
/* globals XPCOMUtils, Services, Task, Promise, SearchSuggestionController, FormHistory, PrivateBrowsingUtils */
"use strict";
this.EXPORTED_SYMBOLS = [
"ContentSearch",
];
const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/Promise.jsm");
Cu.import("resource://gre/modules/Task.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "FormHistory",
"resource://gre/modules/FormHistory.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils",
"resource://gre/modules/PrivateBrowsingUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "SearchSuggestionController",
"resource://gre/modules/SearchSuggestionController.jsm");
const INBOUND_MESSAGE = "ContentSearch";
const OUTBOUND_MESSAGE = INBOUND_MESSAGE;
const MAX_LOCAL_SUGGESTIONS = 3;
const MAX_SUGGESTIONS = 6;
/**
* ContentSearch receives messages named INBOUND_MESSAGE and sends messages
* named OUTBOUND_MESSAGE. The data of each message is expected to look like
* { type, data }. type is the message's type (or subtype if you consider the
* type of the message itself to be INBOUND_MESSAGE), and data is data that is
* specific to the type.
*
* Inbound messages have the following types:
*
* AddFormHistoryEntry
* Adds an entry to the search form history.
* data: the entry, a string
* GetSuggestions
* Retrieves an array of search suggestions given a search string.
* data: { engineName, searchString, [remoteTimeout] }
* GetState
* Retrieves the current search engine state.
* data: null
* GetStrings
* Retrieves localized search UI strings.
* data: null
* ManageEngines
* Opens the search engine management window.
* data: null
* RemoveFormHistoryEntry
* Removes an entry from the search form history.
* data: the entry, a string
* Search
* Performs a search.
* Any GetSuggestions messages in the queue from the same target will be
* cancelled.
* data: { engineName, searchString, healthReportKey, searchPurpose }
* SetCurrentEngine
* Sets the current engine.
* data: the name of the engine
* SpeculativeConnect
* Speculatively connects to an engine.
* data: the name of the engine
*
* Outbound messages have the following types:
*
* CurrentEngine
* Broadcast when the current engine changes.
* data: see _currentEngineObj
* CurrentState
* Broadcast when the current search state changes.
* data: see currentStateObj
* State
* Sent in reply to GetState.
* data: see currentStateObj
* Strings
* Sent in reply to GetStrings
* data: Object containing string names and values for the current locale.
* Suggestions
* Sent in reply to GetSuggestions.
* data: see _onMessageGetSuggestions
* SuggestionsCancelled
* Sent in reply to GetSuggestions when pending GetSuggestions events are
* cancelled.
* data: null
*/
this.ContentSearch = {
// Inbound events are queued and processed in FIFO order instead of handling
// them immediately, which would result in non-FIFO responses due to the
// asynchrononicity added by converting image data URIs to ArrayBuffers.
_eventQueue: [],
_currentEventPromise: null,
// This is used to handle search suggestions. It maps xul:browsers to objects
// { controller, previousFormHistoryResult }. See _onMessageGetSuggestions.
_suggestionMap: new WeakMap(),
// Resolved when we finish shutting down.
_destroyedPromise: null,
// The current controller and browser in _onMessageGetSuggestions. Allows
// fetch cancellation from _cancelSuggestions.
_currentSuggestion: null,
init: function () {
Cc["@mozilla.org/globalmessagemanager;1"].
getService(Ci.nsIMessageListenerManager).
addMessageListener(INBOUND_MESSAGE, this);
Services.obs.addObserver(this, "browser-search-engine-modified", false);
Services.obs.addObserver(this, "shutdown-leaks-before-check", false);
Services.prefs.addObserver("browser.search.hiddenOneOffs", this, false);
this._stringBundle = Services.strings.createBundle("chrome://global/locale/autocomplete.properties");
},
get searchSuggestionUIStrings() {
if (this._searchSuggestionUIStrings) {
return this._searchSuggestionUIStrings;
}
this._searchSuggestionUIStrings = {};
let searchBundle = Services.strings.createBundle("chrome://browser/locale/search.properties");
let stringNames = ["searchHeader", "searchPlaceholder", "searchForSomethingWith",
"searchWithHeader", "searchSettings"];
for (let name of stringNames) {
this._searchSuggestionUIStrings[name] = searchBundle.GetStringFromName(name);
}
return this._searchSuggestionUIStrings;
},
destroy: function () {
if (this._destroyedPromise) {
return this._destroyedPromise;
}
Cc["@mozilla.org/globalmessagemanager;1"].
getService(Ci.nsIMessageListenerManager).
removeMessageListener(INBOUND_MESSAGE, this);
Services.obs.removeObserver(this, "browser-search-engine-modified");
Services.obs.removeObserver(this, "shutdown-leaks-before-check");
this._eventQueue.length = 0;
this._destroyedPromise = Promise.resolve(this._currentEventPromise);
return this._destroyedPromise;
},
/**
* Focuses the search input in the page with the given message manager.
* @param messageManager
* The MessageManager object of the selected browser.
*/
focusInput: function (messageManager) {
messageManager.sendAsyncMessage(OUTBOUND_MESSAGE, {
type: "FocusInput"
});
},
receiveMessage: function (msg) {
// Add a temporary event handler that exists only while the message is in
// the event queue. If the message's source docshell changes browsers in
// the meantime, then we need to update msg.target. event.detail will be
// the docshell's new parent <xul:browser> element.
msg.handleEvent = event => {
let browserData = this._suggestionMap.get(msg.target);
if (browserData) {
this._suggestionMap.delete(msg.target);
this._suggestionMap.set(event.detail, browserData);
}
msg.target.removeEventListener("SwapDocShells", msg, true);
msg.target = event.detail;
msg.target.addEventListener("SwapDocShells", msg, true);
};
msg.target.addEventListener("SwapDocShells", msg, true);
// Search requests cause cancellation of all Suggestion requests from the
// same browser.
if (msg.data.type === "Search") {
this._cancelSuggestions(msg);
}
this._eventQueue.push({
type: "Message",
data: msg,
});
this._processEventQueue();
},
observe: function (subj, topic, data) {
switch (topic) {
case "nsPref:changed":
case "browser-search-engine-modified":
this._eventQueue.push({
type: "Observe",
data: data,
});
this._processEventQueue();
break;
case "shutdown-leaks-before-check":
subj.wrappedJSObject.client.addBlocker(
"ContentSearch: Wait until the service is destroyed", () => this.destroy());
break;
}
},
removeFormHistoryEntry: function (msg, entry) {
let browserData = this._suggestionDataForBrowser(msg.target);
if (browserData && browserData.previousFormHistoryResult) {
let { previousFormHistoryResult } = browserData;
for (let i = 0; i < previousFormHistoryResult.matchCount; i++) {
if (previousFormHistoryResult.getValueAt(i) === entry) {
previousFormHistoryResult.removeValueAt(i, true);
break;
}
}
}
},
performSearch: function (msg, data) {
this._ensureDataHasProperties(data, [
"engineName",
"searchString",
"healthReportKey",
"searchPurpose",
]);
let engine = Services.search.getEngineByName(data.engineName);
let submission = engine.getSubmission(data.searchString, "", data.searchPurpose);
let browser = msg.target;
let win = browser.ownerGlobal;
if (!win) {
// The browser may have been closed between the time its content sent the
// message and the time we handle it.
return;
}
let where = win.whereToOpenLink(data.originalEvent);
// There is a chance that by the time we receive the search message, the user
// has switched away from the tab that triggered the search. If, based on the
// event, we need to load the search in the same tab that triggered it (i.e.
// where === "current"), openUILinkIn will not work because that tab is no
// longer the current one. For this case we manually load the URI.
if (where === "current") {
browser.loadURIWithFlags(submission.uri.spec,
Ci.nsIWebNavigation.LOAD_FLAGS_NONE, null, null,
submission.postData);
} else {
let params = {
postData: submission.postData,
inBackground: Services.prefs.getBoolPref("browser.tabs.loadInBackground"),
};
win.openUILinkIn(submission.uri.spec, where, params);
}
win.BrowserSearch.recordSearchInTelemetry(engine, data.healthReportKey,
{ selection: data.selection });
return;
},
getSuggestions: Task.async(function* (engineName, searchString, browser, remoteTimeout=null) {
let engine = Services.search.getEngineByName(engineName);
if (!engine) {
throw new Error("Unknown engine name: " + engineName);
}
let browserData = this._suggestionDataForBrowser(browser, true);
let { controller } = browserData;
let ok = SearchSuggestionController.engineOffersSuggestions(engine);
controller.maxLocalResults = ok ? MAX_LOCAL_SUGGESTIONS : MAX_SUGGESTIONS;
controller.maxRemoteResults = ok ? MAX_SUGGESTIONS : 0;
controller.remoteTimeout = remoteTimeout || undefined;
let priv = PrivateBrowsingUtils.isBrowserPrivate(browser);
// fetch() rejects its promise if there's a pending request, but since we
// process our event queue serially, there's never a pending request.
this._currentSuggestion = { controller: controller, target: browser };
let suggestions = yield controller.fetch(searchString, priv, engine);
this._currentSuggestion = null;
// suggestions will be null if the request was cancelled
let result = {};
if (!suggestions) {
return result;
}
// Keep the form history result so RemoveFormHistoryEntry can remove entries
// from it. Keeping only one result isn't foolproof because the client may
// try to remove an entry from one set of suggestions after it has requested
// more but before it's received them. In that case, the entry may not
// appear in the new suggestions. But that should happen rarely.
browserData.previousFormHistoryResult = suggestions.formHistoryResult;
result = {
engineName,
term: suggestions.term,
local: suggestions.local,
remote: suggestions.remote,
};
return result;
}),
addFormHistoryEntry: Task.async(function* (browser, entry="") {
let isPrivate = false;
try {
// isBrowserPrivate assumes that the passed-in browser has all the normal
// properties, which won't be true if the browser has been destroyed.
// That may be the case here due to the asynchronous nature of messaging.
isPrivate = PrivateBrowsingUtils.isBrowserPrivate(browser.target);
} catch (err) {
return false;
}
if (isPrivate || entry === "") {
return false;
}
let browserData = this._suggestionDataForBrowser(browser.target, true);
FormHistory.update({
op: "bump",
fieldname: browserData.controller.formHistoryParam,
value: entry,
}, {
handleCompletion: () => {},
handleError: err => {
Cu.reportError("Error adding form history entry: " + err);
},
});
return true;
}),
currentStateObj: Task.async(function* (uriFlag=false) {
let state = {
engines: [],
currentEngine: yield this._currentEngineObj(),
};
if (uriFlag) {
state.currentEngine.iconBuffer = Services.search.currentEngine.getIconURLBySize(16, 16);
}
let pref = Services.prefs.getCharPref("browser.search.hiddenOneOffs");
let hiddenList = pref ? pref.split(",") : [];
for (let engine of Services.search.getVisibleEngines()) {
let uri = engine.getIconURLBySize(16, 16);
let iconBuffer = uri;
if (!uriFlag) {
iconBuffer = yield this._arrayBufferFromDataURI(uri);
}
state.engines.push({
name: engine.name,
iconBuffer,
hidden: hiddenList.indexOf(engine.name) !== -1,
});
}
return state;
}),
_processEventQueue: function () {
if (this._currentEventPromise || !this._eventQueue.length) {
return;
}
let event = this._eventQueue.shift();
this._currentEventPromise = Task.spawn(function* () {
try {
yield this["_on" + event.type](event.data);
} catch (err) {
Cu.reportError(err);
} finally {
this._currentEventPromise = null;
this._processEventQueue();
}
}.bind(this));
},
_cancelSuggestions: function (msg) {
let cancelled = false;
// cancel active suggestion request
if (this._currentSuggestion && this._currentSuggestion.target === msg.target) {
this._currentSuggestion.controller.stop();
cancelled = true;
}
// cancel queued suggestion requests
for (let i = 0; i < this._eventQueue.length; i++) {
let m = this._eventQueue[i].data;
if (msg.target === m.target && m.data.type === "GetSuggestions") {
this._eventQueue.splice(i, 1);
cancelled = true;
i--;
}
}
if (cancelled) {
this._reply(msg, "SuggestionsCancelled");
}
},
_onMessage: Task.async(function* (msg) {
let methodName = "_onMessage" + msg.data.type;
if (methodName in this) {
yield this._initService();
yield this[methodName](msg, msg.data.data);
if (!Cu.isDeadWrapper(msg.target)) {
msg.target.removeEventListener("SwapDocShells", msg, true);
}
}
}),
_onMessageGetState: function (msg, data) {
return this.currentStateObj().then(state => {
this._reply(msg, "State", state);
});
},
_onMessageGetStrings: function (msg, data) {
this._reply(msg, "Strings", this.searchSuggestionUIStrings);
},
_onMessageSearch: function (msg, data) {
this.performSearch(msg, data);
},
_onMessageSetCurrentEngine: function (msg, data) {
Services.search.currentEngine = Services.search.getEngineByName(data);
},
_onMessageManageEngines: function (msg, data) {
let browserWin = msg.target.ownerGlobal;
browserWin.openPreferences("paneSearch");
},
_onMessageGetSuggestions: Task.async(function* (msg, data) {
this._ensureDataHasProperties(data, [
"engineName",
"searchString",
]);
let {engineName, searchString} = data;
let suggestions = yield this.getSuggestions(engineName, searchString, msg.target);
this._reply(msg, "Suggestions", {
engineName: data.engineName,
searchString: suggestions.term,
formHistory: suggestions.local,
remote: suggestions.remote,
});
}),
_onMessageAddFormHistoryEntry: Task.async(function* (msg, entry) {
yield this.addFormHistoryEntry(msg, entry);
}),
_onMessageRemoveFormHistoryEntry: function (msg, entry) {
this.removeFormHistoryEntry(msg, entry);
},
_onMessageSpeculativeConnect: function (msg, engineName) {
let engine = Services.search.getEngineByName(engineName);
if (!engine) {
throw new Error("Unknown engine name: " + engineName);
}
if (msg.target.contentWindow) {
engine.speculativeConnect({
window: msg.target.contentWindow,
});
}
},
_onObserve: Task.async(function* (data) {
if (data === "engine-current") {
let engine = yield this._currentEngineObj();
this._broadcast("CurrentEngine", engine);
}
else if (data !== "engine-default") {
// engine-default is always sent with engine-current and isn't otherwise
// relevant to content searches.
let state = yield this.currentStateObj();
this._broadcast("CurrentState", state);
}
}),
_suggestionDataForBrowser: function (browser, create=false) {
let data = this._suggestionMap.get(browser);
if (!data && create) {
// Since one SearchSuggestionController instance is meant to be used per
// autocomplete widget, this means that we assume each xul:browser has at
// most one such widget.
data = {
controller: new SearchSuggestionController(),
};
this._suggestionMap.set(browser, data);
}
return data;
},
_reply: function (msg, type, data) {
// We reply asyncly to messages, and by the time we reply the browser we're
// responding to may have been destroyed. messageManager is null then.
if (!Cu.isDeadWrapper(msg.target) && msg.target.messageManager) {
msg.target.messageManager.sendAsyncMessage(...this._msgArgs(type, data));
}
},
_broadcast: function (type, data) {
Cc["@mozilla.org/globalmessagemanager;1"].
getService(Ci.nsIMessageListenerManager).
broadcastAsyncMessage(...this._msgArgs(type, data));
},
_msgArgs: function (type, data) {
return [OUTBOUND_MESSAGE, {
type: type,
data: data,
}];
},
_currentEngineObj: Task.async(function* () {
let engine = Services.search.currentEngine;
let favicon = engine.getIconURLBySize(16, 16);
let placeholder = this._stringBundle.formatStringFromName(
"searchWithEngine", [engine.name], 1);
let obj = {
name: engine.name,
placeholder: placeholder,
iconBuffer: yield this._arrayBufferFromDataURI(favicon),
};
return obj;
}),
_arrayBufferFromDataURI: function (uri) {
if (!uri) {
return Promise.resolve(null);
}
let deferred = Promise.defer();
let xhr = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].
createInstance(Ci.nsIXMLHttpRequest);
xhr.open("GET", uri, true);
xhr.responseType = "arraybuffer";
xhr.onload = () => {
deferred.resolve(xhr.response);
};
xhr.onerror = xhr.onabort = xhr.ontimeout = () => {
deferred.resolve(null);
};
try {
// This throws if the URI is erroneously encoded.
xhr.send();
}
catch (err) {
return Promise.resolve(null);
}
return deferred.promise;
},
_ensureDataHasProperties: function (data, requiredProperties) {
for (let prop of requiredProperties) {
if (!(prop in data)) {
throw new Error("Message data missing required property: " + prop);
}
}
},
_initService: function () {
if (!this._initServicePromise) {
let deferred = Promise.defer();
this._initServicePromise = deferred.promise;
Services.search.init(() => deferred.resolve());
}
return this._initServicePromise;
},
};

View file

@ -0,0 +1,393 @@
/* 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";
const {classes: Cc, interfaces: Ci, results: Cr, utils: Cu} = Components;
this.EXPORTED_SYMBOLS = [ "ContentWebRTC" ];
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
XPCOMUtils.defineLazyServiceGetter(this, "MediaManagerService",
"@mozilla.org/mediaManagerService;1",
"nsIMediaManagerService");
const kBrowserURL = "chrome://browser/content/browser.xul";
this.ContentWebRTC = {
_initialized: false,
init: function() {
if (this._initialized)
return;
this._initialized = true;
Services.obs.addObserver(handleGUMRequest, "getUserMedia:request", false);
Services.obs.addObserver(handlePCRequest, "PeerConnection:request", false);
Services.obs.addObserver(updateIndicators, "recording-device-events", false);
Services.obs.addObserver(removeBrowserSpecificIndicator, "recording-window-ended", false);
if (Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_CONTENT)
Services.obs.addObserver(processShutdown, "content-child-shutdown", false);
},
uninit: function() {
Services.obs.removeObserver(handleGUMRequest, "getUserMedia:request");
Services.obs.removeObserver(handlePCRequest, "PeerConnection:request");
Services.obs.removeObserver(updateIndicators, "recording-device-events");
Services.obs.removeObserver(removeBrowserSpecificIndicator, "recording-window-ended");
if (Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_CONTENT)
Services.obs.removeObserver(processShutdown, "content-child-shutdown");
this._initialized = false;
},
// Called only for 'unload' to remove pending gUM prompts in reloaded frames.
handleEvent: function(aEvent) {
let contentWindow = aEvent.target.defaultView;
let mm = getMessageManagerForWindow(contentWindow);
for (let key of contentWindow.pendingGetUserMediaRequests.keys()) {
mm.sendAsyncMessage("webrtc:CancelRequest", key);
}
for (let key of contentWindow.pendingPeerConnectionRequests.keys()) {
mm.sendAsyncMessage("rtcpeer:CancelRequest", key);
}
},
receiveMessage: function(aMessage) {
switch (aMessage.name) {
case "rtcpeer:Allow":
case "rtcpeer:Deny": {
let callID = aMessage.data.callID;
let contentWindow = Services.wm.getOuterWindowWithId(aMessage.data.windowID);
forgetPCRequest(contentWindow, callID);
let topic = (aMessage.name == "rtcpeer:Allow") ? "PeerConnection:response:allow" :
"PeerConnection:response:deny";
Services.obs.notifyObservers(null, topic, callID);
break;
}
case "webrtc:Allow": {
let callID = aMessage.data.callID;
let contentWindow = Services.wm.getOuterWindowWithId(aMessage.data.windowID);
let devices = contentWindow.pendingGetUserMediaRequests.get(callID);
forgetGUMRequest(contentWindow, callID);
let allowedDevices = Cc["@mozilla.org/array;1"]
.createInstance(Ci.nsIMutableArray);
for (let deviceIndex of aMessage.data.devices)
allowedDevices.appendElement(devices[deviceIndex], /* weak =*/ false);
Services.obs.notifyObservers(allowedDevices, "getUserMedia:response:allow", callID);
break;
}
case "webrtc:Deny":
denyGUMRequest(aMessage.data);
break;
case "webrtc:StopSharing":
Services.obs.notifyObservers(null, "getUserMedia:revoke", aMessage.data);
break;
}
}
};
function handlePCRequest(aSubject, aTopic, aData) {
let { windowID, innerWindowID, callID, isSecure } = aSubject;
let contentWindow = Services.wm.getOuterWindowWithId(windowID);
let mm = getMessageManagerForWindow(contentWindow);
if (!mm) {
// Workaround for Bug 1207784. To use WebRTC, add-ons right now use
// hiddenWindow.mozRTCPeerConnection which is only privileged on OSX. Other
// platforms end up here without a message manager.
// TODO: Remove once there's a better way (1215591).
// Skip permission check in the absence of a message manager.
Services.obs.notifyObservers(null, "PeerConnection:response:allow", callID);
return;
}
if (!contentWindow.pendingPeerConnectionRequests) {
setupPendingListsInitially(contentWindow);
}
contentWindow.pendingPeerConnectionRequests.add(callID);
let request = {
windowID: windowID,
innerWindowID: innerWindowID,
callID: callID,
documentURI: contentWindow.document.documentURI,
secure: isSecure,
};
mm.sendAsyncMessage("rtcpeer:Request", request);
}
function handleGUMRequest(aSubject, aTopic, aData) {
let constraints = aSubject.getConstraints();
let secure = aSubject.isSecure;
let contentWindow = Services.wm.getOuterWindowWithId(aSubject.windowID);
contentWindow.navigator.mozGetUserMediaDevices(
constraints,
function (devices) {
// If the window has been closed while we were waiting for the list of
// devices, there's nothing to do in the callback anymore.
if (contentWindow.closed)
return;
prompt(contentWindow, aSubject.windowID, aSubject.callID,
constraints, devices, secure);
},
function (error) {
// bug 827146 -- In the future, the UI should catch NotFoundError
// and allow the user to plug in a device, instead of immediately failing.
denyGUMRequest({callID: aSubject.callID}, error);
},
aSubject.innerWindowID,
aSubject.callID);
}
function prompt(aContentWindow, aWindowID, aCallID, aConstraints, aDevices, aSecure) {
let audioDevices = [];
let videoDevices = [];
let devices = [];
// MediaStreamConstraints defines video as 'boolean or MediaTrackConstraints'.
let video = aConstraints.video || aConstraints.picture;
let audio = aConstraints.audio;
let sharingScreen = video && typeof(video) != "boolean" &&
video.mediaSource != "camera";
let sharingAudio = audio && typeof(audio) != "boolean" &&
audio.mediaSource != "microphone";
for (let device of aDevices) {
device = device.QueryInterface(Ci.nsIMediaDevice);
switch (device.type) {
case "audio":
// Check that if we got a microphone, we have not requested an audio
// capture, and if we have requested an audio capture, we are not
// getting a microphone instead.
if (audio && (device.mediaSource == "microphone") != sharingAudio) {
audioDevices.push({name: device.name, deviceIndex: devices.length,
id: device.rawId, mediaSource: device.mediaSource});
devices.push(device);
}
break;
case "video":
// Verify that if we got a camera, we haven't requested a screen share,
// or that if we requested a screen share we aren't getting a camera.
if (video && (device.mediaSource == "camera") != sharingScreen) {
let deviceObject = {name: device.name, deviceIndex: devices.length,
id: device.rawId, mediaSource: device.mediaSource};
if (device.scary)
deviceObject.scary = true;
videoDevices.push(deviceObject);
devices.push(device);
}
break;
}
}
let requestTypes = [];
if (videoDevices.length)
requestTypes.push(sharingScreen ? "Screen" : "Camera");
if (audioDevices.length)
requestTypes.push(sharingAudio ? "AudioCapture" : "Microphone");
if (!requestTypes.length) {
denyGUMRequest({callID: aCallID}, "NotFoundError");
return;
}
if (!aContentWindow.pendingGetUserMediaRequests) {
setupPendingListsInitially(aContentWindow);
}
aContentWindow.pendingGetUserMediaRequests.set(aCallID, devices);
let request = {
callID: aCallID,
windowID: aWindowID,
origin: aContentWindow.origin,
documentURI: aContentWindow.document.documentURI,
secure: aSecure,
requestTypes: requestTypes,
sharingScreen: sharingScreen,
sharingAudio: sharingAudio,
audioDevices: audioDevices,
videoDevices: videoDevices
};
let mm = getMessageManagerForWindow(aContentWindow);
mm.sendAsyncMessage("webrtc:Request", request);
}
function denyGUMRequest(aData, aError) {
let msg = null;
if (aError) {
msg = Cc["@mozilla.org/supports-string;1"].createInstance(Ci.nsISupportsString);
msg.data = aError;
}
Services.obs.notifyObservers(msg, "getUserMedia:response:deny", aData.callID);
if (!aData.windowID)
return;
let contentWindow = Services.wm.getOuterWindowWithId(aData.windowID);
if (contentWindow.pendingGetUserMediaRequests)
forgetGUMRequest(contentWindow, aData.callID);
}
function forgetGUMRequest(aContentWindow, aCallID) {
aContentWindow.pendingGetUserMediaRequests.delete(aCallID);
forgetPendingListsEventually(aContentWindow);
}
function forgetPCRequest(aContentWindow, aCallID) {
aContentWindow.pendingPeerConnectionRequests.delete(aCallID);
forgetPendingListsEventually(aContentWindow);
}
function setupPendingListsInitially(aContentWindow) {
if (aContentWindow.pendingGetUserMediaRequests) {
return;
}
aContentWindow.pendingGetUserMediaRequests = new Map();
aContentWindow.pendingPeerConnectionRequests = new Set();
aContentWindow.addEventListener("unload", ContentWebRTC);
}
function forgetPendingListsEventually(aContentWindow) {
if (aContentWindow.pendingGetUserMediaRequests.size ||
aContentWindow.pendingPeerConnectionRequests.size) {
return;
}
aContentWindow.pendingGetUserMediaRequests = null;
aContentWindow.pendingPeerConnectionRequests = null;
aContentWindow.removeEventListener("unload", ContentWebRTC);
}
function updateIndicators(aSubject, aTopic, aData) {
if (aSubject instanceof Ci.nsIPropertyBag &&
aSubject.getProperty("requestURL") == kBrowserURL) {
// Ignore notifications caused by the browser UI showing previews.
return;
}
let contentWindowArray = MediaManagerService.activeMediaCaptureWindows;
let count = contentWindowArray.length;
let state = {
showGlobalIndicator: count > 0,
showCameraIndicator: false,
showMicrophoneIndicator: false,
showScreenSharingIndicator: ""
};
let cpmm = Cc["@mozilla.org/childprocessmessagemanager;1"]
.getService(Ci.nsIMessageSender);
cpmm.sendAsyncMessage("webrtc:UpdatingIndicators");
// If several iframes in the same page use media streams, it's possible to
// have the same top level window several times. We use a Set to avoid
// sending duplicate notifications.
let contentWindows = new Set();
for (let i = 0; i < count; ++i) {
contentWindows.add(contentWindowArray.queryElementAt(i, Ci.nsISupports).top);
}
for (let contentWindow of contentWindows) {
if (contentWindow.document.documentURI == kBrowserURL) {
// There may be a preview shown at the same time as other streams.
continue;
}
let tabState = getTabStateForContentWindow(contentWindow);
if (tabState.camera)
state.showCameraIndicator = true;
if (tabState.microphone)
state.showMicrophoneIndicator = true;
if (tabState.screen) {
if (tabState.screen == "Screen") {
state.showScreenSharingIndicator = "Screen";
}
else if (tabState.screen == "Window") {
if (state.showScreenSharingIndicator != "Screen")
state.showScreenSharingIndicator = "Window";
}
else if (tabState.screen == "Application") {
if (!state.showScreenSharingIndicator)
state.showScreenSharingIndicator = "Application";
}
else if (tabState.screen == "Browser") {
if (!state.showScreenSharingIndicator)
state.showScreenSharingIndicator = "Browser";
}
}
let mm = getMessageManagerForWindow(contentWindow);
mm.sendAsyncMessage("webrtc:UpdateBrowserIndicators", tabState);
}
cpmm.sendAsyncMessage("webrtc:UpdateGlobalIndicators", state);
}
function removeBrowserSpecificIndicator(aSubject, aTopic, aData) {
let contentWindow = Services.wm.getOuterWindowWithId(aData).top;
if (contentWindow.document.documentURI == kBrowserURL) {
// Ignore notifications caused by the browser UI showing previews.
return;
}
let tabState = getTabStateForContentWindow(contentWindow);
if (!tabState.camera && !tabState.microphone && !tabState.screen)
tabState = {windowId: tabState.windowId};
let mm = getMessageManagerForWindow(contentWindow);
if (mm)
mm.sendAsyncMessage("webrtc:UpdateBrowserIndicators", tabState);
}
function getTabStateForContentWindow(aContentWindow) {
let camera = {}, microphone = {}, screen = {}, window = {}, app = {}, browser = {};
MediaManagerService.mediaCaptureWindowState(aContentWindow, camera, microphone,
screen, window, app, browser);
let tabState = {camera: camera.value, microphone: microphone.value};
if (screen.value)
tabState.screen = "Screen";
else if (window.value)
tabState.screen = "Window";
else if (app.value)
tabState.screen = "Application";
else if (browser.value)
tabState.screen = "Browser";
tabState.windowId = getInnerWindowIDForWindow(aContentWindow);
tabState.documentURI = aContentWindow.document.documentURI;
return tabState;
}
function getInnerWindowIDForWindow(aContentWindow) {
return aContentWindow.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIDOMWindowUtils)
.currentInnerWindowID;
}
function getMessageManagerForWindow(aContentWindow) {
let ir = aContentWindow.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIDocShell)
.sameTypeRootTreeItem
.QueryInterface(Ci.nsIInterfaceRequestor);
try {
// If e10s is disabled, this throws NS_NOINTERFACE for closed tabs.
return ir.getInterface(Ci.nsIContentFrameMessageManager);
} catch (e) {
if (e.result == Cr.NS_NOINTERFACE) {
return null;
}
throw e;
}
}
function processShutdown() {
ContentWebRTC.uninit();
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,128 @@
/* 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 = ["E10SUtils"];
const {interfaces: Ci, utils: Cu, classes: Cc} = Components;
Cu.import("resource://gre/modules/Services.jsm");
function getAboutModule(aURL) {
// Needs to match NS_GetAboutModuleName
let moduleName = aURL.path.replace(/[#?].*/, "").toLowerCase();
let contract = "@mozilla.org/network/protocol/about;1?what=" + moduleName;
try {
return Cc[contract].getService(Ci.nsIAboutModule);
}
catch (e) {
// Either the about module isn't defined or it is broken. In either case
// ignore it.
return null;
}
}
this.E10SUtils = {
canLoadURIInProcess: function(aURL, aProcess) {
// loadURI in browser.xml treats null as about:blank
if (!aURL)
aURL = "about:blank";
// Javascript urls can load in any process, they apply to the current document
if (aURL.startsWith("javascript:"))
return true;
let processIsRemote = aProcess == Ci.nsIXULRuntime.PROCESS_TYPE_CONTENT;
let canLoadRemote = true;
let mustLoadRemote = true;
if (aURL.startsWith("about:")) {
let url = Services.io.newURI(aURL, null, null);
let module = getAboutModule(url);
// If the module doesn't exist then an error page will be loading, that
// should be ok to load in either process
if (module) {
let flags = module.getURIFlags(url);
canLoadRemote = !!(flags & Ci.nsIAboutModule.URI_CAN_LOAD_IN_CHILD);
mustLoadRemote = !!(flags & Ci.nsIAboutModule.URI_MUST_LOAD_IN_CHILD);
}
}
if (aURL.startsWith("chrome:")) {
let url;
try {
// This can fail for invalid Chrome URIs, in which case we will end up
// not loading anything anyway.
url = Services.io.newURI(aURL, null, null);
} catch (ex) {
canLoadRemote = true;
mustLoadRemote = false;
}
if (url) {
let chromeReg = Cc["@mozilla.org/chrome/chrome-registry;1"].
getService(Ci.nsIXULChromeRegistry);
canLoadRemote = chromeReg.canLoadURLRemotely(url);
mustLoadRemote = chromeReg.mustLoadURLRemotely(url);
}
}
if (aURL.startsWith("moz-extension:")) {
canLoadRemote = false;
mustLoadRemote = false;
}
if (aURL.startsWith("view-source:")) {
return this.canLoadURIInProcess(aURL.substr("view-source:".length), aProcess);
}
if (mustLoadRemote)
return processIsRemote;
if (!canLoadRemote && processIsRemote)
return false;
return true;
},
shouldLoadURI: function(aDocShell, aURI, aReferrer) {
// Inner frames should always load in the current process
if (aDocShell.QueryInterface(Ci.nsIDocShellTreeItem).sameTypeParent)
return true;
// If the URI can be loaded in the current process then continue
return this.canLoadURIInProcess(aURI.spec, Services.appinfo.processType);
},
redirectLoad: function(aDocShell, aURI, aReferrer, aFreshProcess) {
// Retarget the load to the correct process
let messageManager = aDocShell.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIContentFrameMessageManager);
let sessionHistory = aDocShell.getInterface(Ci.nsIWebNavigation).sessionHistory;
messageManager.sendAsyncMessage("Browser:LoadURI", {
loadOptions: {
uri: aURI.spec,
flags: Ci.nsIWebNavigation.LOAD_FLAGS_NONE,
referrer: aReferrer ? aReferrer.spec : null,
reloadInFreshProcess: !!aFreshProcess,
},
historyIndex: sessionHistory.requestedIndex,
});
return false;
},
wrapHandlingUserInput: function(aWindow, aIsHandling, aCallback) {
var handlingUserInput;
try {
handlingUserInput = aWindow.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIDOMWindowUtils)
.setHandlingUserInput(aIsHandling);
aCallback();
} finally {
handlingUserInput.destruct();
}
},
};

View file

@ -0,0 +1,104 @@
/* 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 = [ "Feeds" ];
Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
Components.utils.import("resource://gre/modules/Services.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "BrowserUtils",
"resource://gre/modules/BrowserUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "RecentWindow",
"resource:///modules/RecentWindow.jsm");
const { interfaces: Ci, classes: Cc } = Components;
this.Feeds = {
init() {
let mm = Cc["@mozilla.org/globalmessagemanager;1"].getService(Ci.nsIMessageListenerManager);
mm.addMessageListener("WCCR:registerProtocolHandler", this);
mm.addMessageListener("WCCR:registerContentHandler", this);
Services.ppmm.addMessageListener("WCCR:setAutoHandler", this);
Services.ppmm.addMessageListener("FeedConverter:addLiveBookmark", this);
},
receiveMessage(aMessage) {
let data = aMessage.data;
switch (aMessage.name) {
case "WCCR:registerProtocolHandler": {
let registrar = Cc["@mozilla.org/embeddor.implemented/web-content-handler-registrar;1"].
getService(Ci.nsIWebContentHandlerRegistrar);
registrar.registerProtocolHandler(data.protocol, data.uri, data.title,
aMessage.target);
break;
}
case "WCCR:registerContentHandler": {
let registrar = Cc["@mozilla.org/embeddor.implemented/web-content-handler-registrar;1"].
getService(Ci.nsIWebContentHandlerRegistrar);
registrar.registerContentHandler(data.contentType, data.uri, data.title,
aMessage.target);
break;
}
case "WCCR:setAutoHandler": {
let registrar = Cc["@mozilla.org/embeddor.implemented/web-content-handler-registrar;1"].
getService(Ci.nsIWebContentConverterService);
registrar.setAutoHandler(data.contentType, data.handler);
break;
}
case "FeedConverter:addLiveBookmark": {
let topWindow = RecentWindow.getMostRecentBrowserWindow();
topWindow.PlacesCommandHook.addLiveBookmark(data.spec, data.title, data.subtitle)
.catch(Components.utils.reportError);
break;
}
}
},
/**
* isValidFeed: checks whether the given data represents a valid feed.
*
* @param aLink
* An object representing a feed with title, href and type.
* @param aPrincipal
* The principal of the document, used for security check.
* @param aIsFeed
* Whether this is already a known feed or not, if true only a security
* check will be performed.
*/
isValidFeed: function(aLink, aPrincipal, aIsFeed) {
if (!aLink || !aPrincipal)
return false;
var type = aLink.type.toLowerCase().replace(/^\s+|\s*(?:;.*)?$/g, "");
if (!aIsFeed) {
aIsFeed = (type == "application/rss+xml" ||
type == "application/atom+xml");
}
if (aIsFeed) {
// re-create the principal as it may be a CPOW.
// once this can't be a CPOW anymore, we should just use aPrincipal instead
// of creating a new one.
let principalURI = BrowserUtils.makeURIFromCPOW(aPrincipal.URI);
let principalToCheck =
Services.scriptSecurityManager.createCodebasePrincipal(principalURI, aPrincipal.originAttributes);
try {
BrowserUtils.urlSecurityCheck(aLink.href, principalToCheck,
Ci.nsIScriptSecurityManager.DISALLOW_INHERIT_PRINCIPAL);
return type || "application/rss+xml";
}
catch (ex) {
}
}
return null;
},
};

View file

@ -0,0 +1,235 @@
/* 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/. */
/*
* Handles the validation callback from nsIFormFillController and
* the display of the help panel on invalid elements.
*/
"use strict";
var Cc = Components.classes;
var Ci = Components.interfaces;
var Cu = Components.utils;
var HTMLInputElement = Ci.nsIDOMHTMLInputElement;
var HTMLTextAreaElement = Ci.nsIDOMHTMLTextAreaElement;
var HTMLSelectElement = Ci.nsIDOMHTMLSelectElement;
var HTMLButtonElement = Ci.nsIDOMHTMLButtonElement;
this.EXPORTED_SYMBOLS = [ "FormSubmitObserver" ];
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/BrowserUtils.jsm");
function FormSubmitObserver(aWindow, aTabChildGlobal) {
this.init(aWindow, aTabChildGlobal);
}
FormSubmitObserver.prototype =
{
_validationMessage: "",
_content: null,
_element: null,
/*
* Public apis
*/
init: function(aWindow, aTabChildGlobal)
{
this._content = aWindow;
this._tab = aTabChildGlobal;
this._mm =
this._content.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIDocShell)
.sameTypeRootTreeItem
.QueryInterface(Ci.nsIDocShell)
.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIContentFrameMessageManager);
// nsIFormSubmitObserver callback about invalid forms. See HTMLFormElement
// for details.
Services.obs.addObserver(this, "invalidformsubmit", false);
this._tab.addEventListener("pageshow", this, false);
this._tab.addEventListener("unload", this, false);
},
uninit: function()
{
Services.obs.removeObserver(this, "invalidformsubmit");
this._content.removeEventListener("pageshow", this, false);
this._content.removeEventListener("unload", this, false);
this._mm = null;
this._element = null;
this._content = null;
this._tab = null;
},
/*
* Events
*/
handleEvent: function (aEvent) {
switch (aEvent.type) {
case "pageshow":
if (this._isRootDocumentEvent(aEvent)) {
this._hidePopup();
}
break;
case "unload":
this.uninit();
break;
case "input":
this._onInput(aEvent);
break;
case "blur":
this._onBlur(aEvent);
break;
}
},
/*
* nsIFormSubmitObserver
*/
notifyInvalidSubmit : function (aFormElement, aInvalidElements)
{
// We are going to handle invalid form submission attempt by focusing the
// first invalid element and show the corresponding validation message in a
// panel attached to the element.
if (!aInvalidElements.length) {
return;
}
// Insure that this is the FormSubmitObserver associated with the
// element / window this notification is about.
let element = aInvalidElements.queryElementAt(0, Ci.nsISupports);
if (this._content != element.ownerGlobal.top.document.defaultView) {
return;
}
if (!(element instanceof HTMLInputElement ||
element instanceof HTMLTextAreaElement ||
element instanceof HTMLSelectElement ||
element instanceof HTMLButtonElement)) {
return;
}
// Update validation message before showing notification
this._validationMessage = element.validationMessage;
// Don't connect up to the same element more than once.
if (this._element == element) {
this._showPopup(element);
return;
}
this._element = element;
element.focus();
// Watch for input changes which may change the validation message.
element.addEventListener("input", this, false);
// Watch for focus changes so we can disconnect our listeners and
// hide the popup.
element.addEventListener("blur", this, false);
this._showPopup(element);
},
/*
* Internal
*/
/*
* Handles input changes on the form element we've associated a popup
* with. Updates the validation message or closes the popup if form data
* becomes valid.
*/
_onInput: function (aEvent) {
let element = aEvent.originalTarget;
// If the form input is now valid, hide the popup.
if (element.validity.valid) {
this._hidePopup();
return;
}
// If the element is still invalid for a new reason, we should update
// the popup error message.
if (this._validationMessage != element.validationMessage) {
this._validationMessage = element.validationMessage;
this._showPopup(element);
}
},
/*
* Blur event handler in which we disconnect from the form element and
* hide the popup.
*/
_onBlur: function (aEvent) {
aEvent.originalTarget.removeEventListener("input", this, false);
aEvent.originalTarget.removeEventListener("blur", this, false);
this._element = null;
this._hidePopup();
},
/*
* Send the show popup message to chrome with appropriate position
* information. Can be called repetitively to update the currently
* displayed popup position and text.
*/
_showPopup: function (aElement) {
// Collect positional information and show the popup
let panelData = {};
panelData.message = this._validationMessage;
// Note, this is relative to the browser and needs to be translated
// in chrome.
panelData.contentRect = BrowserUtils.getElementBoundingRect(aElement);
// We want to show the popup at the middle of checkbox and radio buttons
// and where the content begin for the other elements.
let offset = 0;
if (aElement.tagName == 'INPUT' &&
(aElement.type == 'radio' || aElement.type == 'checkbox')) {
panelData.position = "bottomcenter topleft";
} else {
let win = aElement.ownerGlobal;
let style = win.getComputedStyle(aElement, null);
if (style.direction == 'rtl') {
offset = parseInt(style.paddingRight) + parseInt(style.borderRightWidth);
} else {
offset = parseInt(style.paddingLeft) + parseInt(style.borderLeftWidth);
}
let zoomFactor = this._getWindowUtils().fullZoom;
panelData.offset = Math.round(offset * zoomFactor);
panelData.position = "after_start";
}
this._mm.sendAsyncMessage("FormValidation:ShowPopup", panelData);
},
_hidePopup: function () {
this._mm.sendAsyncMessage("FormValidation:HidePopup", {});
},
_getWindowUtils: function () {
return this._content.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils);
},
_isRootDocumentEvent: function (aEvent) {
if (this._content == null) {
return true;
}
let target = aEvent.originalTarget;
return (target == this._content.document ||
(target.ownerDocument && target.ownerDocument == this._content.document));
},
QueryInterface : XPCOMUtils.generateQI([Ci.nsIFormSubmitObserver])
};

View file

@ -0,0 +1,157 @@
/* 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/. */
/*
* Chrome side handling of form validation popup.
*/
"use strict";
var Cc = Components.classes;
var Ci = Components.interfaces;
var Cu = Components.utils;
this.EXPORTED_SYMBOLS = [ "FormValidationHandler" ];
Cu.import("resource://gre/modules/Services.jsm");
var FormValidationHandler =
{
_panel: null,
_anchor: null,
/*
* Public apis
*/
init: function () {
let mm = Cc["@mozilla.org/globalmessagemanager;1"].getService(Ci.nsIMessageListenerManager);
mm.addMessageListener("FormValidation:ShowPopup", this);
mm.addMessageListener("FormValidation:HidePopup", this);
},
uninit: function () {
let mm = Cc["@mozilla.org/globalmessagemanager;1"].getService(Ci.nsIMessageListenerManager);
mm.removeMessageListener("FormValidation:ShowPopup", this);
mm.removeMessageListener("FormValidation:HidePopup", this);
this._panel = null;
this._anchor = null;
},
hidePopup: function () {
this._hidePopup();
},
/*
* Events
*/
receiveMessage: function (aMessage) {
let window = aMessage.target.ownerGlobal;
let json = aMessage.json;
let tabBrowser = window.gBrowser;
switch (aMessage.name) {
case "FormValidation:ShowPopup":
// target is the <browser>, make sure we're receiving a message
// from the foreground tab.
if (tabBrowser && aMessage.target != tabBrowser.selectedBrowser) {
return;
}
this._showPopup(window, json);
break;
case "FormValidation:HidePopup":
this._hidePopup();
break;
}
},
observe: function (aSubject, aTopic, aData) {
this._hidePopup();
},
handleEvent: function (aEvent) {
switch (aEvent.type) {
case "FullZoomChange":
case "TextZoomChange":
case "ZoomChangeUsingMouseWheel":
case "scroll":
this._hidePopup();
break;
case "popuphiding":
this._onPopupHiding(aEvent);
break;
}
},
/*
* Internal
*/
_onPopupHiding: function (aEvent) {
aEvent.originalTarget.removeEventListener("popuphiding", this, true);
let tabBrowser = aEvent.originalTarget.ownerDocument.getElementById("content");
tabBrowser.selectedBrowser.removeEventListener("scroll", this, true);
tabBrowser.selectedBrowser.removeEventListener("FullZoomChange", this, false);
tabBrowser.selectedBrowser.removeEventListener("TextZoomChange", this, false);
tabBrowser.selectedBrowser.removeEventListener("ZoomChangeUsingMouseWheel", this, false);
this._panel.hidden = true;
this._panel = null;
this._anchor.hidden = true;
this._anchor = null;
},
/*
* Shows the form validation popup at a specified position or updates the
* messaging and position if the popup is already displayed.
*
* @aWindow - the chrome window
* @aPanelData - Object that contains popup information
* aPanelData stucture detail:
* contentRect - the bounding client rect of the target element. If
* content is remote, this is relative to the browser, otherwise its
* relative to the window.
* position - popup positional string constants.
* message - the form element validation message text.
*/
_showPopup: function (aWindow, aPanelData) {
let previouslyShown = !!this._panel;
this._panel = aWindow.document.getElementById("invalid-form-popup");
this._panel.firstChild.textContent = aPanelData.message;
this._panel.hidden = false;
let tabBrowser = aWindow.gBrowser;
this._anchor = tabBrowser.popupAnchor;
this._anchor.left = aPanelData.contentRect.left;
this._anchor.top = aPanelData.contentRect.top;
this._anchor.width = aPanelData.contentRect.width;
this._anchor.height = aPanelData.contentRect.height;
this._anchor.hidden = false;
// Display the panel if it isn't already visible.
if (!previouslyShown) {
// Cleanup after the popup is hidden
this._panel.addEventListener("popuphiding", this, true);
// Hide if the user scrolls the page
tabBrowser.selectedBrowser.addEventListener("scroll", this, true);
tabBrowser.selectedBrowser.addEventListener("FullZoomChange", this, false);
tabBrowser.selectedBrowser.addEventListener("TextZoomChange", this, false);
tabBrowser.selectedBrowser.addEventListener("ZoomChangeUsingMouseWheel", this, false);
// Open the popup
this._panel.openPopup(this._anchor, aPanelData.position, 0, 0, false);
}
},
/*
* Hide the popup if currently displayed. Will fire an event to onPopupHiding
* above if visible.
*/
_hidePopup: function () {
if (this._panel) {
this._panel.hidePopup();
}
}
};

View file

@ -0,0 +1,86 @@
/* 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 = ["HiddenFrame"];
const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
Cu.import("resource://gre/modules/PromiseUtils.jsm");
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/Timer.jsm");
const HTML_NS = "http://www.w3.org/1999/xhtml";
const XUL_PAGE = "data:application/vnd.mozilla.xul+xml;charset=utf-8,<window%20id='win'/>";
/**
* An hidden frame object. It takes care of creating an IFRAME and attaching it the
* |hiddenDOMWindow|.
*/
function HiddenFrame() {}
HiddenFrame.prototype = {
_frame: null,
_deferred: null,
_retryTimerId: null,
get hiddenDOMDocument() {
return Services.appShell.hiddenDOMWindow.document;
},
get isReady() {
return this.hiddenDOMDocument.readyState === "complete";
},
/**
* Gets the |contentWindow| of the hidden frame. Creates the frame if needed.
* @returns Promise Returns a promise which is resolved when the hidden frame has finished
* loading.
*/
get: function () {
if (!this._deferred) {
this._deferred = PromiseUtils.defer();
this._create();
}
return this._deferred.promise;
},
destroy: function () {
clearTimeout(this._retryTimerId);
if (this._frame) {
if (!Cu.isDeadWrapper(this._frame)) {
this._frame.removeEventListener("load", this, true);
this._frame.remove();
}
this._frame = null;
this._deferred = null;
}
},
handleEvent: function () {
let contentWindow = this._frame.contentWindow;
if (contentWindow.location.href === XUL_PAGE) {
this._frame.removeEventListener("load", this, true);
this._deferred.resolve(contentWindow);
} else {
contentWindow.location = XUL_PAGE;
}
},
_create: function () {
if (this.isReady) {
let doc = this.hiddenDOMDocument;
this._frame = doc.createElementNS(HTML_NS, "iframe");
this._frame.addEventListener("load", this, true);
doc.documentElement.appendChild(this._frame);
} else {
// Check again if |hiddenDOMDocument| is ready as soon as possible.
this._retryTimerId = setTimeout(this._create.bind(this), 0);
}
}
};

View file

@ -0,0 +1,172 @@
/* 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";
const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
this.EXPORTED_SYMBOLS = ["LaterRun"];
Cu.import("resource://gre/modules/Preferences.jsm");
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "setInterval", "resource://gre/modules/Timer.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "clearInterval", "resource://gre/modules/Timer.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "RecentWindow", "resource://gre/modules/RecentWindow.jsm");
const kEnabledPref = "browser.laterrun.enabled";
const kPagePrefRoot = "browser.laterrun.pages.";
// Number of sessions we've been active in
const kSessionCountPref = "browser.laterrun.bookkeeping.sessionCount";
// Time the profile was created at:
const kProfileCreationTime = "browser.laterrun.bookkeeping.profileCreationTime";
// After 50 sessions or 1 month since install, assume we will no longer be
// interested in showing anything to "new" users
const kSelfDestructSessionLimit = 50;
const kSelfDestructHoursLimit = 31 * 24;
class Page {
constructor({pref, minimumHoursSinceInstall, minimumSessionCount, requireBoth, url}) {
this.pref = pref;
this.minimumHoursSinceInstall = minimumHoursSinceInstall || 0;
this.minimumSessionCount = minimumSessionCount || 1;
this.requireBoth = requireBoth || false;
this.url = url;
}
get hasRun() {
return Preferences.get(this.pref + "hasRun", false);
}
applies(sessionInfo) {
if (this.hasRun) {
return false;
}
if (this.requireBoth) {
return sessionInfo.sessionCount >= this.minimumSessionCount &&
sessionInfo.hoursSinceInstall >= this.minimumHoursSinceInstall;
}
return sessionInfo.sessionCount >= this.minimumSessionCount ||
sessionInfo.hoursSinceInstall >= this.minimumHoursSinceInstall;
}
}
let LaterRun = {
init() {
if (!this.enabled) {
return;
}
// If this is the first run, set the time we were installed
if (!Preferences.has(kProfileCreationTime)) {
// We need to store seconds in order to fit within int prefs.
Preferences.set(kProfileCreationTime, Math.floor(Date.now() / 1000));
}
this.sessionCount++;
if (this.hoursSinceInstall > kSelfDestructHoursLimit ||
this.sessionCount > kSelfDestructSessionLimit) {
this.selfDestruct();
return;
}
},
// The enabled, hoursSinceInstall and sessionCount properties mirror the
// preferences system, and are here for convenience.
get enabled() {
return Preferences.get(kEnabledPref, false);
},
set enabled(val) {
let wasEnabled = this.enabled;
Preferences.set(kEnabledPref, val);
if (val && !wasEnabled) {
this.init();
}
},
get hoursSinceInstall() {
let installStamp = Preferences.get(kProfileCreationTime, Date.now() / 1000);
return Math.floor((Date.now() / 1000 - installStamp) / 3600);
},
get sessionCount() {
if (this._sessionCount) {
return this._sessionCount;
}
return this._sessionCount = Preferences.get(kSessionCountPref, 0);
},
set sessionCount(val) {
this._sessionCount = val;
Preferences.set(kSessionCountPref, val);
},
// Because we don't want to keep incrementing this indefinitely for no reason,
// we will turn ourselves off after a set amount of time/sessions (see top of
// file).
selfDestruct() {
Preferences.set(kEnabledPref, false);
},
// Create an array of Page objects based on the currently set prefs
readPages() {
// Enumerate all the pages.
let allPrefsForPages = Services.prefs.getChildList(kPagePrefRoot);
let pageDataStore = new Map();
for (let pref of allPrefsForPages) {
let [slug, prop] = pref.substring(kPagePrefRoot.length).split(".");
if (!pageDataStore.has(slug)) {
pageDataStore.set(slug, {pref: pref.substring(0, pref.length - prop.length)});
}
let defaultPrefValue = 0;
if (prop == "requireBoth" || prop == "hasRun") {
defaultPrefValue = false;
} else if (prop == "url") {
defaultPrefValue = "";
}
pageDataStore.get(slug)[prop] = Preferences.get(pref, defaultPrefValue);
}
let rv = [];
for (let [, pageData] of pageDataStore) {
if (pageData.url) {
let uri = null;
try {
let urlString = Services.urlFormatter.formatURL(pageData.url.trim());
uri = Services.io.newURI(urlString, null, null);
} catch (ex) {
Cu.reportError("Invalid LaterRun page URL " + pageData.url + " ignored.");
continue;
}
if (!uri.schemeIs("https")) {
Cu.reportError("Insecure LaterRun page URL " + uri.spec + " ignored.");
} else {
pageData.url = uri.spec;
rv.push(new Page(pageData));
}
}
}
return rv;
},
// Return a URL for display as a 'later run' page if its criteria are matched,
// or null otherwise.
// NB: will only return one page at a time; if multiple pages match, it's up
// to the preference service which one gets shown first, and the next one
// will be shown next startup instead.
getURL() {
if (!this.enabled) {
return null;
}
let pages = this.readPages();
let page = pages.find(page => page.applies(this));
if (page) {
Services.prefs.setBoolPref(page.pref + "hasRun", true);
return page.url;
}
return null;
},
};
LaterRun.init();

View file

@ -0,0 +1,194 @@
/* 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/. */
/*
* This module adjusts network priority for tabs in a way that gives 'important'
* tabs a higher priority. There are 3 levels of priority. Each is listed below
* with the priority adjustment used.
*
* Highest (-10): Selected tab in the focused window.
* Medium (0): Background tabs in the focused window.
* Selected tab in background windows.
* Lowest (+10): Background tabs in background windows.
*/
this.EXPORTED_SYMBOLS = ["trackBrowserWindow"];
const Ci = Components.interfaces;
Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
// Lazy getters
XPCOMUtils.defineLazyServiceGetter(this, "_focusManager",
"@mozilla.org/focus-manager;1",
"nsIFocusManager");
// Constants
const TAB_EVENTS = ["TabBrowserInserted", "TabSelect", "TabRemotenessChange"];
const WINDOW_EVENTS = ["activate", "unload"];
// lower value means higher priority
const PRIORITY_DELTA = Ci.nsISupportsPriority.PRIORITY_NORMAL - Ci.nsISupportsPriority.PRIORITY_LOW;
// Variables
var _lastFocusedWindow = null;
var _windows = [];
// this is used for restoring the priority after TabRemotenessChange
var _priorityBackup = new WeakMap();
// Exported symbol
this.trackBrowserWindow = function trackBrowserWindow(aWindow) {
WindowHelper.addWindow(aWindow);
}
// Global methods
function _handleEvent(aEvent) {
switch (aEvent.type) {
case "TabBrowserInserted":
BrowserHelper.onOpen(aEvent.target.linkedBrowser);
break;
case "TabSelect":
BrowserHelper.onSelect(aEvent.target.linkedBrowser);
break;
case "activate":
WindowHelper.onActivate(aEvent.target);
break;
case "TabRemotenessChange":
BrowserHelper.onRemotenessChange(aEvent.target.linkedBrowser);
break;
case "unload":
WindowHelper.removeWindow(aEvent.currentTarget);
break;
}
}
// Methods that impact a browser. Put into single object for organization.
var BrowserHelper = {
onOpen: function NP_BH_onOpen(aBrowser) {
_priorityBackup.set(aBrowser.permanentKey, Ci.nsISupportsPriority.PRIORITY_NORMAL);
// If the tab is in the focused window, leave priority as it is
if (aBrowser.ownerGlobal != _lastFocusedWindow)
this.decreasePriority(aBrowser);
},
onSelect: function NP_BH_onSelect(aBrowser) {
let windowEntry = WindowHelper.getEntry(aBrowser.ownerGlobal);
if (windowEntry.lastSelectedBrowser)
this.decreasePriority(windowEntry.lastSelectedBrowser);
this.increasePriority(aBrowser);
windowEntry.lastSelectedBrowser = aBrowser;
},
onRemotenessChange: function (aBrowser) {
aBrowser.setPriority(_priorityBackup.get(aBrowser.permanentKey));
},
increasePriority: function NP_BH_increasePriority(aBrowser) {
aBrowser.adjustPriority(PRIORITY_DELTA);
_priorityBackup.set(aBrowser.permanentKey,
_priorityBackup.get(aBrowser.permanentKey) + PRIORITY_DELTA);
},
decreasePriority: function NP_BH_decreasePriority(aBrowser) {
aBrowser.adjustPriority(PRIORITY_DELTA * -1);
_priorityBackup.set(aBrowser.permanentKey,
_priorityBackup.get(aBrowser.permanentKey) - PRIORITY_DELTA);
}
};
// Methods that impact a window. Put into single object for organization.
var WindowHelper = {
addWindow: function NP_WH_addWindow(aWindow) {
// Build internal data object
_windows.push({ window: aWindow, lastSelectedBrowser: null });
// Add event listeners
TAB_EVENTS.forEach(function(event) {
aWindow.gBrowser.tabContainer.addEventListener(event, _handleEvent, false);
});
WINDOW_EVENTS.forEach(function(event) {
aWindow.addEventListener(event, _handleEvent, false);
});
// This gets called AFTER activate event, so if this is the focused window
// we want to activate it. Otherwise, deprioritize it.
if (aWindow == _focusManager.activeWindow)
this.handleFocusedWindow(aWindow);
else
this.decreasePriority(aWindow);
// Select the selected tab
BrowserHelper.onSelect(aWindow.gBrowser.selectedBrowser);
},
removeWindow: function NP_WH_removeWindow(aWindow) {
if (aWindow == _lastFocusedWindow)
_lastFocusedWindow = null;
// Delete this window from our tracking
_windows.splice(this.getEntryIndex(aWindow), 1);
// Remove the event listeners
TAB_EVENTS.forEach(function(event) {
aWindow.gBrowser.tabContainer.removeEventListener(event, _handleEvent, false);
});
WINDOW_EVENTS.forEach(function(event) {
aWindow.removeEventListener(event, _handleEvent, false);
});
},
onActivate: function NP_WH_onActivate(aWindow, aHasFocus) {
// If this window was the last focused window, we don't need to do anything
if (aWindow == _lastFocusedWindow)
return;
// handleFocusedWindow will deprioritize the current window
this.handleFocusedWindow(aWindow);
// Lastly we should increase priority for this window
this.increasePriority(aWindow);
},
handleFocusedWindow: function NP_WH_handleFocusedWindow(aWindow) {
// If we have a last focused window, we need to deprioritize it first
if (_lastFocusedWindow)
this.decreasePriority(_lastFocusedWindow);
// aWindow is now focused
_lastFocusedWindow = aWindow;
},
// Auxiliary methods
increasePriority: function NP_WH_increasePriority(aWindow) {
aWindow.gBrowser.browsers.forEach(function(aBrowser) {
BrowserHelper.increasePriority(aBrowser);
});
},
decreasePriority: function NP_WH_decreasePriority(aWindow) {
aWindow.gBrowser.browsers.forEach(function(aBrowser) {
BrowserHelper.decreasePriority(aBrowser);
});
},
getEntry: function NP_WH_getEntry(aWindow) {
return _windows[this.getEntryIndex(aWindow)];
},
getEntryIndex: function NP_WH_getEntryAtIndex(aWindow) {
// Assumes that every object has a unique window & it's in the array
for (let i = 0; i < _windows.length; i++)
if (_windows[i].window == aWindow)
return i;
}
};

View file

@ -0,0 +1,595 @@
/* 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 = [
"PermissionUI",
];
/**
* PermissionUI is responsible for exposing both a prototype
* PermissionPrompt that can be used by arbitrary browser
* components and add-ons, but also hosts the implementations of
* built-in permission prompts.
*
* If you're developing a feature that requires web content to ask
* for special permissions from the user, this module is for you.
*
* Suppose a system add-on wants to add a new prompt for a new request
* for getting more low-level access to the user's sound card, and the
* permission request is coming up from content by way of the
* nsContentPermissionHelper. The system add-on could then do the following:
*
* Cu.import("resource://gre/modules/Integration.jsm");
* Cu.import("resource:///modules/PermissionUI.jsm");
*
* const SoundCardIntegration = (base) => ({
* __proto__: base,
* createPermissionPrompt(type, request) {
* if (type != "sound-api") {
* return super.createPermissionPrompt(...arguments);
* }
*
* return {
* __proto__: PermissionUI.PermissionPromptForRequestPrototype,
* get permissionKey() {
* return "sound-permission";
* }
* // etc - see the documentation for PermissionPrompt for
* // a better idea of what things one can and should override.
* }
* },
* });
*
* // Add-on startup:
* Integration.contentPermission.register(SoundCardIntegration);
* // ...
* // Add-on shutdown:
* Integration.contentPermission.unregister(SoundCardIntegration);
*
* Note that PermissionPromptForRequestPrototype must be used as the
* prototype, since the prompt is wrapping an nsIContentPermissionRequest,
* and going through nsIContentPermissionPrompt.
*
* It is, however, possible to take advantage of PermissionPrompt without
* having to go through nsIContentPermissionPrompt or with a
* nsIContentPermissionRequest. The PermissionPromptPrototype can be
* imported, subclassed, and have prompt() called directly, without
* the caller having called into createPermissionPrompt.
*/
const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Services",
"resource://gre/modules/Services.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils",
"resource://gre/modules/PrivateBrowsingUtils.jsm");
XPCOMUtils.defineLazyGetter(this, "gBrandBundle", function() {
return Services.strings
.createBundle('chrome://branding/locale/brand.properties');
});
XPCOMUtils.defineLazyGetter(this, "gBrowserBundle", function() {
return Services.strings
.createBundle('chrome://browser/locale/browser.properties');
});
this.PermissionUI = {};
/**
* PermissionPromptPrototype should be subclassed by callers that
* want to display prompts to the user. See each method and property
* below for guidance on what to override.
*
* Note that if you're creating a prompt for an
* nsIContentPermissionRequest, you'll want to subclass
* PermissionPromptForRequestPrototype instead.
*/
this.PermissionPromptPrototype = {
/**
* Returns the associated <xul:browser> for the request. This should
* work for the e10s and non-e10s case.
*
* Subclasses must override this.
*
* @return {<xul:browser>}
*/
get browser() {
throw new Error("Not implemented.");
},
/**
* Returns the nsIPrincipal associated with the request.
*
* Subclasses must override this.
*
* @return {nsIPrincipal}
*/
get principal() {
throw new Error("Not implemented.");
},
/**
* If the nsIPermissionManager is being queried and written
* to for this permission request, set this to the key to be
* used. If this is undefined, user permissions will not be
* read from or written to.
*
* Note that if a permission is set, in any follow-up
* prompting within the expiry window of that permission,
* the prompt will be skipped and the allow or deny choice
* will be selected automatically.
*/
get permissionKey() {
return undefined;
},
/**
* These are the options that will be passed to the
* PopupNotification when it is shown. See the documentation
* for PopupNotification for more details.
*
* Note that prompt() will automatically set displayURI to
* be the URI of the requesting pricipal, unless the displayURI is exactly
* set to false.
*/
get popupOptions() {
return {};
},
/**
* PopupNotification requires a unique ID to open the notification.
* You must return a unique ID string here, for which PopupNotification
* will then create a <xul:popupnotification> node with the ID
* "<notificationID>-notification".
*
* If there's a custom <xul:popupnotification> you're hoping to show,
* then you need to make sure its ID has the "-notification" suffix,
* and then return the prefix here.
*
* See PopupNotification.jsm for more details.
*
* @return {string}
* The unique ID that will be used to as the
* "<unique ID>-notification" ID for the <xul:popupnotification>
* to use or create.
*/
get notificationID() {
throw new Error("Not implemented.");
},
/**
* The ID of the element to anchor the PopupNotification to.
*
* @return {string}
*/
get anchorID() {
return "default-notification-icon";
},
/**
* The message to show the user in the PopupNotification. This
* is usually a string describing the permission that is being
* requested.
*
* Subclasses must override this.
*
* @return {string}
*/
get message() {
throw new Error("Not implemented.");
},
/**
* This will be called if the request is to be cancelled.
*
* Subclasses only need to override this if they provide a
* permissionKey.
*/
cancel() {
throw new Error("Not implemented.")
},
/**
* This will be called if the request is to be allowed.
*
* Subclasses only need to override this if they provide a
* permissionKey.
*/
allow() {
throw new Error("Not implemented.");
},
/**
* The actions that will be displayed in the PopupNotification
* via a dropdown menu. The first item in this array will be
* the default selection. Each action is an Object with the
* following properties:
*
* label (string):
* The label that will be displayed for this choice.
* accessKey (string):
* The access key character that will be used for this choice.
* action (Ci.nsIPermissionManager action, optional)
* The nsIPermissionManager action that will be associated with
* this choice. For example, Ci.nsIPermissionManager.DENY_ACTION.
*
* If omitted, the nsIPermissionManager will not be written to
* when this choice is chosen.
* expireType (Ci.nsIPermissionManager expiration policy, optional)
* The nsIPermissionManager expiration policy that will be associated
* with this choice. For example, Ci.nsIPermissionManager.EXPIRE_SESSION.
*
* If action is not set, expireType will be ignored.
* callback (function, optional)
* A callback function that will fire if the user makes this choice.
*/
get promptActions() {
return [];
},
/**
* If the prompt will be shown to the user, this callback will
* be called just before. Subclasses may want to override this
* in order to, for example, bump a counter Telemetry probe for
* how often a particular permission request is seen.
*/
onBeforeShow() {},
/**
* Will determine if a prompt should be shown to the user, and if so,
* will show it.
*
* If a permissionKey is defined prompt() might automatically
* allow or cancel itself based on the user's current
* permission settings without displaying the prompt.
*
* If the <xul:browser> that the request is associated with
* does not belong to a browser window with the PopupNotifications
* global set, the prompt request is ignored.
*/
prompt() {
let chromeWin = this.browser.ownerGlobal;
if (!chromeWin.PopupNotifications) {
return;
}
// We ignore requests from non-nsIStandardURLs
let requestingURI = this.principal.URI;
if (!(requestingURI instanceof Ci.nsIStandardURL)) {
return;
}
if (this.permissionKey) {
// If we're reading and setting permissions, then we need
// to check to see if we already have a permission setting
// for this particular principal.
let result =
Services.perms.testExactPermissionFromPrincipal(this.principal,
this.permissionKey);
if (result == Ci.nsIPermissionManager.DENY_ACTION) {
this.cancel();
return;
}
if (result == Ci.nsIPermissionManager.ALLOW_ACTION) {
this.allow();
return;
}
}
// Transform the PermissionPrompt actions into PopupNotification actions.
let popupNotificationActions = [];
for (let promptAction of this.promptActions) {
// Don't offer action in PB mode if the action remembers permission
// for more than a session.
if (PrivateBrowsingUtils.isWindowPrivate(chromeWin) &&
promptAction.expireType != Ci.nsIPermissionManager.EXPIRE_SESSION &&
promptAction.action) {
continue;
}
let action = {
label: promptAction.label,
accessKey: promptAction.accessKey,
callback: () => {
if (promptAction.callback) {
promptAction.callback();
}
if (this.permissionKey) {
// Remember permissions.
if (promptAction.action) {
Services.perms.addFromPrincipal(this.principal,
this.permissionKey,
promptAction.action,
promptAction.expireType);
}
// Grant permission if action is null or ALLOW_ACTION.
if (!promptAction.action ||
promptAction.action == Ci.nsIPermissionManager.ALLOW_ACTION) {
this.allow();
} else {
this.cancel();
}
}
},
};
if (promptAction.dismiss) {
action.dismiss = promptAction.dismiss
}
popupNotificationActions.push(action);
}
let mainAction = popupNotificationActions.length ?
popupNotificationActions[0] : null;
let secondaryActions = popupNotificationActions.splice(1);
let options = this.popupOptions;
if (!options.hasOwnProperty('displayURI') || options.displayURI) {
options.displayURI = this.principal.URI;
}
this.onBeforeShow();
chromeWin.PopupNotifications.show(this.browser,
this.notificationID,
this.message,
this.anchorID,
mainAction,
secondaryActions,
options);
},
};
PermissionUI.PermissionPromptPrototype = PermissionPromptPrototype;
/**
* A subclass of PermissionPromptPrototype that assumes
* that this.request is an nsIContentPermissionRequest
* and fills in some of the required properties on the
* PermissionPrompt. For callers that are wrapping an
* nsIContentPermissionRequest, this should be subclassed
* rather than PermissionPromptPrototype.
*/
this.PermissionPromptForRequestPrototype = {
__proto__: PermissionPromptPrototype,
get browser() {
// In the e10s-case, the <xul:browser> will be at request.element.
// In the single-process case, we have to use some XPCOM incantations
// to resolve to the <xul:browser>.
if (this.request.element) {
return this.request.element;
}
return this.request
.window
.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIWebNavigation)
.QueryInterface(Ci.nsIDocShell)
.chromeEventHandler;
},
get principal() {
return this.request.principal;
},
cancel() {
this.request.cancel();
},
allow() {
this.request.allow();
},
};
PermissionUI.PermissionPromptForRequestPrototype =
PermissionPromptForRequestPrototype;
/**
* Creates a PermissionPrompt for a nsIContentPermissionRequest for
* the GeoLocation API.
*
* @param request (nsIContentPermissionRequest)
* The request for a permission from content.
*/
function GeolocationPermissionPrompt(request) {
this.request = request;
}
GeolocationPermissionPrompt.prototype = {
__proto__: PermissionPromptForRequestPrototype,
get permissionKey() {
return "geo";
},
get popupOptions() {
let pref = "browser.geolocation.warning.infoURL";
return {
learnMoreURL: Services.urlFormatter.formatURLPref(pref),
};
},
get notificationID() {
return "geolocation";
},
get anchorID() {
return "geo-notification-icon";
},
get message() {
let message;
if (this.principal.URI.schemeIs("file")) {
message = gBrowserBundle.GetStringFromName("geolocation.shareWithFile2");
} else {
message = gBrowserBundle.GetStringFromName("geolocation.shareWithSite2");
}
return message;
},
get promptActions() {
// We collect Telemetry data on Geolocation prompts and how users
// respond to them. The probe keys are a bit verbose, so let's alias them.
const SHARE_LOCATION =
Ci.nsISecurityUITelemetry.WARNING_GEOLOCATION_REQUEST_SHARE_LOCATION;
const ALWAYS_SHARE =
Ci.nsISecurityUITelemetry.WARNING_GEOLOCATION_REQUEST_ALWAYS_SHARE;
const NEVER_SHARE =
Ci.nsISecurityUITelemetry.WARNING_GEOLOCATION_REQUEST_NEVER_SHARE;
let secHistogram = Services.telemetry.getHistogramById("SECURITY_UI");
let actions = [{
label: gBrowserBundle.GetStringFromName("geolocation.shareLocation"),
accessKey:
gBrowserBundle.GetStringFromName("geolocation.shareLocation.accesskey"),
action: null,
expireType: null,
callback: function() {
secHistogram.add(SHARE_LOCATION);
},
}];
if (!this.principal.URI.schemeIs("file")) {
// Always share location action.
actions.push({
label: gBrowserBundle.GetStringFromName("geolocation.alwaysShareLocation"),
accessKey:
gBrowserBundle.GetStringFromName("geolocation.alwaysShareLocation.accesskey"),
action: Ci.nsIPermissionManager.ALLOW_ACTION,
expireType: null,
callback: function() {
secHistogram.add(ALWAYS_SHARE);
},
});
// Never share location action.
actions.push({
label: gBrowserBundle.GetStringFromName("geolocation.neverShareLocation"),
accessKey:
gBrowserBundle.GetStringFromName("geolocation.neverShareLocation.accesskey"),
action: Ci.nsIPermissionManager.DENY_ACTION,
expireType: null,
callback: function() {
secHistogram.add(NEVER_SHARE);
},
});
}
return actions;
},
onBeforeShow() {
let secHistogram = Services.telemetry.getHistogramById("SECURITY_UI");
const SHOW_REQUEST = Ci.nsISecurityUITelemetry.WARNING_GEOLOCATION_REQUEST;
secHistogram.add(SHOW_REQUEST);
},
};
PermissionUI.GeolocationPermissionPrompt = GeolocationPermissionPrompt;
/**
* Creates a PermissionPrompt for a nsIContentPermissionRequest for
* the Desktop Notification API.
*
* @param request (nsIContentPermissionRequest)
* The request for a permission from content.
* @return {PermissionPrompt} (see documentation in header)
*/
function DesktopNotificationPermissionPrompt(request) {
this.request = request;
}
DesktopNotificationPermissionPrompt.prototype = {
__proto__: PermissionPromptForRequestPrototype,
get permissionKey() {
return "desktop-notification";
},
get popupOptions() {
let learnMoreURL =
Services.urlFormatter.formatURLPref("app.support.baseURL") + "push";
// The eventCallback is bound to the Notification that's being
// shown. We'll stash a reference to this in the closure so that
// the request can be cancelled.
let prompt = this;
let eventCallback = function(type) {
if (type == "dismissed") {
// Bug 1259148: Hide the doorhanger icon. Unlike other permission
// doorhangers, the user can't restore the doorhanger using the icon
// in the location bar. Instead, the site will be notified that the
// doorhanger was dismissed.
this.remove();
prompt.request.cancel();
}
};
return {
learnMoreURL,
eventCallback,
};
},
get notificationID() {
return "web-notifications";
},
get anchorID() {
return "web-notifications-notification-icon";
},
get message() {
return gBrowserBundle.GetStringFromName("webNotifications.receiveFromSite");
},
get promptActions() {
let promptActions;
// Only show "allow for session" in PB mode, we don't
// support "allow for session" in non-PB mode.
if (PrivateBrowsingUtils.isBrowserPrivate(this.browser)) {
promptActions = [
{
label: gBrowserBundle.GetStringFromName("webNotifications.receiveForSession"),
accessKey:
gBrowserBundle.GetStringFromName("webNotifications.receiveForSession.accesskey"),
action: Ci.nsIPermissionManager.ALLOW_ACTION,
expireType: Ci.nsIPermissionManager.EXPIRE_SESSION,
}
];
} else {
promptActions = [
{
label: gBrowserBundle.GetStringFromName("webNotifications.alwaysReceive"),
accessKey:
gBrowserBundle.GetStringFromName("webNotifications.alwaysReceive.accesskey"),
action: Ci.nsIPermissionManager.ALLOW_ACTION,
expireType: null,
},
{
label: gBrowserBundle.GetStringFromName("webNotifications.neverShow"),
accessKey:
gBrowserBundle.GetStringFromName("webNotifications.neverShow.accesskey"),
action: Ci.nsIPermissionManager.DENY_ACTION,
expireType: null,
},
];
}
return promptActions;
},
};
PermissionUI.DesktopNotificationPermissionPrompt =
DesktopNotificationPermissionPrompt;

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,397 @@
/* -*- mode: js; indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
var Cc = Components.classes;
var Ci = Components.interfaces;
var Cu = Components.utils;
this.EXPORTED_SYMBOLS = ["ProcessHangMonitor"];
Cu.import("resource://gre/modules/AppConstants.jsm");
Cu.import("resource://gre/modules/Services.jsm");
/**
* This JSM is responsible for observing content process hang reports
* and asking the user what to do about them. See nsIHangReport for
* the platform interface.
*/
var ProcessHangMonitor = {
/**
* This timeout is the wait period applied after a user selects "Wait" in
* an existing notification.
*/
get WAIT_EXPIRATION_TIME() {
try {
return Services.prefs.getIntPref("browser.hangNotification.waitPeriod");
} catch (ex) {
return 10000;
}
},
/**
* Collection of hang reports that haven't expired or been dismissed
* by the user. These are nsIHangReports.
*/
_activeReports: new Set(),
/**
* Collection of hang reports that have been suppressed for a short
* period of time. Value is an nsITimer for when the wait time
* expires.
*/
_pausedReports: new Map(),
/**
* Initialize hang reporting. Called once in the parent process.
*/
init: function() {
Services.obs.addObserver(this, "process-hang-report", false);
Services.obs.addObserver(this, "clear-hang-report", false);
Services.obs.addObserver(this, "xpcom-shutdown", false);
Services.ww.registerNotification(this);
},
/**
* Terminate JavaScript associated with the hang being reported for
* the selected browser in |win|.
*/
terminateScript: function(win) {
this.handleUserInput(win, report => report.terminateScript());
},
/**
* Start devtools debugger for JavaScript associated with the hang
* being reported for the selected browser in |win|.
*/
debugScript: function(win) {
this.handleUserInput(win, report => {
function callback() {
report.endStartingDebugger();
}
report.beginStartingDebugger();
let svc = Cc["@mozilla.org/dom/slow-script-debug;1"].getService(Ci.nsISlowScriptDebug);
let handler = svc.remoteActivationHandler;
handler.handleSlowScriptDebug(report.scriptBrowser, callback);
});
},
/**
* Terminate the plugin process associated with a hang being reported
* for the selected browser in |win|. Will attempt to generate a combined
* crash report for all processes.
*/
terminatePlugin: function(win) {
this.handleUserInput(win, report => report.terminatePlugin());
},
/**
* Dismiss the browser notification and invoke an appropriate action based on
* the hang type.
*/
stopIt: function (win) {
let report = this.findActiveReport(win.gBrowser.selectedBrowser);
if (!report) {
return;
}
switch (report.hangType) {
case report.SLOW_SCRIPT:
this.terminateScript(win);
break;
case report.PLUGIN_HANG:
this.terminatePlugin(win);
break;
}
},
/**
* Dismiss the notification, clear the report from the active list and set up
* a new timer to track a wait period during which we won't notify.
*/
waitLonger: function(win) {
let report = this.findActiveReport(win.gBrowser.selectedBrowser);
if (!report) {
return;
}
// Remove the report from the active list.
this.removeActiveReport(report);
// NOTE, we didn't call userCanceled on nsIHangReport here. This insures
// we don't repeatedly generate and cache crash report data for this hang
// in the process hang reporter. It already has one report for the browser
// process we want it hold onto.
// Create a new wait timer with notify callback
let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
timer.initWithCallback(() => {
for (let [stashedReport, otherTimer] of this._pausedReports) {
if (otherTimer === timer) {
this.removePausedReport(stashedReport);
// We're still hung, so move the report back to the active
// list and update the UI.
this._activeReports.add(report);
this.updateWindows();
break;
}
}
}, this.WAIT_EXPIRATION_TIME, timer.TYPE_ONE_SHOT);
this._pausedReports.set(report, timer);
// remove the browser notification associated with this hang
this.updateWindows();
},
/**
* If there is a hang report associated with the selected browser in
* |win|, invoke |func| on that report and stop notifying the user
* about it.
*/
handleUserInput: function(win, func) {
let report = this.findActiveReport(win.gBrowser.selectedBrowser);
if (!report) {
return null;
}
this.removeActiveReport(report);
return func(report);
},
observe: function(subject, topic, data) {
switch (topic) {
case "xpcom-shutdown":
Services.obs.removeObserver(this, "xpcom-shutdown");
Services.obs.removeObserver(this, "process-hang-report");
Services.obs.removeObserver(this, "clear-hang-report");
Services.ww.unregisterNotification(this);
break;
case "process-hang-report":
this.reportHang(subject.QueryInterface(Ci.nsIHangReport));
break;
case "clear-hang-report":
this.clearHang(subject.QueryInterface(Ci.nsIHangReport));
break;
case "domwindowopened":
// Install event listeners on the new window in case one of
// its tabs is already hung.
let win = subject.QueryInterface(Ci.nsIDOMWindow);
let listener = (ev) => {
win.removeEventListener("load", listener, true);
this.updateWindows();
};
win.addEventListener("load", listener, true);
break;
}
},
/**
* Find a active hang report for the given <browser> element.
*/
findActiveReport: function(browser) {
let frameLoader = browser.QueryInterface(Ci.nsIFrameLoaderOwner).frameLoader;
for (let report of this._activeReports) {
if (report.isReportForBrowser(frameLoader)) {
return report;
}
}
return null;
},
/**
* Find a paused hang report for the given <browser> element.
*/
findPausedReport: function(browser) {
let frameLoader = browser.QueryInterface(Ci.nsIFrameLoaderOwner).frameLoader;
for (let [report, ] of this._pausedReports) {
if (report.isReportForBrowser(frameLoader)) {
return report;
}
}
return null;
},
/**
* Remove an active hang report from the active list and cancel the timer
* associated with it.
*/
removeActiveReport: function(report) {
this._activeReports.delete(report);
this.updateWindows();
},
/**
* Remove a paused hang report from the paused list and cancel the timer
* associated with it.
*/
removePausedReport: function(report) {
let timer = this._pausedReports.get(report);
if (timer) {
timer.cancel();
}
this._pausedReports.delete(report);
},
/**
* Iterate over all XUL windows and ensure that the proper hang
* reports are shown for each one. Also install event handlers in
* each window to watch for events that would cause a different hang
* report to be displayed.
*/
updateWindows: function() {
let e = Services.wm.getEnumerator("navigator:browser");
while (e.hasMoreElements()) {
let win = e.getNext();
this.updateWindow(win);
// Only listen for these events if there are active hang reports.
if (this._activeReports.size) {
this.trackWindow(win);
} else {
this.untrackWindow(win);
}
}
},
/**
* If there is a hang report for the current tab in |win|, display it.
*/
updateWindow: function(win) {
let report = this.findActiveReport(win.gBrowser.selectedBrowser);
if (report) {
this.showNotification(win, report);
} else {
this.hideNotification(win);
}
},
/**
* Show the notification for a hang.
*/
showNotification: function(win, report) {
let nb = win.document.getElementById("high-priority-global-notificationbox");
let notification = nb.getNotificationWithValue("process-hang");
if (notification) {
return;
}
let bundle = win.gNavigatorBundle;
let buttons = [{
label: bundle.getString("processHang.button_stop.label"),
accessKey: bundle.getString("processHang.button_stop.accessKey"),
callback: function() {
ProcessHangMonitor.stopIt(win);
}
},
{
label: bundle.getString("processHang.button_wait.label"),
accessKey: bundle.getString("processHang.button_wait.accessKey"),
callback: function() {
ProcessHangMonitor.waitLonger(win);
}
}];
if (AppConstants.MOZ_DEV_EDITION && report.hangType == report.SLOW_SCRIPT) {
buttons.push({
label: bundle.getString("processHang.button_debug.label"),
accessKey: bundle.getString("processHang.button_debug.accessKey"),
callback: function() {
ProcessHangMonitor.debugScript(win);
}
});
}
nb.appendNotification(bundle.getString("processHang.label"),
"process-hang",
"chrome://browser/content/aboutRobots-icon.png",
nb.PRIORITY_WARNING_HIGH, buttons);
},
/**
* Ensure that no hang notifications are visible in |win|.
*/
hideNotification: function(win) {
let nb = win.document.getElementById("high-priority-global-notificationbox");
let notification = nb.getNotificationWithValue("process-hang");
if (notification) {
nb.removeNotification(notification);
}
},
/**
* Install event handlers on |win| to watch for events that would
* cause a different hang report to be displayed.
*/
trackWindow: function(win) {
win.gBrowser.tabContainer.addEventListener("TabSelect", this, true);
win.gBrowser.tabContainer.addEventListener("TabRemotenessChange", this, true);
},
untrackWindow: function(win) {
win.gBrowser.tabContainer.removeEventListener("TabSelect", this, true);
win.gBrowser.tabContainer.removeEventListener("TabRemotenessChange", this, true);
},
handleEvent: function(event) {
let win = event.target.ownerGlobal;
// If a new tab is selected or if a tab changes remoteness, then
// we may need to show or hide a hang notification.
if (event.type == "TabSelect" || event.type == "TabRemotenessChange") {
this.updateWindow(win);
}
},
/**
* Handle a potentially new hang report. If it hasn't been seen
* before, show a notification for it in all open XUL windows.
*/
reportHang: function(report) {
// If this hang was already reported reset the timer for it.
if (this._activeReports.has(report)) {
// if this report is in active but doesn't have a notification associated
// with it, display a notification.
this.updateWindows();
return;
}
// If this hang was already reported and paused by the user ignore it.
if (this._pausedReports.has(report)) {
return;
}
// On e10s this counts slow-script/hanged-plugin notice only once.
// This code is not reached on non-e10s.
if (report.hangType == report.SLOW_SCRIPT) {
// On non-e10s, SLOW_SCRIPT_NOTICE_COUNT is probed at nsGlobalWindow.cpp
Services.telemetry.getHistogramById("SLOW_SCRIPT_NOTICE_COUNT").add();
} else if (report.hangType == report.PLUGIN_HANG) {
// On non-e10s we have sufficient plugin telemetry probes,
// so PLUGIN_HANG_NOTICE_COUNT is only probed on e10s.
Services.telemetry.getHistogramById("PLUGIN_HANG_NOTICE_COUNT").add();
}
this._activeReports.add(report);
this.updateWindows();
},
clearHang: function(report) {
this.removeActiveReport(report);
this.removePausedReport(report);
report.userCanceled();
},
};

View file

@ -0,0 +1,102 @@
// -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
this.EXPORTED_SYMBOLS = [ "ReaderParent" ];
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/Services.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils", "resource://gre/modules/PlacesUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "ReaderMode", "resource://gre/modules/ReaderMode.jsm");
const gStringBundle = Services.strings.createBundle("chrome://global/locale/aboutReader.properties");
var ReaderParent = {
MESSAGES: [
"Reader:UpdateReaderButton",
],
init() {
let mm = Cc["@mozilla.org/globalmessagemanager;1"].getService(Ci.nsIMessageListenerManager);
for (let msg of this.MESSAGES) {
mm.addMessageListener(msg, this);
}
},
receiveMessage(message) {
switch (message.name) {
case "Reader:UpdateReaderButton": {
let browser = message.target;
if (message.data && message.data.isArticle !== undefined) {
browser.isArticle = message.data.isArticle;
}
this.updateReaderButton(browser);
break;
}
}
},
updateReaderButton(browser) {
let win = browser.ownerGlobal;
if (browser != win.gBrowser.selectedBrowser) {
return;
}
let button = win.document.getElementById("reader-mode-button");
let command = win.document.getElementById("View:ReaderView");
let key = win.document.getElementById("key_toggleReaderMode");
// aria-reader is not a real ARIA attribute. However, this will cause
// Gecko accessibility to expose the "reader" object attribute. We do this
// so that the reader state is easy for accessibility clients to access
// programmatically.
if (browser.currentURI.spec.startsWith("about:reader")) {
button.setAttribute("readeractive", true);
button.hidden = false;
let closeText = gStringBundle.GetStringFromName("readerView.close");
button.setAttribute("tooltiptext", closeText);
command.setAttribute("label", closeText);
command.setAttribute("hidden", false);
command.setAttribute("accesskey", gStringBundle.GetStringFromName("readerView.close.accesskey"));
key.setAttribute("disabled", false);
browser.setAttribute("aria-reader", "active");
} else {
button.removeAttribute("readeractive");
button.hidden = !browser.isArticle;
let enterText = gStringBundle.GetStringFromName("readerView.enter");
button.setAttribute("tooltiptext", enterText);
command.setAttribute("label", enterText);
command.setAttribute("hidden", !browser.isArticle);
command.setAttribute("accesskey", gStringBundle.GetStringFromName("readerView.enter.accesskey"));
key.setAttribute("disabled", !browser.isArticle);
if (browser.isArticle) {
browser.setAttribute("aria-reader", "available");
} else {
browser.removeAttribute("aria-reader");
}
}
},
forceShowReaderIcon(browser) {
browser.isArticle = true;
this.updateReaderButton(browser);
},
buttonClick(event) {
if (event.button != 0) {
return;
}
this.toggleReaderMode(event);
},
toggleReaderMode(event) {
let win = event.target.ownerGlobal;
let browser = win.gBrowser.selectedBrowser;
browser.messageManager.sendAsyncMessage("Reader:ToggleReaderMode");
}
};

View file

@ -0,0 +1,67 @@
/* 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 = ["RecentWindow"];
const Cu = Components.utils;
Cu.import("resource://gre/modules/AppConstants.jsm");
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/PrivateBrowsingUtils.jsm");
this.RecentWindow = {
/*
* Get the most recent browser window.
*
* @param aOptions an object accepting the arguments for the search.
* * private: true to restrict the search to private windows
* only, false to restrict the search to non-private only.
* Omit the property to search in both groups.
* * allowPopups: true if popup windows are permissable.
*/
getMostRecentBrowserWindow: function RW_getMostRecentBrowserWindow(aOptions) {
let checkPrivacy = typeof aOptions == "object" &&
"private" in aOptions;
let allowPopups = typeof aOptions == "object" && !!aOptions.allowPopups;
function isSuitableBrowserWindow(win) {
return (!win.closed &&
(allowPopups || win.toolbar.visible) &&
(!checkPrivacy ||
PrivateBrowsingUtils.permanentPrivateBrowsing ||
PrivateBrowsingUtils.isWindowPrivate(win) == aOptions.private));
}
let broken_wm_z_order =
AppConstants.platform != "macosx" && AppConstants.platform != "win";
if (broken_wm_z_order) {
let win = Services.wm.getMostRecentWindow("navigator:browser");
// if we're lucky, this isn't a popup, and we can just return this
if (win && !isSuitableBrowserWindow(win)) {
win = null;
let windowList = Services.wm.getEnumerator("navigator:browser");
// this is oldest to newest, so this gets a bit ugly
while (windowList.hasMoreElements()) {
let nextWin = windowList.getNext();
if (isSuitableBrowserWindow(nextWin))
win = nextWin;
}
}
return win;
}
let windowList = Services.wm.getZOrderDOMWindowEnumerator("navigator:browser", true);
while (windowList.hasMoreElements()) {
let win = windowList.getNext();
if (isSuitableBrowserWindow(win))
return win;
}
return null;
}
};

View file

@ -0,0 +1,110 @@
/* vim: set ts=2 sw=2 et tw=80: */
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
var Cc = Components.classes;
var Ci = Components.interfaces;
var Cu = Components.utils;
this.EXPORTED_SYMBOLS = [ "RemotePrompt" ];
Cu.import("resource:///modules/PlacesUIUtils.jsm");
Cu.import("resource://gre/modules/PrivateBrowsingUtils.jsm");
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/SharedPromptUtils.jsm");
var RemotePrompt = {
init: function() {
let mm = Cc["@mozilla.org/globalmessagemanager;1"].getService(Ci.nsIMessageListenerManager);
mm.addMessageListener("Prompt:Open", this);
},
receiveMessage: function(message) {
switch (message.name) {
case "Prompt:Open":
if (message.data.uri) {
this.openModalWindow(message.data, message.target);
} else {
this.openTabPrompt(message.data, message.target)
}
break;
}
},
openTabPrompt: function(args, browser) {
let window = browser.ownerGlobal;
let tabPrompt = window.gBrowser.getTabModalPromptBox(browser)
let newPrompt;
let needRemove = false;
let promptId = args._remoteId;
function onPromptClose(forceCleanup) {
// It's possible that we removed the prompt during the
// appendPrompt call below. In that case, newPrompt will be
// undefined. We set the needRemove flag to remember to remove
// it right after we've finished adding it.
if (newPrompt)
tabPrompt.removePrompt(newPrompt);
else
needRemove = true;
PromptUtils.fireDialogEvent(window, "DOMModalDialogClosed", browser);
browser.messageManager.sendAsyncMessage("Prompt:Close", args);
}
browser.messageManager.addMessageListener("Prompt:ForceClose", function listener(message) {
// If this was for another prompt in the same tab, ignore it.
if (message.data._remoteId !== promptId) {
return;
}
browser.messageManager.removeMessageListener("Prompt:ForceClose", listener);
if (newPrompt) {
newPrompt.abortPrompt();
}
});
try {
let eventDetail = {
tabPrompt: true,
promptPrincipal: args.promptPrincipal,
inPermitUnload: args.inPermitUnload,
};
PromptUtils.fireDialogEvent(window, "DOMWillOpenModalDialog", browser, eventDetail);
args.promptActive = true;
newPrompt = tabPrompt.appendPrompt(args, onPromptClose);
if (needRemove) {
tabPrompt.removePrompt(newPrompt);
}
// TODO since we don't actually open a window, need to check if
// there's other stuff in nsWindowWatcher::OpenWindowInternal
// that we might need to do here as well.
} catch (ex) {
onPromptClose(true);
}
},
openModalWindow: function(args, browser) {
let window = browser.ownerGlobal;
try {
PromptUtils.fireDialogEvent(window, "DOMWillOpenModalDialog", browser);
let bag = PromptUtils.objectToPropBag(args);
Services.ww.openWindow(window, args.uri, "_blank",
"centerscreen,chrome,modal,titlebar", bag);
PromptUtils.propBagToObject(bag, args);
} finally {
PromptUtils.fireDialogEvent(window, "DOMModalDialogClosed", browser);
browser.messageManager.sendAsyncMessage("Prompt:Close", args);
}
}
};

View file

@ -0,0 +1,22 @@
/* 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";
//
// A shared module for sanitize.js
//
// Until bug 1167238 lands, this serves only as a way to ensure that
// sanitize is loaded from its own compartment, rather than from that
// of the sanitize dialog.
//
this.EXPORTED_SYMBOLS = ["Sanitizer"];
const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
var scope = {};
Cc["@mozilla.org/moz/jssubscript-loader;1"].getService(Ci.mozIJSSubScriptLoader)
.loadSubScript("chrome://browser/content/sanitize.js", scope);
this.Sanitizer = scope.Sanitizer;

View file

@ -0,0 +1,269 @@
/* 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/. */
this.EXPORTED_SYMBOLS = [ "SitePermissions" ];
Components.utils.import("resource://gre/modules/Services.jsm");
var gStringBundle =
Services.strings.createBundle("chrome://browser/locale/sitePermissions.properties");
this.SitePermissions = {
UNKNOWN: Services.perms.UNKNOWN_ACTION,
ALLOW: Services.perms.ALLOW_ACTION,
BLOCK: Services.perms.DENY_ACTION,
SESSION: Components.interfaces.nsICookiePermission.ACCESS_SESSION,
/* Returns all custom permissions for a given URI, the return
* type is a list of objects with the keys:
* - id: the permissionId of the permission
* - state: a constant representing the current permission state
* (e.g. SitePermissions.ALLOW)
*
* To receive a more detailed, albeit less performant listing see
* SitePermissions.getPermissionDetailsByURI().
*
* install addon permission is excluded, check bug 1303108
*/
getAllByURI: function (aURI) {
let result = [];
if (!this.isSupportedURI(aURI)) {
return result;
}
let permissions = Services.perms.getAllForURI(aURI);
while (permissions.hasMoreElements()) {
let permission = permissions.getNext();
// filter out unknown permissions
if (gPermissionObject[permission.type]) {
// XXX Bug 1303108 - Control Center should only show non-default permissions
if (permission.type == "install") {
continue;
}
result.push({
id: permission.type,
state: permission.capability,
});
}
}
return result;
},
/* Returns an object representing the aId permission. It contains the
* following keys:
* - id: the permissionID of the permission
* - label: the localized label
* - state: a constant representing the aState permission state
* (e.g. SitePermissions.ALLOW), or the default if aState is omitted
* - availableStates: an array of all available states for that permission,
* represented as objects with the keys:
* - id: the state constant
* - label: the translated label of that state
*/
getPermissionItem: function (aId, aState) {
let availableStates = this.getAvailableStates(aId).map(state => {
return { id: state, label: this.getStateLabel(aId, state) };
});
if (aState == undefined)
aState = this.getDefault(aId);
return {id: aId, label: this.getPermissionLabel(aId),
state: aState, availableStates};
},
/* Returns a list of objects representing all permissions that are currently
* set for the given URI. See getPermissionItem for the content of each object.
*/
getPermissionDetailsByURI: function (aURI) {
let permissions = [];
for (let {state, id} of this.getAllByURI(aURI)) {
permissions.push(this.getPermissionItem(id, state));
}
return permissions;
},
/* Checks whether a UI for managing permissions should be exposed for a given
* URI. This excludes file URIs, for instance, as they don't have a host,
* even though nsIPermissionManager can still handle them.
*/
isSupportedURI: function (aURI) {
return aURI.schemeIs("http") || aURI.schemeIs("https");
},
/* Returns an array of all permission IDs.
*/
listPermissions: function () {
return Object.keys(gPermissionObject);
},
/* Returns an array of permission states to be exposed to the user for a
* permission with the given ID.
*/
getAvailableStates: function (aPermissionID) {
if (aPermissionID in gPermissionObject &&
gPermissionObject[aPermissionID].states)
return gPermissionObject[aPermissionID].states;
if (this.getDefault(aPermissionID) == this.UNKNOWN)
return [ SitePermissions.UNKNOWN, SitePermissions.ALLOW, SitePermissions.BLOCK ];
return [ SitePermissions.ALLOW, SitePermissions.BLOCK ];
},
/* Returns the default state of a particular permission.
*/
getDefault: function (aPermissionID) {
if (aPermissionID in gPermissionObject &&
gPermissionObject[aPermissionID].getDefault)
return gPermissionObject[aPermissionID].getDefault();
return this.UNKNOWN;
},
/* Returns the state of a particular permission for a given URI.
*/
get: function (aURI, aPermissionID) {
if (!this.isSupportedURI(aURI))
return this.UNKNOWN;
let state;
if (aPermissionID in gPermissionObject &&
gPermissionObject[aPermissionID].exactHostMatch)
state = Services.perms.testExactPermission(aURI, aPermissionID);
else
state = Services.perms.testPermission(aURI, aPermissionID);
return state;
},
/* Sets the state of a particular permission for a given URI.
*/
set: function (aURI, aPermissionID, aState) {
if (!this.isSupportedURI(aURI))
return;
if (aState == this.UNKNOWN) {
this.remove(aURI, aPermissionID);
return;
}
Services.perms.add(aURI, aPermissionID, aState);
},
/* Removes the saved state of a particular permission for a given URI.
*/
remove: function (aURI, aPermissionID) {
if (!this.isSupportedURI(aURI))
return;
Services.perms.remove(aURI, aPermissionID);
},
/* Returns the localized label for the permission with the given ID, to be
* used in a UI for managing permissions.
*/
getPermissionLabel: function (aPermissionID) {
let labelID = gPermissionObject[aPermissionID].labelID || aPermissionID;
return gStringBundle.GetStringFromName("permission." + labelID + ".label");
},
/* Returns the localized label for the given permission state, to be used in
* a UI for managing permissions.
*/
getStateLabel: function (aPermissionID, aState, aInUse = false) {
switch (aState) {
case this.UNKNOWN:
if (aInUse)
return gStringBundle.GetStringFromName("allowTemporarily");
return gStringBundle.GetStringFromName("alwaysAsk");
case this.ALLOW:
return gStringBundle.GetStringFromName("allow");
case this.SESSION:
return gStringBundle.GetStringFromName("allowForSession");
case this.BLOCK:
return gStringBundle.GetStringFromName("block");
default:
return null;
}
}
};
var gPermissionObject = {
/* Holds permission ID => options pairs.
*
* Supported options:
*
* - exactHostMatch
* Allows sub domains to have their own permissions.
* Defaults to false.
*
* - getDefault
* Called to get the permission's default state.
* Defaults to UNKNOWN, indicating that the user will be asked each time
* a page asks for that permissions.
*
* - labelID
* Use the given ID instead of the permission name for looking up strings.
* e.g. "desktop-notification2" to use permission.desktop-notification2.label
*
* - states
* Array of permission states to be exposed to the user.
* Defaults to ALLOW, BLOCK and the default state (see getDefault).
*/
"image": {
getDefault: function () {
return Services.prefs.getIntPref("permissions.default.image") == 2 ?
SitePermissions.BLOCK : SitePermissions.ALLOW;
}
},
"cookie": {
states: [ SitePermissions.ALLOW, SitePermissions.SESSION, SitePermissions.BLOCK ],
getDefault: function () {
if (Services.prefs.getIntPref("network.cookie.cookieBehavior") == 2)
return SitePermissions.BLOCK;
if (Services.prefs.getIntPref("network.cookie.lifetimePolicy") == 2)
return SitePermissions.SESSION;
return SitePermissions.ALLOW;
}
},
"desktop-notification": {
exactHostMatch: true,
labelID: "desktop-notification2",
},
"camera": {},
"microphone": {},
"screen": {
states: [ SitePermissions.UNKNOWN, SitePermissions.BLOCK ],
},
"popup": {
getDefault: function () {
return Services.prefs.getBoolPref("dom.disable_open_during_load") ?
SitePermissions.BLOCK : SitePermissions.ALLOW;
}
},
"install": {
getDefault: function () {
return Services.prefs.getBoolPref("xpinstall.whitelist.required") ?
SitePermissions.BLOCK : SitePermissions.ALLOW;
}
},
"geo": {
exactHostMatch: true
},
"indexedDB": {}
};
const kPermissionIDs = Object.keys(gPermissionObject);

View file

@ -0,0 +1,24 @@
/* 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 = ["TransientPrefs"];
Components.utils.import("resource://gre/modules/Preferences.jsm");
var prefVisibility = new Map;
/* Use for preferences that should only be visible when they've been modified.
When reset to their default state, they remain visible until restarting the
application. */
this.TransientPrefs = {
prefShouldBeVisible: function (prefName) {
if (Preferences.isSet(prefName))
prefVisibility.set(prefName, true);
return !!prefVisibility.get(prefName);
}
};

View file

@ -0,0 +1,51 @@
// -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
this.EXPORTED_SYMBOLS = [ "URLBarZoom" ];
Components.utils.import("resource://gre/modules/Services.jsm");
var URLBarZoom = {
init: function(aWindow) {
// Register ourselves with the service so we know when the zoom prefs change.
Services.obs.addObserver(updateZoomButton, "browser-fullZoom:zoomChange", false);
Services.obs.addObserver(updateZoomButton, "browser-fullZoom:zoomReset", false);
Services.obs.addObserver(updateZoomButton, "browser-fullZoom:location-change", false);
},
}
function updateZoomButton(aSubject, aTopic) {
let win = aSubject.ownerDocument.defaultView;
let customizableZoomControls = win.document.getElementById("zoom-controls");
let zoomResetButton = win.document.getElementById("urlbar-zoom-button");
let zoomFactor = Math.round(win.ZoomManager.zoom * 100);
// Ensure that zoom controls haven't already been added to browser in Customize Mode
if (customizableZoomControls &&
customizableZoomControls.getAttribute("cui-areatype") == "toolbar") {
zoomResetButton.hidden = true;
return;
}
if (zoomFactor != 100) {
// Check if zoom button is visible and update label if it is
if (zoomResetButton.hidden) {
zoomResetButton.hidden = false;
}
// Only allow pulse animation for zoom changes, not tab switching
if (aTopic != "browser-fullZoom:location-change") {
zoomResetButton.setAttribute("animate", "true");
} else {
zoomResetButton.removeAttribute("animate");
}
zoomResetButton.setAttribute("label",
win.gNavigatorBundle.getFormattedString("urlbar-zoom-button.label", [zoomFactor]));
// Hide button if zoom is at 100%
} else {
zoomResetButton.hidden = true;
}
}

View file

@ -0,0 +1,53 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
const {interfaces: Ci, utils: Cu} = Components;
this.EXPORTED_SYMBOLS = ["Windows8WindowFrameColor"];
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
var Registry = Cu.import("resource://gre/modules/WindowsRegistry.jsm").WindowsRegistry;
var Windows8WindowFrameColor = {
_windowFrameColor: null,
get: function() {
if (this._windowFrameColor)
return this._windowFrameColor;
const HKCU = Ci.nsIWindowsRegKey.ROOT_KEY_CURRENT_USER;
const dwmKey = "Software\\Microsoft\\Windows\\DWM";
let customizationColor = Registry.readRegKey(HKCU, dwmKey,
"ColorizationColor");
if (customizationColor == undefined) {
// Seems to be the default color (hardcoded because of bug 1065998)
return [158, 158, 158];
}
// The color returned from the Registry is in decimal form.
let customizationColorHex = customizationColor.toString(16);
// Zero-pad the number just to make sure that it is 8 digits.
customizationColorHex = ("00000000" + customizationColorHex).substr(-8);
let customizationColorArray = customizationColorHex.match(/../g);
let [, fgR, fgG, fgB] = customizationColorArray.map(val => parseInt(val, 16));
let colorizationColorBalance = Registry.readRegKey(HKCU, dwmKey,
"ColorizationColorBalance");
if (colorizationColorBalance == undefined) {
colorizationColorBalance = 78;
}
// Window frame base color when Color Intensity is at 0, see bug 1004576.
let frameBaseColor = 217;
let alpha = colorizationColorBalance / 100;
// Alpha-blend the foreground color with the frame base color.
let r = Math.round(fgR * alpha + frameBaseColor * (1 - alpha));
let g = Math.round(fgG * alpha + frameBaseColor * (1 - alpha));
let b = Math.round(fgB * alpha + frameBaseColor * (1 - alpha));
return this._windowFrameColor = [r, g, b];
},
};

View file

@ -0,0 +1,579 @@
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
Components.utils.import("resource://gre/modules/Services.jsm");
/**
* Constants
*/
const Cc = Components.classes;
const Ci = Components.interfaces;
// Stop updating jumplists after some idle time.
const IDLE_TIMEOUT_SECONDS = 5 * 60;
// Prefs
const PREF_TASKBAR_BRANCH = "browser.taskbar.lists.";
const PREF_TASKBAR_ENABLED = "enabled";
const PREF_TASKBAR_ITEMCOUNT = "maxListItemCount";
const PREF_TASKBAR_FREQUENT = "frequent.enabled";
const PREF_TASKBAR_RECENT = "recent.enabled";
const PREF_TASKBAR_TASKS = "tasks.enabled";
const PREF_TASKBAR_REFRESH = "refreshInSeconds";
// Hash keys for pendingStatements.
const LIST_TYPE = {
FREQUENT: 0
, RECENT: 1
}
/**
* Exports
*/
this.EXPORTED_SYMBOLS = [
"WinTaskbarJumpList",
];
/**
* Smart getters
*/
XPCOMUtils.defineLazyGetter(this, "_prefs", function() {
return Services.prefs.getBranch(PREF_TASKBAR_BRANCH);
});
XPCOMUtils.defineLazyGetter(this, "_stringBundle", function() {
return Services.strings
.createBundle("chrome://browser/locale/taskbar.properties");
});
XPCOMUtils.defineLazyGetter(this, "PlacesUtils", function() {
Components.utils.import("resource://gre/modules/PlacesUtils.jsm");
return PlacesUtils;
});
XPCOMUtils.defineLazyGetter(this, "NetUtil", function() {
Components.utils.import("resource://gre/modules/NetUtil.jsm");
return NetUtil;
});
XPCOMUtils.defineLazyServiceGetter(this, "_idle",
"@mozilla.org/widget/idleservice;1",
"nsIIdleService");
XPCOMUtils.defineLazyServiceGetter(this, "_taskbarService",
"@mozilla.org/windows-taskbar;1",
"nsIWinTaskbar");
XPCOMUtils.defineLazyServiceGetter(this, "_winShellService",
"@mozilla.org/browser/shell-service;1",
"nsIWindowsShellService");
XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils",
"resource://gre/modules/PrivateBrowsingUtils.jsm");
/**
* Global functions
*/
function _getString(name) {
return _stringBundle.GetStringFromName(name);
}
// Task list configuration data object.
var tasksCfg = [
/**
* Task configuration options: title, description, args, iconIndex, open, close.
*
* title - Task title displayed in the list. (strings in the table are temp fillers.)
* description - Tooltip description on the list item.
* args - Command line args to invoke the task.
* iconIndex - Optional win icon index into the main application for the
* list item.
* open - Boolean indicates if the command should be visible after the browser opens.
* close - Boolean indicates if the command should be visible after the browser closes.
*/
// Open new tab
{
get title() { return _getString("taskbar.tasks.newTab.label"); },
get description() { return _getString("taskbar.tasks.newTab.description"); },
args: "-new-tab about:blank",
iconIndex: 3, // New window icon
open: true,
close: true, // The jump list already has an app launch icon, but
// we don't always update the list on shutdown.
// Thus true for consistency.
},
// Open new window
{
get title() { return _getString("taskbar.tasks.newWindow.label"); },
get description() { return _getString("taskbar.tasks.newWindow.description"); },
args: "-browser",
iconIndex: 2, // New tab icon
open: true,
close: true, // No point, but we don't always update the list on
// shutdown. Thus true for consistency.
},
// Open new private window
{
get title() { return _getString("taskbar.tasks.newPrivateWindow.label"); },
get description() { return _getString("taskbar.tasks.newPrivateWindow.description"); },
args: "-private-window",
iconIndex: 4, // Private browsing mode icon
open: true,
close: true, // No point, but we don't always update the list on
// shutdown. Thus true for consistency.
},
];
// Implementation
this.WinTaskbarJumpList =
{
_builder: null,
_tasks: null,
_shuttingDown: false,
/**
* Startup, shutdown, and update
*/
startup: function WTBJL_startup() {
// exit if this isn't win7 or higher.
if (!this._initTaskbar())
return;
// Win shell shortcut maintenance. If we've gone through an update,
// this will update any pinned taskbar shortcuts. Not specific to
// jump lists, but this was a convienent place to call it.
try {
// dev builds may not have helper.exe, ignore failures.
this._shortcutMaintenance();
} catch (ex) {
}
// Store our task list config data
this._tasks = tasksCfg;
// retrieve taskbar related prefs.
this._refreshPrefs();
// observer for private browsing and our prefs branch
this._initObs();
// jump list refresh timer
this._updateTimer();
},
update: function WTBJL_update() {
// are we disabled via prefs? don't do anything!
if (!this._enabled)
return;
// do what we came here to do, update the taskbar jumplist
this._buildList();
},
_shutdown: function WTBJL__shutdown() {
this._shuttingDown = true;
// Correctly handle a clear history on shutdown. If there are no
// entries be sure to empty all history lists. Luckily Places caches
// this value, so it's a pretty fast call.
if (!PlacesUtils.history.hasHistoryEntries) {
this.update();
}
this._free();
},
_shortcutMaintenance: function WTBJL__maintenace() {
_winShellService.shortcutMaintenance();
},
/**
* List building
*
* @note Async builders must add their mozIStoragePendingStatement to
* _pendingStatements object, using a different LIST_TYPE entry for
* each statement. Once finished they must remove it and call
* commitBuild(). When there will be no more _pendingStatements,
* commitBuild() will commit for real.
*/
_pendingStatements: {},
_hasPendingStatements: function WTBJL__hasPendingStatements() {
return Object.keys(this._pendingStatements).length > 0;
},
_buildList: function WTBJL__buildList() {
if (this._hasPendingStatements()) {
// We were requested to update the list while another update was in
// progress, this could happen at shutdown, idle or privatebrowsing.
// Abort the current list building.
for (let listType in this._pendingStatements) {
this._pendingStatements[listType].cancel();
delete this._pendingStatements[listType];
}
this._builder.abortListBuild();
}
// anything to build?
if (!this._showFrequent && !this._showRecent && !this._showTasks) {
// don't leave the last list hanging on the taskbar.
this._deleteActiveJumpList();
return;
}
if (!this._startBuild())
return;
if (this._showTasks)
this._buildTasks();
// Space for frequent items takes priority over recent.
if (this._showFrequent)
this._buildFrequent();
if (this._showRecent)
this._buildRecent();
this._commitBuild();
},
/**
* Taskbar api wrappers
*/
_startBuild: function WTBJL__startBuild() {
var removedItems = Cc["@mozilla.org/array;1"].
createInstance(Ci.nsIMutableArray);
this._builder.abortListBuild();
if (this._builder.initListBuild(removedItems)) {
// Prior to building, delete removed items from history.
this._clearHistory(removedItems);
return true;
}
return false;
},
_commitBuild: function WTBJL__commitBuild() {
if (!this._hasPendingStatements() && !this._builder.commitListBuild()) {
this._builder.abortListBuild();
}
},
_buildTasks: function WTBJL__buildTasks() {
var items = Cc["@mozilla.org/array;1"].
createInstance(Ci.nsIMutableArray);
this._tasks.forEach(function (task) {
if ((this._shuttingDown && !task.close) || (!this._shuttingDown && !task.open))
return;
var item = this._getHandlerAppItem(task.title, task.description,
task.args, task.iconIndex, null);
items.appendElement(item, false);
}, this);
if (items.length > 0)
this._builder.addListToBuild(this._builder.JUMPLIST_CATEGORY_TASKS, items);
},
_buildCustom: function WTBJL__buildCustom(title, items) {
if (items.length > 0)
this._builder.addListToBuild(this._builder.JUMPLIST_CATEGORY_CUSTOMLIST, items, title);
},
_buildFrequent: function WTBJL__buildFrequent() {
// If history is empty, just bail out.
if (!PlacesUtils.history.hasHistoryEntries) {
return;
}
// Windows supports default frequent and recent lists,
// but those depend on internal windows visit tracking
// which we don't populate. So we build our own custom
// frequent and recent lists using our nav history data.
var items = Cc["@mozilla.org/array;1"].
createInstance(Ci.nsIMutableArray);
// track frequent items so that we don't add them to
// the recent list.
this._frequentHashList = [];
this._pendingStatements[LIST_TYPE.FREQUENT] = this._getHistoryResults(
Ci.nsINavHistoryQueryOptions.SORT_BY_VISITCOUNT_DESCENDING,
this._maxItemCount,
function (aResult) {
if (!aResult) {
delete this._pendingStatements[LIST_TYPE.FREQUENT];
// The are no more results, build the list.
this._buildCustom(_getString("taskbar.frequent.label"), items);
this._commitBuild();
return;
}
let title = aResult.title || aResult.uri;
let faviconPageUri = Services.io.newURI(aResult.uri, null, null);
let shortcut = this._getHandlerAppItem(title, title, aResult.uri, 1,
faviconPageUri);
items.appendElement(shortcut, false);
this._frequentHashList.push(aResult.uri);
},
this
);
},
_buildRecent: function WTBJL__buildRecent() {
// If history is empty, just bail out.
if (!PlacesUtils.history.hasHistoryEntries) {
return;
}
var items = Cc["@mozilla.org/array;1"].
createInstance(Ci.nsIMutableArray);
// Frequent items will be skipped, so we select a double amount of
// entries and stop fetching results at _maxItemCount.
var count = 0;
this._pendingStatements[LIST_TYPE.RECENT] = this._getHistoryResults(
Ci.nsINavHistoryQueryOptions.SORT_BY_DATE_DESCENDING,
this._maxItemCount * 2,
function (aResult) {
if (!aResult) {
// The are no more results, build the list.
this._buildCustom(_getString("taskbar.recent.label"), items);
delete this._pendingStatements[LIST_TYPE.RECENT];
this._commitBuild();
return;
}
if (count >= this._maxItemCount) {
return;
}
// Do not add items to recent that have already been added to frequent.
if (this._frequentHashList &&
this._frequentHashList.indexOf(aResult.uri) != -1) {
return;
}
let title = aResult.title || aResult.uri;
let faviconPageUri = Services.io.newURI(aResult.uri, null, null);
let shortcut = this._getHandlerAppItem(title, title, aResult.uri, 1,
faviconPageUri);
items.appendElement(shortcut, false);
count++;
},
this
);
},
_deleteActiveJumpList: function WTBJL__deleteAJL() {
this._builder.deleteActiveList();
},
/**
* Jump list item creation helpers
*/
_getHandlerAppItem: function WTBJL__getHandlerAppItem(name, description,
args, iconIndex,
faviconPageUri) {
var file = Services.dirsvc.get("XREExeF", Ci.nsILocalFile);
var handlerApp = Cc["@mozilla.org/uriloader/local-handler-app;1"].
createInstance(Ci.nsILocalHandlerApp);
handlerApp.executable = file;
// handlers default to the leaf name if a name is not specified
if (name && name.length != 0)
handlerApp.name = name;
handlerApp.detailedDescription = description;
handlerApp.appendParameter(args);
var item = Cc["@mozilla.org/windows-jumplistshortcut;1"].
createInstance(Ci.nsIJumpListShortcut);
item.app = handlerApp;
item.iconIndex = iconIndex;
item.faviconPageUri = faviconPageUri;
return item;
},
_getSeparatorItem: function WTBJL__getSeparatorItem() {
var item = Cc["@mozilla.org/windows-jumplistseparator;1"].
createInstance(Ci.nsIJumpListSeparator);
return item;
},
/**
* Nav history helpers
*/
_getHistoryResults:
function WTBLJL__getHistoryResults(aSortingMode, aLimit, aCallback, aScope) {
var options = PlacesUtils.history.getNewQueryOptions();
options.maxResults = aLimit;
options.sortingMode = aSortingMode;
var query = PlacesUtils.history.getNewQuery();
// Return the pending statement to the caller, to allow cancelation.
return PlacesUtils.history.QueryInterface(Ci.nsPIPlacesDatabase)
.asyncExecuteLegacyQueries([query], 1, options, {
handleResult: function (aResultSet) {
for (let row; (row = aResultSet.getNextRow());) {
try {
aCallback.call(aScope,
{ uri: row.getResultByIndex(1)
, title: row.getResultByIndex(2)
});
} catch (e) {}
}
},
handleError: function (aError) {
Components.utils.reportError(
"Async execution error (" + aError.result + "): " + aError.message);
},
handleCompletion: function (aReason) {
aCallback.call(WinTaskbarJumpList, null);
},
});
},
_clearHistory: function WTBJL__clearHistory(items) {
if (!items)
return;
var URIsToRemove = [];
var e = items.enumerate();
while (e.hasMoreElements()) {
let oldItem = e.getNext().QueryInterface(Ci.nsIJumpListShortcut);
if (oldItem) {
try { // in case we get a bad uri
let uriSpec = oldItem.app.getParameter(0);
URIsToRemove.push(NetUtil.newURI(uriSpec));
} catch (err) { }
}
}
if (URIsToRemove.length > 0) {
PlacesUtils.bhistory.removePages(URIsToRemove, URIsToRemove.length, true);
}
},
/**
* Prefs utilities
*/
_refreshPrefs: function WTBJL__refreshPrefs() {
this._enabled = _prefs.getBoolPref(PREF_TASKBAR_ENABLED);
this._showFrequent = _prefs.getBoolPref(PREF_TASKBAR_FREQUENT);
this._showRecent = _prefs.getBoolPref(PREF_TASKBAR_RECENT);
this._showTasks = _prefs.getBoolPref(PREF_TASKBAR_TASKS);
this._maxItemCount = _prefs.getIntPref(PREF_TASKBAR_ITEMCOUNT);
},
/**
* Init and shutdown utilities
*/
_initTaskbar: function WTBJL__initTaskbar() {
this._builder = _taskbarService.createJumpListBuilder();
if (!this._builder || !this._builder.available)
return false;
return true;
},
_initObs: function WTBJL__initObs() {
// If the browser is closed while in private browsing mode, the "exit"
// notification is fired on quit-application-granted.
// History cleanup can happen at profile-change-teardown.
Services.obs.addObserver(this, "profile-before-change", false);
Services.obs.addObserver(this, "browser:purge-session-history", false);
_prefs.addObserver("", this, false);
},
_freeObs: function WTBJL__freeObs() {
Services.obs.removeObserver(this, "profile-before-change");
Services.obs.removeObserver(this, "browser:purge-session-history");
_prefs.removeObserver("", this);
},
_updateTimer: function WTBJL__updateTimer() {
if (this._enabled && !this._shuttingDown && !this._timer) {
this._timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
this._timer.initWithCallback(this,
_prefs.getIntPref(PREF_TASKBAR_REFRESH)*1000,
this._timer.TYPE_REPEATING_SLACK);
}
else if ((!this._enabled || this._shuttingDown) && this._timer) {
this._timer.cancel();
delete this._timer;
}
},
_hasIdleObserver: false,
_updateIdleObserver: function WTBJL__updateIdleObserver() {
if (this._enabled && !this._shuttingDown && !this._hasIdleObserver) {
_idle.addIdleObserver(this, IDLE_TIMEOUT_SECONDS);
this._hasIdleObserver = true;
}
else if ((!this._enabled || this._shuttingDown) && this._hasIdleObserver) {
_idle.removeIdleObserver(this, IDLE_TIMEOUT_SECONDS);
this._hasIdleObserver = false;
}
},
_free: function WTBJL__free() {
this._freeObs();
this._updateTimer();
this._updateIdleObserver();
delete this._builder;
},
/**
* Notification handlers
*/
notify: function WTBJL_notify(aTimer) {
// Add idle observer on the first notification so it doesn't hit startup.
this._updateIdleObserver();
this.update();
},
observe: function WTBJL_observe(aSubject, aTopic, aData) {
switch (aTopic) {
case "nsPref:changed":
if (this._enabled == true && !_prefs.getBoolPref(PREF_TASKBAR_ENABLED))
this._deleteActiveJumpList();
this._refreshPrefs();
this._updateTimer();
this._updateIdleObserver();
this.update();
break;
case "profile-before-change":
this._shutdown();
break;
case "browser:purge-session-history":
this.update();
break;
case "idle":
if (this._timer) {
this._timer.cancel();
delete this._timer;
}
break;
case "active":
this._updateTimer();
break;
}
},
};

View file

@ -0,0 +1,862 @@
/* vim: se cin sw=2 ts=2 et filetype=javascript :
* 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/. */
/*
* This module implements the front end behavior for AeroPeek. Starting in
* Windows Vista, the taskbar began showing live thumbnail previews of windows
* when the user hovered over the window icon in the taskbar. Starting with
* Windows 7, the taskbar allows an application to expose its tabbed interface
* in the taskbar by showing thumbnail previews rather than the default window
* preview. Additionally, when a user hovers over a thumbnail (tab or window),
* they are shown a live preview of the window (or tab + its containing window).
*
* In Windows 7, a title, icon, close button and optional toolbar are shown for
* each preview. This feature does not make use of the toolbar. For window
* previews, the title is the window title and the icon the window icon. For
* tab previews, the title is the page title and the page's favicon. In both
* cases, the close button "does the right thing."
*
* The primary objects behind this feature are nsITaskbarTabPreview and
* nsITaskbarPreviewController. Each preview has a controller. The controller
* responds to the user's interactions on the taskbar and provides the required
* data to the preview for determining the size of the tab and thumbnail. The
* PreviewController class implements this interface. The preview will request
* the controller to provide a thumbnail or preview when the user interacts with
* the taskbar. To reduce the overhead of drawing the tab area, the controller
* implementation caches the tab's contents in a <canvas> element. If no
* previews or thumbnails have been requested for some time, the controller will
* discard its cached tab contents.
*
* Screen real estate is limited so when there are too many thumbnails to fit
* on the screen, the taskbar stops displaying thumbnails and instead displays
* just the title, icon and close button in a similar fashion to previous
* versions of the taskbar. If there are still too many previews to fit on the
* screen, the taskbar resorts to a scroll up and scroll down button pair to let
* the user scroll through the list of tabs. Since this is undoubtedly
* inconvenient for users with many tabs, the AeroPeek objects turns off all of
* the tab previews. This tells the taskbar to revert to one preview per window.
* If the number of tabs falls below this magic threshold, the preview-per-tab
* behavior returns. There is no reliable way to determine when the scroll
* buttons appear on the taskbar, so a magic pref-controlled number determines
* when this threshold has been crossed.
*/
this.EXPORTED_SYMBOLS = ["AeroPeek"];
const Cc = Components.classes;
const Ci = Components.interfaces;
const Cu = Components.utils;
Cu.import("resource://gre/modules/NetUtil.jsm");
Cu.import("resource://gre/modules/PlacesUtils.jsm");
Cu.import("resource://gre/modules/PrivateBrowsingUtils.jsm");
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
// Pref to enable/disable preview-per-tab
const TOGGLE_PREF_NAME = "browser.taskbar.previews.enable";
// Pref to determine the magic auto-disable threshold
const DISABLE_THRESHOLD_PREF_NAME = "browser.taskbar.previews.max";
// Pref to control the time in seconds that tab contents live in the cache
const CACHE_EXPIRATION_TIME_PREF_NAME = "browser.taskbar.previews.cachetime";
const WINTASKBAR_CONTRACTID = "@mozilla.org/windows-taskbar;1";
// Various utility properties
XPCOMUtils.defineLazyServiceGetter(this, "imgTools",
"@mozilla.org/image/tools;1",
"imgITools");
XPCOMUtils.defineLazyModuleGetter(this, "PageThumbs",
"resource://gre/modules/PageThumbs.jsm");
// nsIURI -> imgIContainer
function _imageFromURI(uri, privateMode, callback) {
let channel = NetUtil.newChannel({
uri: uri,
loadUsingSystemPrincipal: true,
contentPolicyType: Ci.nsIContentPolicy.TYPE_INTERNAL_IMAGE
});
try {
channel.QueryInterface(Ci.nsIPrivateBrowsingChannel);
channel.setPrivate(privateMode);
} catch (e) {
// Ignore channels which do not support nsIPrivateBrowsingChannel
}
NetUtil.asyncFetch(channel, function(inputStream, resultCode) {
if (!Components.isSuccessCode(resultCode))
return;
try {
let out_img = { value: null };
imgTools.decodeImageData(inputStream, channel.contentType, out_img);
callback(out_img.value);
} catch (e) {
// We failed, so use the default favicon (only if this wasn't the default
// favicon).
let defaultURI = PlacesUtils.favicons.defaultFavicon;
if (!defaultURI.equals(uri))
_imageFromURI(defaultURI, privateMode, callback);
}
});
}
// string? -> imgIContainer
function getFaviconAsImage(iconurl, privateMode, callback) {
if (iconurl) {
_imageFromURI(NetUtil.newURI(iconurl), privateMode, callback);
} else {
_imageFromURI(PlacesUtils.favicons.defaultFavicon, privateMode, callback);
}
}
// Snaps the given rectangle to be pixel-aligned at the given scale
function snapRectAtScale(r, scale) {
let x = Math.floor(r.x * scale);
let y = Math.floor(r.y * scale);
let width = Math.ceil((r.x + r.width) * scale) - x;
let height = Math.ceil((r.y + r.height) * scale) - y;
r.x = x / scale;
r.y = y / scale;
r.width = width / scale;
r.height = height / scale;
}
// PreviewController
/*
* This class manages the behavior of thumbnails and previews. It has the following
* responsibilities:
* 1) responding to requests from Windows taskbar for a thumbnail or window
* preview.
* 2) listens for dom events that result in a thumbnail or window preview needing
* to be refresh, and communicates this to the taskbar.
* 3) Handles querying and returning to the taskbar new thumbnail or window
* preview images through PageThumbs.
*
* @param win
* The TabWindow (see below) that owns the preview that this controls
* @param tab
* The <tab> that this preview is associated with
*/
function PreviewController(win, tab) {
this.win = win;
this.tab = tab;
this.linkedBrowser = tab.linkedBrowser;
this.preview = this.win.createTabPreview(this);
this.tab.addEventListener("TabAttrModified", this, false);
XPCOMUtils.defineLazyGetter(this, "canvasPreview", function () {
let canvas = PageThumbs.createCanvas();
canvas.mozOpaque = true;
return canvas;
});
}
PreviewController.prototype = {
QueryInterface: XPCOMUtils.generateQI([Ci.nsITaskbarPreviewController,
Ci.nsIDOMEventListener]),
destroy: function () {
this.tab.removeEventListener("TabAttrModified", this, false);
// Break cycles, otherwise we end up leaking the window with everything
// attached to it.
delete this.win;
delete this.preview;
},
get wrappedJSObject() {
return this;
},
// Resizes the canvasPreview to 0x0, essentially freeing its memory.
resetCanvasPreview: function () {
this.canvasPreview.width = 0;
this.canvasPreview.height = 0;
},
/**
* Set the canvas dimensions.
*/
resizeCanvasPreview: function (aRequestedWidth, aRequestedHeight) {
this.canvasPreview.width = aRequestedWidth;
this.canvasPreview.height = aRequestedHeight;
},
get zoom() {
// Note that winutils.fullZoom accounts for "quantization" of the zoom factor
// from nsIContentViewer due to conversion through appUnits.
// We do -not- want screenPixelsPerCSSPixel here, because that would -also-
// incorporate any scaling that is applied due to hi-dpi resolution options.
return this.tab.linkedBrowser.fullZoom;
},
get screenPixelsPerCSSPixel() {
let chromeWin = this.tab.ownerGlobal;
let windowUtils = chromeWin.getInterface(Ci.nsIDOMWindowUtils);
return windowUtils.screenPixelsPerCSSPixel;
},
get browserDims() {
return this.tab.linkedBrowser.getBoundingClientRect();
},
cacheBrowserDims: function () {
let dims = this.browserDims;
this._cachedWidth = dims.width;
this._cachedHeight = dims.height;
},
testCacheBrowserDims: function () {
let dims = this.browserDims;
return this._cachedWidth == dims.width &&
this._cachedHeight == dims.height;
},
/**
* Capture a new thumbnail image for this preview. Called by the controller
* in response to a request for a new thumbnail image.
*/
updateCanvasPreview: function (aFullScale, aCallback) {
// Update our cached browser dims so that delayed resize
// events don't trigger another invalidation if this tab becomes active.
this.cacheBrowserDims();
PageThumbs.captureToCanvas(this.linkedBrowser, this.canvasPreview,
aCallback, { fullScale: aFullScale });
// If we're updating the canvas, then we're in the middle of a peek so
// don't discard the cache of previews.
AeroPeek.resetCacheTimer();
},
updateTitleAndTooltip: function () {
let title = this.win.tabbrowser.getWindowTitleForBrowser(this.linkedBrowser);
this.preview.title = title;
this.preview.tooltip = title;
},
// nsITaskbarPreviewController
// window width and height, not browser
get width() {
return this.win.width;
},
// window width and height, not browser
get height() {
return this.win.height;
},
get thumbnailAspectRatio() {
let browserDims = this.browserDims;
// Avoid returning 0
let tabWidth = browserDims.width || 1;
// Avoid divide by 0
let tabHeight = browserDims.height || 1;
return tabWidth / tabHeight;
},
/**
* Responds to taskbar requests for window previews. Returns the results asynchronously
* through updateCanvasPreview.
*
* @param aTaskbarCallback nsITaskbarPreviewCallback results callback
*/
requestPreview: function (aTaskbarCallback) {
// Grab a high res content preview
this.resetCanvasPreview();
this.updateCanvasPreview(true, (aPreviewCanvas) => {
let winWidth = this.win.width;
let winHeight = this.win.height;
let composite = PageThumbs.createCanvas();
// Use transparency, Aero glass is drawn black without it.
composite.mozOpaque = false;
let ctx = composite.getContext('2d');
let scale = this.screenPixelsPerCSSPixel / this.zoom;
composite.width = winWidth * scale;
composite.height = winHeight * scale;
ctx.save();
ctx.scale(scale, scale);
// Draw chrome. Note we currently do not get scrollbars for remote frames
// in the image above.
ctx.drawWindow(this.win.win, 0, 0, winWidth, winHeight, "rgba(0,0,0,0)");
// Draw the content are into the composite canvas at the right location.
ctx.drawImage(aPreviewCanvas, this.browserDims.x, this.browserDims.y,
aPreviewCanvas.width, aPreviewCanvas.height);
ctx.restore();
// Deliver the resulting composite canvas to Windows
this.win.tabbrowser.previewTab(this.tab, function () {
aTaskbarCallback.done(composite, false);
});
});
},
/**
* Responds to taskbar requests for tab thumbnails. Returns the results asynchronously
* through updateCanvasPreview.
*
* Note Windows requests a specific width and height here, if the resulting thumbnail
* does not match these dimensions thumbnail display will fail.
*
* @param aTaskbarCallback nsITaskbarPreviewCallback results callback
* @param aRequestedWidth width of the requested thumbnail
* @param aRequestedHeight height of the requested thumbnail
*/
requestThumbnail: function (aTaskbarCallback, aRequestedWidth, aRequestedHeight) {
this.resizeCanvasPreview(aRequestedWidth, aRequestedHeight);
this.updateCanvasPreview(false, (aThumbnailCanvas) => {
aTaskbarCallback.done(aThumbnailCanvas, false);
});
},
// Event handling
onClose: function () {
this.win.tabbrowser.removeTab(this.tab);
},
onActivate: function () {
this.win.tabbrowser.selectedTab = this.tab;
// Accept activation - this will restore the browser window
// if it's minimized
return true;
},
// nsIDOMEventListener
handleEvent: function (evt) {
switch (evt.type) {
case "TabAttrModified":
this.updateTitleAndTooltip();
break;
}
}
};
XPCOMUtils.defineLazyGetter(PreviewController.prototype, "canvasPreviewFlags",
function () { let canvasInterface = Ci.nsIDOMCanvasRenderingContext2D;
return canvasInterface.DRAWWINDOW_DRAW_VIEW
| canvasInterface.DRAWWINDOW_DRAW_CARET
| canvasInterface.DRAWWINDOW_ASYNC_DECODE_IMAGES
| canvasInterface.DRAWWINDOW_DO_NOT_FLUSH;
});
// TabWindow
/*
* This class monitors a browser window for changes to its tabs
*
* @param win
* The nsIDOMWindow browser window
*/
function TabWindow(win) {
this.win = win;
this.tabbrowser = win.gBrowser;
this.previews = new Map();
for (let i = 0; i < this.tabEvents.length; i++)
this.tabbrowser.tabContainer.addEventListener(this.tabEvents[i], this, false);
for (let i = 0; i < this.winEvents.length; i++)
this.win.addEventListener(this.winEvents[i], this, false);
this.tabbrowser.addTabsProgressListener(this);
AeroPeek.windows.push(this);
let tabs = this.tabbrowser.tabs;
for (let i = 0; i < tabs.length; i++)
this.newTab(tabs[i]);
this.updateTabOrdering();
AeroPeek.checkPreviewCount();
}
TabWindow.prototype = {
_enabled: false,
_cachedWidth: 0,
_cachedHeight: 0,
tabEvents: ["TabOpen", "TabClose", "TabSelect", "TabMove"],
winEvents: ["resize"],
destroy: function () {
this._destroying = true;
let tabs = this.tabbrowser.tabs;
this.tabbrowser.removeTabsProgressListener(this);
for (let i = 0; i < this.winEvents.length; i++)
this.win.removeEventListener(this.winEvents[i], this, false);
for (let i = 0; i < this.tabEvents.length; i++)
this.tabbrowser.tabContainer.removeEventListener(this.tabEvents[i], this, false);
for (let i = 0; i < tabs.length; i++)
this.removeTab(tabs[i]);
let idx = AeroPeek.windows.indexOf(this.win.gTaskbarTabGroup);
AeroPeek.windows.splice(idx, 1);
AeroPeek.checkPreviewCount();
},
get width () {
return this.win.innerWidth;
},
get height () {
return this.win.innerHeight;
},
cacheDims: function () {
this._cachedWidth = this.width;
this._cachedHeight = this.height;
},
testCacheDims: function () {
return this._cachedWidth == this.width && this._cachedHeight == this.height;
},
// Invoked when the given tab is added to this window
newTab: function (tab) {
let controller = new PreviewController(this, tab);
// It's OK to add the preview now while the favicon still loads.
this.previews.set(tab, controller.preview);
AeroPeek.addPreview(controller.preview);
// updateTitleAndTooltip relies on having controller.preview which is lazily resolved.
// Now that we've updated this.previews, it will resolve successfully.
controller.updateTitleAndTooltip();
},
createTabPreview: function (controller) {
let docShell = this.win
.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIWebNavigation)
.QueryInterface(Ci.nsIDocShell);
let preview = AeroPeek.taskbar.createTaskbarTabPreview(docShell, controller);
preview.visible = AeroPeek.enabled;
preview.active = this.tabbrowser.selectedTab == controller.tab;
this.onLinkIconAvailable(controller.tab.linkedBrowser,
controller.tab.getAttribute("image"));
return preview;
},
// Invoked when the given tab is closed
removeTab: function (tab) {
let preview = this.previewFromTab(tab);
preview.active = false;
preview.visible = false;
preview.move(null);
preview.controller.wrappedJSObject.destroy();
this.previews.delete(tab);
AeroPeek.removePreview(preview);
},
get enabled () {
return this._enabled;
},
set enabled (enable) {
this._enabled = enable;
// Because making a tab visible requires that the tab it is next to be
// visible, it is far simpler to unset the 'next' tab and recreate them all
// at once.
for (let [, preview] of this.previews) {
preview.move(null);
preview.visible = enable;
}
this.updateTabOrdering();
},
previewFromTab: function (tab) {
return this.previews.get(tab);
},
updateTabOrdering: function () {
let previews = this.previews;
let tabs = this.tabbrowser.tabs;
// Previews are internally stored using a map, so we need to iterate the
// tabbrowser's array of tabs to retrieve previews in the same order.
let inorder = [];
for (let t of tabs) {
if (previews.has(t)) {
inorder.push(previews.get(t));
}
}
// Since the internal taskbar array has not yet been updated we must force
// on it the sorting order of our local array. To do so we must walk
// the local array backwards, otherwise we would send move requests in the
// wrong order. See bug 522610 for details.
for (let i = inorder.length - 1; i >= 0; i--) {
inorder[i].move(inorder[i + 1] || null);
}
},
// nsIDOMEventListener
handleEvent: function (evt) {
let tab = evt.originalTarget;
switch (evt.type) {
case "TabOpen":
this.newTab(tab);
this.updateTabOrdering();
break;
case "TabClose":
this.removeTab(tab);
this.updateTabOrdering();
break;
case "TabSelect":
this.previewFromTab(tab).active = true;
break;
case "TabMove":
this.updateTabOrdering();
break;
case "resize":
if (!AeroPeek._prefenabled)
return;
this.onResize();
break;
}
},
// Set or reset a timer that will invalidate visible thumbnails soon.
setInvalidationTimer: function () {
if (!this.invalidateTimer) {
this.invalidateTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
}
this.invalidateTimer.cancel();
// delay 1 second before invalidating
this.invalidateTimer.initWithCallback(() => {
// invalidate every preview. note the internal implementation of
// invalidate ignores thumbnails that aren't visible.
this.previews.forEach(function (aPreview) {
let controller = aPreview.controller.wrappedJSObject;
if (!controller.testCacheBrowserDims()) {
controller.cacheBrowserDims();
aPreview.invalidate();
}
});
}, 1000, Ci.nsITimer.TYPE_ONE_SHOT);
},
onResize: function () {
// Specific to a window.
// Call invalidate on each tab thumbnail so that Windows will request an
// updated image. However don't do this repeatedly across multiple resize
// events triggered during window border drags.
if (this.testCacheDims()) {
return;
}
// update the window dims on our TabWindow object.
this.cacheDims();
// invalidate soon
this.setInvalidationTimer();
},
invalidateTabPreview: function(aBrowser) {
for (let [tab, preview] of this.previews) {
if (aBrowser == tab.linkedBrowser) {
preview.invalidate();
break;
}
}
},
// Browser progress listener
onLocationChange: function (aBrowser) {
// I'm not sure we need this, onStateChange does a really good job
// of picking up page changes.
// this.invalidateTabPreview(aBrowser);
},
onStateChange: function (aBrowser, aWebProgress, aRequest, aStateFlags, aStatus) {
if (aStateFlags & Ci.nsIWebProgressListener.STATE_STOP &&
aStateFlags & Ci.nsIWebProgressListener.STATE_IS_NETWORK) {
this.invalidateTabPreview(aBrowser);
}
},
directRequestProtocols: new Set([
"file", "chrome", "resource", "about"
]),
onLinkIconAvailable: function (aBrowser, aIconURL) {
let requestURL = null;
if (aIconURL) {
let shouldRequestFaviconURL = true;
try {
let urlObject = NetUtil.newURI(aIconURL);
shouldRequestFaviconURL =
!this.directRequestProtocols.has(urlObject.scheme);
} catch (ex) {}
requestURL = shouldRequestFaviconURL ?
"moz-anno:favicon:" + aIconURL :
aIconURL;
}
let isDefaultFavicon = !requestURL;
getFaviconAsImage(
requestURL,
PrivateBrowsingUtils.isWindowPrivate(this.win),
img => {
let index = this.tabbrowser.browsers.indexOf(aBrowser);
// Only add it if we've found the index and the URI is still the same.
// The tab could have closed, and there's no guarantee the icons
// will have finished fetching 'in order'.
if (index != -1) {
let tab = this.tabbrowser.tabs[index];
let preview = this.previews.get(tab);
if (tab.getAttribute("image") == aIconURL ||
(!preview.icon && isDefaultFavicon)) {
preview.icon = img;
}
}
}
);
}
}
// AeroPeek
/*
* This object acts as global storage and external interface for this feature.
* It maintains the values of the prefs.
*/
this.AeroPeek = {
available: false,
// Does the pref say we're enabled?
__prefenabled: false,
_enabled: true,
initialized: false,
// nsITaskbarTabPreview array
previews: [],
// TabWindow array
windows: [],
// nsIWinTaskbar service
taskbar: null,
// Maximum number of previews
maxpreviews: 20,
// Length of time in seconds that previews are cached
cacheLifespan: 20,
initialize: function () {
if (!(WINTASKBAR_CONTRACTID in Cc))
return;
this.taskbar = Cc[WINTASKBAR_CONTRACTID].getService(Ci.nsIWinTaskbar);
this.available = this.taskbar.available;
if (!this.available)
return;
this.prefs.addObserver(TOGGLE_PREF_NAME, this, true);
this.enabled = this._prefenabled = this.prefs.getBoolPref(TOGGLE_PREF_NAME);
this.initialized = true;
},
destroy: function destroy() {
this._enabled = false;
if (this.cacheTimer)
this.cacheTimer.cancel();
},
get enabled() {
return this._enabled;
},
set enabled(enable) {
if (this._enabled == enable)
return;
this._enabled = enable;
this.windows.forEach(function (win) {
win.enabled = enable;
});
},
get _prefenabled() {
return this.__prefenabled;
},
set _prefenabled(enable) {
if (enable == this.__prefenabled) {
return;
}
this.__prefenabled = enable;
if (enable) {
this.enable();
} else {
this.disable();
}
},
_observersAdded: false,
enable() {
if (!this._observersAdded) {
this.prefs.addObserver(DISABLE_THRESHOLD_PREF_NAME, this, true);
this.prefs.addObserver(CACHE_EXPIRATION_TIME_PREF_NAME, this, true);
PlacesUtils.history.addObserver(this, true);
this._observersAdded = true;
}
this.cacheLifespan = this.prefs.getIntPref(CACHE_EXPIRATION_TIME_PREF_NAME);
this.maxpreviews = this.prefs.getIntPref(DISABLE_THRESHOLD_PREF_NAME);
// If the user toggled us on/off while the browser was already up
// (rather than this code running on startup because the pref was
// already set to true), we must initialize previews for open windows:
if (this.initialized) {
let browserWindows = Services.wm.getEnumerator("navigator:browser");
while (browserWindows.hasMoreElements()) {
let win = browserWindows.getNext();
if (!win.closed) {
this.onOpenWindow(win);
}
}
}
},
disable() {
while (this.windows.length) {
// We can't call onCloseWindow here because it'll bail if we're not
// enabled.
let tabWinObject = this.windows[0];
tabWinObject.destroy(); // This will remove us from the array.
delete tabWinObject.win.gTaskbarTabGroup; // Tidy up the window.
}
},
addPreview: function (preview) {
this.previews.push(preview);
this.checkPreviewCount();
},
removePreview: function (preview) {
let idx = this.previews.indexOf(preview);
this.previews.splice(idx, 1);
this.checkPreviewCount();
},
checkPreviewCount: function () {
if (!this._prefenabled) {
return;
}
this.enabled = this.previews.length <= this.maxpreviews;
},
onOpenWindow: function (win) {
// This occurs when the taskbar service is not available (xp, vista)
if (!this.available || !this._prefenabled)
return;
win.gTaskbarTabGroup = new TabWindow(win);
},
onCloseWindow: function (win) {
// This occurs when the taskbar service is not available (xp, vista)
if (!this.available || !this._prefenabled)
return;
win.gTaskbarTabGroup.destroy();
delete win.gTaskbarTabGroup;
if (this.windows.length == 0)
this.destroy();
},
resetCacheTimer: function () {
this.cacheTimer.cancel();
this.cacheTimer.init(this, 1000*this.cacheLifespan, Ci.nsITimer.TYPE_ONE_SHOT);
},
// nsIObserver
observe: function (aSubject, aTopic, aData) {
if (aTopic == "nsPref:changed" && aData == TOGGLE_PREF_NAME) {
this._prefenabled = this.prefs.getBoolPref(TOGGLE_PREF_NAME);
}
if (!this._prefenabled) {
return;
}
switch (aTopic) {
case "nsPref:changed":
if (aData == CACHE_EXPIRATION_TIME_PREF_NAME)
break;
if (aData == DISABLE_THRESHOLD_PREF_NAME)
this.maxpreviews = this.prefs.getIntPref(DISABLE_THRESHOLD_PREF_NAME);
// Might need to enable/disable ourselves
this.checkPreviewCount();
break;
case "timer-callback":
this.previews.forEach(function (preview) {
let controller = preview.controller.wrappedJSObject;
controller.resetCanvasPreview();
});
break;
}
},
/* nsINavHistoryObserver implementation */
onBeginUpdateBatch() {},
onEndUpdateBatch() {},
onVisit() {},
onTitleChanged() {},
onFrecencyChanged() {},
onManyFrecenciesChanged() {},
onDeleteURI() {},
onClearHistory() {},
onDeleteVisits() {},
onPageChanged(uri, changedConst, newValue) {
if (this.enabled && changedConst == Ci.nsINavHistoryObserver.ATTRIBUTE_FAVICON) {
for (let win of this.windows) {
for (let [tab, ] of win.previews) {
if (tab.getAttribute("image") == newValue) {
win.onLinkIconAvailable(tab.linkedBrowser, newValue);
}
}
}
}
},
QueryInterface: XPCOMUtils.generateQI([
Ci.nsISupportsWeakReference,
Ci.nsINavHistoryObserver,
Ci.nsIObserver
]),
};
XPCOMUtils.defineLazyGetter(AeroPeek, "cacheTimer", () =>
Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer)
);
XPCOMUtils.defineLazyServiceGetter(AeroPeek, "prefs",
"@mozilla.org/preferences-service;1",
"nsIPrefBranch");
AeroPeek.initialize();

View file

@ -0,0 +1,47 @@
# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
# vim: set filetype=python:
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
EXTRA_JS_MODULES += [
'AboutHome.jsm',
'AboutNewTab.jsm',
'AttributionCode.jsm',
'BrowserUITelemetry.jsm',
'BrowserUsageTelemetry.jsm',
'CastingApps.jsm',
'ContentClick.jsm',
'ContentCrashHandlers.jsm',
'ContentLinkHandler.jsm',
'ContentObservers.jsm',
'ContentSearch.jsm',
'ContentWebRTC.jsm',
'DirectoryLinksProvider.jsm',
'E10SUtils.jsm',
'Feeds.jsm',
'FormSubmitObserver.jsm',
'FormValidationHandler.jsm',
'HiddenFrame.jsm',
'LaterRun.jsm',
'NetworkPrioritizer.jsm',
'offlineAppCache.jsm',
'PermissionUI.jsm',
'PluginContent.jsm',
'ProcessHangMonitor.jsm',
'ReaderParent.jsm',
'RecentWindow.jsm',
'RemotePrompt.jsm',
'Sanitizer.jsm',
'SitePermissions.jsm',
'TransientPrefs.jsm',
'URLBarZoom.jsm',
'webrtcUI.jsm',
]
if CONFIG['MOZ_WIDGET_TOOLKIT'] == 'windows':
EXTRA_JS_MODULES += [
'Windows8WindowFrameColor.jsm',
'WindowsJumpLists.jsm',
'WindowsPreviewPerTab.jsm',
]

View file

@ -0,0 +1,20 @@
/* 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/. */
this.EXPORTED_SYMBOLS = ["OfflineAppCacheHelper"];
Components.utils.import('resource://gre/modules/LoadContextInfo.jsm');
const Cc = Components.classes;
const Ci = Components.interfaces;
this.OfflineAppCacheHelper = {
clear: function() {
var cacheService = Cc["@mozilla.org/netwerk/cache-storage-service;1"].getService(Ci.nsICacheStorageService);
var appCacheStorage = cacheService.appCacheStorage(LoadContextInfo.default, null);
try {
appCacheStorage.asyncEvictStorage(null);
} catch (er) {}
}
};

View file

@ -0,0 +1,969 @@
/* 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 = ["webrtcUI"];
const Cu = Components.utils;
const Cc = Components.classes;
const Ci = Components.interfaces;
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "AppConstants",
"resource://gre/modules/AppConstants.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "PluralForm",
"resource://gre/modules/PluralForm.jsm");
this.webrtcUI = {
init: function () {
Services.obs.addObserver(maybeAddMenuIndicator, "browser-delayed-startup-finished", false);
let ppmm = Cc["@mozilla.org/parentprocessmessagemanager;1"]
.getService(Ci.nsIMessageBroadcaster);
ppmm.addMessageListener("webrtc:UpdatingIndicators", this);
ppmm.addMessageListener("webrtc:UpdateGlobalIndicators", this);
ppmm.addMessageListener("child-process-shutdown", this);
let mm = Cc["@mozilla.org/globalmessagemanager;1"]
.getService(Ci.nsIMessageListenerManager);
mm.addMessageListener("rtcpeer:Request", this);
mm.addMessageListener("rtcpeer:CancelRequest", this);
mm.addMessageListener("webrtc:Request", this);
mm.addMessageListener("webrtc:CancelRequest", this);
mm.addMessageListener("webrtc:UpdateBrowserIndicators", this);
},
uninit: function () {
Services.obs.removeObserver(maybeAddMenuIndicator, "browser-delayed-startup-finished");
let ppmm = Cc["@mozilla.org/parentprocessmessagemanager;1"]
.getService(Ci.nsIMessageBroadcaster);
ppmm.removeMessageListener("webrtc:UpdatingIndicators", this);
ppmm.removeMessageListener("webrtc:UpdateGlobalIndicators", this);
let mm = Cc["@mozilla.org/globalmessagemanager;1"]
.getService(Ci.nsIMessageListenerManager);
mm.removeMessageListener("rtcpeer:Request", this);
mm.removeMessageListener("rtcpeer:CancelRequest", this);
mm.removeMessageListener("webrtc:Request", this);
mm.removeMessageListener("webrtc:CancelRequest", this);
mm.removeMessageListener("webrtc:UpdateBrowserIndicators", this);
if (gIndicatorWindow) {
gIndicatorWindow.close();
gIndicatorWindow = null;
}
},
processIndicators: new Map(),
get showGlobalIndicator() {
for (let [, indicators] of this.processIndicators) {
if (indicators.showGlobalIndicator)
return true;
}
return false;
},
get showCameraIndicator() {
for (let [, indicators] of this.processIndicators) {
if (indicators.showCameraIndicator)
return true;
}
return false;
},
get showMicrophoneIndicator() {
for (let [, indicators] of this.processIndicators) {
if (indicators.showMicrophoneIndicator)
return true;
}
return false;
},
get showScreenSharingIndicator() {
let list = [""];
for (let [, indicators] of this.processIndicators) {
if (indicators.showScreenSharingIndicator)
list.push(indicators.showScreenSharingIndicator);
}
let precedence =
["Screen", "Window", "Application", "Browser", ""];
list.sort((a, b) => { return precedence.indexOf(a) -
precedence.indexOf(b); });
return list[0];
},
_streams: [],
// The boolean parameters indicate which streams should be included in the result.
getActiveStreams: function(aCamera, aMicrophone, aScreen) {
return webrtcUI._streams.filter(aStream => {
let state = aStream.state;
return aCamera && state.camera ||
aMicrophone && state.microphone ||
aScreen && state.screen;
}).map(aStream => {
let state = aStream.state;
let types = {camera: state.camera, microphone: state.microphone,
screen: state.screen};
let browser = aStream.browser;
let browserWindow = browser.ownerGlobal;
let tab = browserWindow.gBrowser &&
browserWindow.gBrowser.getTabForBrowser(browser);
return {uri: state.documentURI, tab: tab, browser: browser, types: types};
});
},
swapBrowserForNotification: function(aOldBrowser, aNewBrowser) {
for (let stream of this._streams) {
if (stream.browser == aOldBrowser)
stream.browser = aNewBrowser;
}
},
forgetStreamsFromBrowser: function(aBrowser) {
this._streams = this._streams.filter(stream => stream.browser != aBrowser);
},
showSharingDoorhanger: function(aActiveStream, aType) {
let browserWindow = aActiveStream.browser.ownerGlobal;
if (aActiveStream.tab) {
browserWindow.gBrowser.selectedTab = aActiveStream.tab;
} else {
aActiveStream.browser.focus();
}
browserWindow.focus();
let identityBox = browserWindow.document.getElementById("identity-box");
if (AppConstants.platform == "macosx" && !Services.focus.activeWindow) {
browserWindow.addEventListener("activate", function onActivate() {
browserWindow.removeEventListener("activate", onActivate);
Services.tm.mainThread.dispatch(function() {
identityBox.click();
}, Ci.nsIThread.DISPATCH_NORMAL);
});
Cc["@mozilla.org/widget/macdocksupport;1"].getService(Ci.nsIMacDockSupport)
.activateApplication(true);
return;
}
identityBox.click();
},
updateMainActionLabel: function(aMenuList) {
let type = aMenuList.selectedItem.getAttribute("devicetype");
let document = aMenuList.ownerDocument;
document.getElementById("webRTC-all-windows-shared").hidden = type != "Screen";
// If we are also requesting audio in addition to screen sharing,
// always use a generic label.
if (!document.getElementById("webRTC-selectMicrophone").hidden)
type = "";
let bundle = document.defaultView.gNavigatorBundle;
let stringId = "getUserMedia.share" + (type || "SelectedItems") + ".label";
let popupnotification = aMenuList.parentNode.parentNode;
popupnotification.setAttribute("buttonlabel", bundle.getString(stringId));
},
receiveMessage: function(aMessage) {
switch (aMessage.name) {
// Add-ons can override stock permission behavior by doing:
//
// var stockReceiveMessage = webrtcUI.receiveMessage;
//
// webrtcUI.receiveMessage = function(aMessage) {
// switch (aMessage.name) {
// case "rtcpeer:Request": {
// // new code.
// break;
// ...
// default:
// return stockReceiveMessage.call(this, aMessage);
//
// Intercepting gUM and peerConnection requests should let an add-on
// limit PeerConnection activity with automatic rules and/or prompts
// in a sensible manner that avoids double-prompting in typical
// gUM+PeerConnection scenarios. For example:
//
// State Sample Action
// --------------------------------------------------------------
// No IP leaked yet + No gUM granted Warn user
// No IP leaked yet + gUM granted Avoid extra dialog
// No IP leaked yet + gUM request pending. Delay until gUM grant
// IP already leaked Too late to warn
case "rtcpeer:Request": {
// Always allow. This code-point exists for add-ons to override.
let { callID, windowID } = aMessage.data;
// Also available: isSecure, innerWindowID. For contentWindow:
//
// let contentWindow = Services.wm.getOuterWindowWithId(windowID);
let mm = aMessage.target.messageManager;
mm.sendAsyncMessage("rtcpeer:Allow",
{ callID: callID, windowID: windowID });
break;
}
case "rtcpeer:CancelRequest":
// No data to release. This code-point exists for add-ons to override.
break;
case "webrtc:Request":
prompt(aMessage.target, aMessage.data);
break;
case "webrtc:CancelRequest":
removePrompt(aMessage.target, aMessage.data);
break;
case "webrtc:UpdatingIndicators":
webrtcUI._streams = [];
break;
case "webrtc:UpdateGlobalIndicators":
updateIndicators(aMessage.data, aMessage.target);
break;
case "webrtc:UpdateBrowserIndicators":
let id = aMessage.data.windowId;
let index;
for (index = 0; index < webrtcUI._streams.length; ++index) {
if (webrtcUI._streams[index].state.windowId == id)
break;
}
// If there's no documentURI, the update is actually a removal of the
// stream, triggered by the recording-window-ended notification.
if (!aMessage.data.documentURI && index < webrtcUI._streams.length)
webrtcUI._streams.splice(index, 1);
else
webrtcUI._streams[index] = {browser: aMessage.target, state: aMessage.data};
let tabbrowser = aMessage.target.ownerGlobal.gBrowser;
if (tabbrowser)
tabbrowser.setBrowserSharing(aMessage.target, aMessage.data);
break;
case "child-process-shutdown":
webrtcUI.processIndicators.delete(aMessage.target);
updateIndicators(null, null);
break;
}
}
};
function getBrowserForWindow(aContentWindow) {
return aContentWindow.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIWebNavigation)
.QueryInterface(Ci.nsIDocShell)
.chromeEventHandler;
}
function denyRequest(aBrowser, aRequest) {
aBrowser.messageManager.sendAsyncMessage("webrtc:Deny",
{callID: aRequest.callID,
windowID: aRequest.windowID});
}
function getHost(uri, href) {
let host;
try {
if (!uri) {
uri = Services.io.newURI(href, null, null);
}
host = uri.host;
} catch (ex) {}
if (!host) {
if (uri && uri.scheme.toLowerCase() == "about") {
// For about URIs, just use the full spec, without any #hash parts.
host = uri.specIgnoringRef;
} else {
// This is unfortunate, but we should display *something*...
const kBundleURI = "chrome://browser/locale/browser.properties";
let bundle = Services.strings.createBundle(kBundleURI);
host = bundle.GetStringFromName("getUserMedia.sharingMenuUnknownHost");
}
}
return host;
}
function prompt(aBrowser, aRequest) {
let {audioDevices: audioDevices, videoDevices: videoDevices,
sharingScreen: sharingScreen, sharingAudio: sharingAudio,
requestTypes: requestTypes} = aRequest;
let uri;
try {
// This fails for principals that serialize to "null", e.g. file URIs.
uri = Services.io.newURI(aRequest.origin, null, null);
} catch (e) {
uri = Services.io.newURI(aRequest.documentURI, null, null);
}
let host = getHost(uri);
let chromeDoc = aBrowser.ownerDocument;
let chromeWin = chromeDoc.defaultView;
let stringBundle = chromeWin.gNavigatorBundle;
let stringId = "getUserMedia.share" + requestTypes.join("And") + ".message";
let message = stringBundle.getFormattedString(stringId, [host]);
let mainLabel;
if (sharingScreen || sharingAudio) {
mainLabel = stringBundle.getString("getUserMedia.shareSelectedItems.label");
} else {
let string = stringBundle.getString("getUserMedia.shareSelectedDevices.label");
mainLabel = PluralForm.get(requestTypes.length, string);
}
let notification; // Used by action callbacks.
let mainAction = {
label: mainLabel,
accessKey: stringBundle.getString("getUserMedia.shareSelectedDevices.accesskey"),
// The real callback will be set during the "showing" event. The
// empty function here is so that PopupNotifications.show doesn't
// reject the action.
callback: function() {}
};
let secondaryActions = [
{
label: stringBundle.getString("getUserMedia.denyRequest.label"),
accessKey: stringBundle.getString("getUserMedia.denyRequest.accesskey"),
callback: function () {
denyRequest(notification.browser, aRequest);
}
}
];
// Bug 1037438: implement 'never' for screen sharing.
if (!sharingScreen && !sharingAudio) {
secondaryActions.push({
label: stringBundle.getString("getUserMedia.never.label"),
accessKey: stringBundle.getString("getUserMedia.never.accesskey"),
callback: function () {
denyRequest(notification.browser, aRequest);
// Let someone save "Never" for http sites so that they can be stopped from
// bothering you with doorhangers.
let perms = Services.perms;
if (audioDevices.length)
perms.add(uri, "microphone", perms.DENY_ACTION);
if (videoDevices.length)
perms.add(uri, "camera", perms.DENY_ACTION);
}
});
}
if (aRequest.secure && !sharingScreen && !sharingAudio) {
// Don't show the 'Always' action if the connection isn't secure, or for
// screen/audio sharing (because we can't guess which window the user wants
// to share without prompting).
secondaryActions.unshift({
label: stringBundle.getString("getUserMedia.always.label"),
accessKey: stringBundle.getString("getUserMedia.always.accesskey"),
callback: function (aState) {
mainAction.callback(aState, true);
}
});
}
let options = {
eventCallback: function(aTopic, aNewBrowser) {
if (aTopic == "swapping")
return true;
let chromeDoc = this.browser.ownerDocument;
// Clean-up video streams of screensharing previews.
if ((aTopic == "dismissed" || aTopic == "removed") &&
requestTypes.includes("Screen")) {
let video = chromeDoc.getElementById("webRTC-previewVideo");
video.deviceId = undefined;
if (video.stream) {
video.stream.getTracks().forEach(t => t.stop());
video.stream = null;
video.src = null;
chromeDoc.getElementById("webRTC-preview").hidden = true;
}
let menupopup = chromeDoc.getElementById("webRTC-selectWindow-menupopup");
if (menupopup._commandEventListener) {
menupopup.removeEventListener("command", menupopup._commandEventListener);
menupopup._commandEventListener = null;
}
}
if (aTopic != "showing")
return false;
// DENY_ACTION is handled immediately by MediaManager, but handling
// of ALLOW_ACTION is delayed until the popupshowing event
// to avoid granting permissions automatically to background tabs.
if (aRequest.secure) {
let perms = Services.perms;
let micPerm = perms.testExactPermission(uri, "microphone");
if (micPerm == perms.PROMPT_ACTION)
micPerm = perms.UNKNOWN_ACTION;
let camPerm = perms.testExactPermission(uri, "camera");
let mediaManagerPerm =
perms.testExactPermission(uri, "MediaManagerVideo");
if (mediaManagerPerm) {
perms.remove(uri, "MediaManagerVideo");
}
if (camPerm == perms.PROMPT_ACTION)
camPerm = perms.UNKNOWN_ACTION;
// Screen sharing shouldn't follow the camera permissions.
if (videoDevices.length && sharingScreen)
camPerm = perms.UNKNOWN_ACTION;
// We don't check that permissions are set to ALLOW_ACTION in this
// test; only that they are set. This is because if audio is allowed
// and video is denied persistently, we don't want to show the prompt,
// and will grant audio access immediately.
if ((!audioDevices.length || micPerm) && (!videoDevices.length || camPerm)) {
// All permissions we were about to request are already persistently set.
let allowedDevices = [];
if (videoDevices.length && camPerm == perms.ALLOW_ACTION) {
allowedDevices.push(videoDevices[0].deviceIndex);
let perms = Services.perms;
perms.add(uri, "MediaManagerVideo", perms.ALLOW_ACTION,
perms.EXPIRE_SESSION);
}
if (audioDevices.length && micPerm == perms.ALLOW_ACTION)
allowedDevices.push(audioDevices[0].deviceIndex);
// Remember on which URIs we found persistent permissions so that we
// can remove them if the user clicks 'Stop Sharing'. There's no
// other way for the stop sharing code to know the hostnames of frames
// using devices until bug 1066082 is fixed.
let browser = this.browser;
browser._devicePermissionURIs = browser._devicePermissionURIs || [];
browser._devicePermissionURIs.push(uri);
let mm = browser.messageManager;
mm.sendAsyncMessage("webrtc:Allow", {callID: aRequest.callID,
windowID: aRequest.windowID,
devices: allowedDevices});
this.remove();
return true;
}
}
function listDevices(menupopup, devices) {
while (menupopup.lastChild)
menupopup.removeChild(menupopup.lastChild);
for (let device of devices)
addDeviceToList(menupopup, device.name, device.deviceIndex);
}
function listScreenShareDevices(menupopup, devices) {
while (menupopup.lastChild)
menupopup.removeChild(menupopup.lastChild);
let type = devices[0].mediaSource;
let typeName = type.charAt(0).toUpperCase() + type.substr(1);
let label = chromeDoc.getElementById("webRTC-selectWindow-label");
let stringId = "getUserMedia.select" + typeName;
label.setAttribute("value",
stringBundle.getString(stringId + ".label"));
label.setAttribute("accesskey",
stringBundle.getString(stringId + ".accesskey"));
// "No <type>" is the default because we can't pick a
// 'default' window to share.
addDeviceToList(menupopup,
stringBundle.getString("getUserMedia.no" + typeName + ".label"),
"-1");
menupopup.appendChild(chromeDoc.createElement("menuseparator"));
// Build the list of 'devices'.
let monitorIndex = 1;
for (let i = 0; i < devices.length; ++i) {
let device = devices[i];
let name;
// Building screen list from available screens.
if (type == "screen") {
if (device.name == "Primary Monitor") {
name = stringBundle.getString("getUserMedia.shareEntireScreen.label");
} else {
name = stringBundle.getFormattedString("getUserMedia.shareMonitor.label",
[monitorIndex]);
++monitorIndex;
}
}
else {
name = device.name;
if (type == "application") {
// The application names returned by the platform are of the form:
// <window count>\x1e<application name>
let sepIndex = name.indexOf("\x1e");
let count = name.slice(0, sepIndex);
let stringId = "getUserMedia.shareApplicationWindowCount.label";
name = PluralForm.get(parseInt(count), stringBundle.getString(stringId))
.replace("#1", name.slice(sepIndex + 1))
.replace("#2", count);
}
}
let item = addDeviceToList(menupopup, name, i, typeName);
item.deviceId = device.id;
if (device.scary)
item.scary = true;
}
// Always re-select the "No <type>" item.
chromeDoc.getElementById("webRTC-selectWindow-menulist").removeAttribute("value");
chromeDoc.getElementById("webRTC-all-windows-shared").hidden = true;
menupopup._commandEventListener = event => {
let video = chromeDoc.getElementById("webRTC-previewVideo");
if (video.stream) {
video.stream.getTracks().forEach(t => t.stop());
video.stream = null;
}
let deviceId = event.target.deviceId;
if (deviceId == undefined) {
chromeDoc.getElementById("webRTC-preview").hidden = true;
video.src = null;
return;
}
let scary = event.target.scary;
let warning = chromeDoc.getElementById("webRTC-previewWarning");
warning.hidden = !scary;
let chromeWin = chromeDoc.defaultView;
if (scary) {
warning.hidden = false;
let string;
let bundle = chromeWin.gNavigatorBundle;
let learnMoreText =
bundle.getString("getUserMedia.shareScreen.learnMoreLabel");
let baseURL =
Services.urlFormatter.formatURLPref("app.support.baseURL");
let learnMore =
"<label class='text-link' href='" + baseURL + "screenshare-safety'>" +
learnMoreText + "</label>";
if (type == "screen") {
string = bundle.getFormattedString("getUserMedia.shareScreenWarning.message",
[learnMore]);
}
else {
let brand =
chromeDoc.getElementById("bundle_brand").getString("brandShortName");
string = bundle.getFormattedString("getUserMedia.shareFirefoxWarning.message",
[brand, learnMore]);
}
warning.innerHTML = string;
}
let perms = Services.perms;
let chromeUri = Services.io.newURI(chromeDoc.documentURI, null, null);
perms.add(chromeUri, "MediaManagerVideo", perms.ALLOW_ACTION,
perms.EXPIRE_SESSION);
video.deviceId = deviceId;
let constraints = { video: { mediaSource: type, deviceId: {exact: deviceId } } };
chromeWin.navigator.mediaDevices.getUserMedia(constraints).then(stream => {
if (video.deviceId != deviceId) {
// The user has selected a different device or closed the panel
// before getUserMedia finished.
stream.getTracks().forEach(t => t.stop());
return;
}
video.src = chromeWin.URL.createObjectURL(stream);
video.stream = stream;
chromeDoc.getElementById("webRTC-preview").hidden = false;
video.onloadedmetadata = function(e) {
video.play();
};
});
};
menupopup.addEventListener("command", menupopup._commandEventListener);
}
function addDeviceToList(menupopup, deviceName, deviceIndex, type) {
let menuitem = chromeDoc.createElement("menuitem");
menuitem.setAttribute("value", deviceIndex);
menuitem.setAttribute("label", deviceName);
menuitem.setAttribute("tooltiptext", deviceName);
if (type)
menuitem.setAttribute("devicetype", type);
menupopup.appendChild(menuitem);
return menuitem;
}
chromeDoc.getElementById("webRTC-selectCamera").hidden = !videoDevices.length || sharingScreen;
chromeDoc.getElementById("webRTC-selectWindowOrScreen").hidden = !sharingScreen || !videoDevices.length;
chromeDoc.getElementById("webRTC-selectMicrophone").hidden = !audioDevices.length || sharingAudio;
let camMenupopup = chromeDoc.getElementById("webRTC-selectCamera-menupopup");
let windowMenupopup = chromeDoc.getElementById("webRTC-selectWindow-menupopup");
let micMenupopup = chromeDoc.getElementById("webRTC-selectMicrophone-menupopup");
if (sharingScreen)
listScreenShareDevices(windowMenupopup, videoDevices);
else
listDevices(camMenupopup, videoDevices);
if (!sharingAudio)
listDevices(micMenupopup, audioDevices);
this.mainAction.callback = function(aState, aRemember) {
let allowedDevices = [];
let perms = Services.perms;
if (videoDevices.length) {
let listId = "webRTC-select" + (sharingScreen ? "Window" : "Camera") + "-menulist";
let videoDeviceIndex = chromeDoc.getElementById(listId).value;
let allowCamera = videoDeviceIndex != "-1";
if (allowCamera) {
allowedDevices.push(videoDeviceIndex);
// Session permission will be removed after use
// (it's really one-shot, not for the entire session)
perms.add(uri, "MediaManagerVideo", perms.ALLOW_ACTION,
perms.EXPIRE_SESSION);
}
if (aRemember) {
perms.add(uri, "camera",
allowCamera ? perms.ALLOW_ACTION : perms.DENY_ACTION);
}
}
if (audioDevices.length) {
if (!sharingAudio) {
let audioDeviceIndex = chromeDoc.getElementById("webRTC-selectMicrophone-menulist").value;
let allowMic = audioDeviceIndex != "-1";
if (allowMic)
allowedDevices.push(audioDeviceIndex);
if (aRemember) {
perms.add(uri, "microphone",
allowMic ? perms.ALLOW_ACTION : perms.DENY_ACTION);
}
} else {
// Only one device possible for audio capture.
allowedDevices.push(0);
}
}
if (!allowedDevices.length) {
denyRequest(notification.browser, aRequest);
return;
}
if (aRemember) {
// Remember on which URIs we set persistent permissions so that we
// can remove them if the user clicks 'Stop Sharing'.
aBrowser._devicePermissionURIs = aBrowser._devicePermissionURIs || [];
aBrowser._devicePermissionURIs.push(uri);
}
let mm = notification.browser.messageManager;
mm.sendAsyncMessage("webrtc:Allow", {callID: aRequest.callID,
windowID: aRequest.windowID,
devices: allowedDevices});
};
return false;
}
};
let iconType = "Devices";
if (requestTypes.length == 1 && (requestTypes[0] == "Microphone" ||
requestTypes[0] == "AudioCapture"))
iconType = "Microphone";
if (requestTypes.includes("Screen"))
iconType = "Screen";
let anchorId = "webRTC-share" + iconType + "-notification-icon";
let iconClass = iconType.toLowerCase();
if (iconClass == "devices")
iconClass = "camera";
options.popupIconClass = iconClass + "-icon";
notification =
chromeWin.PopupNotifications.show(aBrowser, "webRTC-shareDevices", message,
anchorId, mainAction, secondaryActions,
options);
notification.callID = aRequest.callID;
}
function removePrompt(aBrowser, aCallId) {
let chromeWin = aBrowser.ownerGlobal;
let notification =
chromeWin.PopupNotifications.getNotification("webRTC-shareDevices", aBrowser);
if (notification && notification.callID == aCallId)
notification.remove();
}
function getGlobalIndicator() {
if (AppConstants.platform != "macosx") {
const INDICATOR_CHROME_URI = "chrome://browser/content/webrtcIndicator.xul";
const features = "chrome,dialog=yes,titlebar=no,popup=yes";
return Services.ww.openWindow(null, INDICATOR_CHROME_URI, "_blank", features, []);
}
let indicator = {
_camera: null,
_microphone: null,
_screen: null,
_hiddenDoc: Cc["@mozilla.org/appshell/appShellService;1"]
.getService(Ci.nsIAppShellService)
.hiddenDOMWindow.document,
_statusBar: Cc["@mozilla.org/widget/macsystemstatusbar;1"]
.getService(Ci.nsISystemStatusBar),
_command: function(aEvent) {
let type = this.getAttribute("type");
if (type == "Camera" || type == "Microphone")
type = "Devices";
else if (type == "Window" || type == "Application" || type == "Browser")
type = "Screen";
webrtcUI.showSharingDoorhanger(aEvent.target.stream, type);
},
_popupShowing: function(aEvent) {
let type = this.getAttribute("type");
let activeStreams;
if (type == "Camera") {
activeStreams = webrtcUI.getActiveStreams(true, false, false);
}
else if (type == "Microphone") {
activeStreams = webrtcUI.getActiveStreams(false, true, false);
}
else if (type == "Screen") {
activeStreams = webrtcUI.getActiveStreams(false, false, true);
type = webrtcUI.showScreenSharingIndicator;
}
let bundle =
Services.strings.createBundle("chrome://browser/locale/webrtcIndicator.properties");
if (activeStreams.length == 1) {
let stream = activeStreams[0];
let menuitem = this.ownerDocument.createElement("menuitem");
let labelId = "webrtcIndicator.sharing" + type + "With.menuitem";
let label = stream.browser.contentTitle || stream.uri;
menuitem.setAttribute("label", bundle.formatStringFromName(labelId, [label], 1));
menuitem.setAttribute("disabled", "true");
this.appendChild(menuitem);
menuitem = this.ownerDocument.createElement("menuitem");
menuitem.setAttribute("label",
bundle.GetStringFromName("webrtcIndicator.controlSharing.menuitem"));
menuitem.setAttribute("type", type);
menuitem.stream = stream;
menuitem.addEventListener("command", indicator._command);
this.appendChild(menuitem);
return true;
}
// We show a different menu when there are several active streams.
let menuitem = this.ownerDocument.createElement("menuitem");
let labelId = "webrtcIndicator.sharing" + type + "WithNTabs.menuitem";
let count = activeStreams.length;
let label = PluralForm.get(count, bundle.GetStringFromName(labelId)).replace("#1", count);
menuitem.setAttribute("label", label);
menuitem.setAttribute("disabled", "true");
this.appendChild(menuitem);
for (let stream of activeStreams) {
let item = this.ownerDocument.createElement("menuitem");
let labelId = "webrtcIndicator.controlSharingOn.menuitem";
let label = stream.browser.contentTitle || stream.uri;
item.setAttribute("label", bundle.formatStringFromName(labelId, [label], 1));
item.setAttribute("type", type);
item.stream = stream;
item.addEventListener("command", indicator._command);
this.appendChild(item);
}
return true;
},
_popupHiding: function(aEvent) {
while (this.firstChild)
this.firstChild.remove();
},
_setIndicatorState: function(aName, aState) {
let field = "_" + aName.toLowerCase();
if (aState && !this[field]) {
let menu = this._hiddenDoc.createElement("menu");
menu.setAttribute("id", "webRTC-sharing" + aName + "-menu");
// The CSS will only be applied if the menu is actually inserted in the DOM.
this._hiddenDoc.documentElement.appendChild(menu);
this._statusBar.addItem(menu);
let menupopup = this._hiddenDoc.createElement("menupopup");
menupopup.setAttribute("type", aName);
menupopup.addEventListener("popupshowing", this._popupShowing);
menupopup.addEventListener("popuphiding", this._popupHiding);
menupopup.addEventListener("command", this._command);
menu.appendChild(menupopup);
this[field] = menu;
}
else if (this[field] && !aState) {
this._statusBar.removeItem(this[field]);
this[field].remove();
this[field] = null
}
},
updateIndicatorState: function() {
this._setIndicatorState("Camera", webrtcUI.showCameraIndicator);
this._setIndicatorState("Microphone", webrtcUI.showMicrophoneIndicator);
this._setIndicatorState("Screen", webrtcUI.showScreenSharingIndicator);
},
close: function() {
this._setIndicatorState("Camera", false);
this._setIndicatorState("Microphone", false);
this._setIndicatorState("Screen", false);
}
};
indicator.updateIndicatorState();
return indicator;
}
function onTabSharingMenuPopupShowing(e) {
let streams = webrtcUI.getActiveStreams(true, true, true);
for (let streamInfo of streams) {
let stringName = "getUserMedia.sharingMenu";
let types = streamInfo.types;
if (types.camera)
stringName += "Camera";
if (types.microphone)
stringName += "Microphone";
if (types.screen)
stringName += types.screen;
let doc = e.target.ownerDocument;
let bundle = doc.defaultView.gNavigatorBundle;
let origin = getHost(null, streamInfo.uri);
let menuitem = doc.createElement("menuitem");
menuitem.setAttribute("label", bundle.getFormattedString(stringName, [origin]));
menuitem.stream = streamInfo;
// We can only open 1 doorhanger at a time. Guessing that users would be
// most eager to control screen/window/app sharing, and only then
// camera/microphone sharing, in that (decreasing) order of priority.
let doorhangerType;
if ((/Screen|Window|Application/).test(stringName)) {
doorhangerType = "Screen";
} else {
doorhangerType = "Devices";
}
menuitem.setAttribute("doorhangertype", doorhangerType);
menuitem.addEventListener("command", onTabSharingMenuPopupCommand);
e.target.appendChild(menuitem);
}
}
function onTabSharingMenuPopupHiding(e) {
while (this.lastChild)
this.lastChild.remove();
}
function onTabSharingMenuPopupCommand(e) {
let type = e.target.getAttribute("doorhangertype");
webrtcUI.showSharingDoorhanger(e.target.stream, type);
}
function showOrCreateMenuForWindow(aWindow) {
let document = aWindow.document;
let menu = document.getElementById("tabSharingMenu");
if (!menu) {
let stringBundle = aWindow.gNavigatorBundle;
menu = document.createElement("menu");
menu.id = "tabSharingMenu";
let labelStringId = "getUserMedia.sharingMenu.label";
menu.setAttribute("label", stringBundle.getString(labelStringId));
let container, insertionPoint;
if (AppConstants.platform == "macosx") {
container = document.getElementById("windowPopup");
insertionPoint = document.getElementById("sep-window-list");
let separator = document.createElement("menuseparator");
separator.id = "tabSharingSeparator";
container.insertBefore(separator, insertionPoint);
} else {
let accesskeyStringId = "getUserMedia.sharingMenu.accesskey";
menu.setAttribute("accesskey", stringBundle.getString(accesskeyStringId));
container = document.getElementById("main-menubar");
insertionPoint = document.getElementById("helpMenu");
}
let popup = document.createElement("menupopup");
popup.id = "tabSharingMenuPopup";
popup.addEventListener("popupshowing", onTabSharingMenuPopupShowing);
popup.addEventListener("popuphiding", onTabSharingMenuPopupHiding);
menu.appendChild(popup);
container.insertBefore(menu, insertionPoint);
} else {
menu.hidden = false;
if (AppConstants.platform == "macosx") {
document.getElementById("tabSharingSeparator").hidden = false;
}
}
}
function maybeAddMenuIndicator(window) {
if (webrtcUI.showGlobalIndicator) {
showOrCreateMenuForWindow(window);
}
}
var gIndicatorWindow = null;
function updateIndicators(data, target) {
if (data) {
// the global indicators specific to this process
let indicators;
if (webrtcUI.processIndicators.has(target)) {
indicators = webrtcUI.processIndicators.get(target);
} else {
indicators = {};
webrtcUI.processIndicators.set(target, indicators);
}
indicators.showGlobalIndicator = data.showGlobalIndicator;
indicators.showCameraIndicator = data.showCameraIndicator;
indicators.showMicrophoneIndicator = data.showMicrophoneIndicator;
indicators.showScreenSharingIndicator = data.showScreenSharingIndicator;
}
let browserWindowEnum = Services.wm.getEnumerator("navigator:browser");
while (browserWindowEnum.hasMoreElements()) {
let chromeWin = browserWindowEnum.getNext();
if (webrtcUI.showGlobalIndicator) {
showOrCreateMenuForWindow(chromeWin);
} else {
let doc = chromeWin.document;
let existingMenu = doc.getElementById("tabSharingMenu");
if (existingMenu) {
existingMenu.hidden = true;
}
if (AppConstants.platform == "macosx") {
let separator = doc.getElementById("tabSharingSeparator");
if (separator) {
separator.hidden = true;
}
}
}
}
if (webrtcUI.showGlobalIndicator) {
if (!gIndicatorWindow)
gIndicatorWindow = getGlobalIndicator();
else
gIndicatorWindow.updateIndicatorState();
} else if (gIndicatorWindow) {
gIndicatorWindow.close();
gIndicatorWindow = null;
}
}