mirror of
https://repo.dactyloidae.xyz/Dactyloidae/UXP.git
synced 2026-10-09 00:37:32 +09:00
Issue #303 Part 1: Move basilisk files from /browser to /application/basilisk
This commit is contained in:
parent
a951f19dfd
commit
177b28a898
1938 changed files with 0 additions and 0 deletions
175
application/basilisk/modules/AboutHome.jsm
Normal file
175
application/basilisk/modules/AboutHome.jsm
Normal 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);
|
||||
});
|
||||
},
|
||||
|
||||
};
|
||||
43
application/basilisk/modules/AboutNewTab.jsm
Normal file
43
application/basilisk/modules/AboutNewTab.jsm
Normal 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;
|
||||
},
|
||||
};
|
||||
123
application/basilisk/modules/AttributionCode.jsm
Normal file
123
application/basilisk/modules/AttributionCode.jsm
Normal 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;
|
||||
}
|
||||
},
|
||||
};
|
||||
888
application/basilisk/modules/BrowserUITelemetry.jsm
Normal file
888
application/basilisk/modules/BrowserUITelemetry.jsm
Normal 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;
|
||||
}
|
||||
468
application/basilisk/modules/BrowserUsageTelemetry.jsm
Normal file
468
application/basilisk/modules/BrowserUsageTelemetry.jsm
Normal 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);
|
||||
},
|
||||
};
|
||||
164
application/basilisk/modules/CastingApps.jsm
Normal file
164
application/basilisk/modules/CastingApps.jsm
Normal 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;
|
||||
}
|
||||
};
|
||||
98
application/basilisk/modules/ContentClick.jsm
Normal file
98
application/basilisk/modules/ContentClick.jsm
Normal 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);
|
||||
}
|
||||
};
|
||||
922
application/basilisk/modules/ContentCrashHandlers.jsm
Normal file
922
application/basilisk/modules/ContentCrashHandlers.jsm
Normal 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);
|
||||
},
|
||||
};
|
||||
147
application/basilisk/modules/ContentLinkHandler.jsm
Normal file
147
application/basilisk/modules/ContentLinkHandler.jsm
Normal 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;
|
||||
},
|
||||
};
|
||||
55
application/basilisk/modules/ContentObservers.jsm
Normal file
55
application/basilisk/modules/ContentObservers.jsm
Normal 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);
|
||||
566
application/basilisk/modules/ContentSearch.jsm
Normal file
566
application/basilisk/modules/ContentSearch.jsm
Normal 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;
|
||||
},
|
||||
};
|
||||
393
application/basilisk/modules/ContentWebRTC.jsm
Normal file
393
application/basilisk/modules/ContentWebRTC.jsm
Normal 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();
|
||||
}
|
||||
1255
application/basilisk/modules/DirectoryLinksProvider.jsm
Normal file
1255
application/basilisk/modules/DirectoryLinksProvider.jsm
Normal file
File diff suppressed because it is too large
Load diff
128
application/basilisk/modules/E10SUtils.jsm
Normal file
128
application/basilisk/modules/E10SUtils.jsm
Normal 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();
|
||||
}
|
||||
},
|
||||
};
|
||||
104
application/basilisk/modules/Feeds.jsm
Normal file
104
application/basilisk/modules/Feeds.jsm
Normal 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;
|
||||
},
|
||||
|
||||
};
|
||||
235
application/basilisk/modules/FormSubmitObserver.jsm
Normal file
235
application/basilisk/modules/FormSubmitObserver.jsm
Normal 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])
|
||||
};
|
||||
157
application/basilisk/modules/FormValidationHandler.jsm
Normal file
157
application/basilisk/modules/FormValidationHandler.jsm
Normal 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();
|
||||
}
|
||||
}
|
||||
};
|
||||
86
application/basilisk/modules/HiddenFrame.jsm
Normal file
86
application/basilisk/modules/HiddenFrame.jsm
Normal 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);
|
||||
}
|
||||
}
|
||||
};
|
||||
172
application/basilisk/modules/LaterRun.jsm
Normal file
172
application/basilisk/modules/LaterRun.jsm
Normal 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();
|
||||
194
application/basilisk/modules/NetworkPrioritizer.jsm
Normal file
194
application/basilisk/modules/NetworkPrioritizer.jsm
Normal 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;
|
||||
}
|
||||
};
|
||||
|
||||
595
application/basilisk/modules/PermissionUI.jsm
Normal file
595
application/basilisk/modules/PermissionUI.jsm
Normal 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;
|
||||
1132
application/basilisk/modules/PluginContent.jsm
Normal file
1132
application/basilisk/modules/PluginContent.jsm
Normal file
File diff suppressed because it is too large
Load diff
397
application/basilisk/modules/ProcessHangMonitor.jsm
Normal file
397
application/basilisk/modules/ProcessHangMonitor.jsm
Normal 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();
|
||||
},
|
||||
};
|
||||
102
application/basilisk/modules/ReaderParent.jsm
Normal file
102
application/basilisk/modules/ReaderParent.jsm
Normal 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");
|
||||
}
|
||||
};
|
||||
67
application/basilisk/modules/RecentWindow.jsm
Normal file
67
application/basilisk/modules/RecentWindow.jsm
Normal 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;
|
||||
}
|
||||
};
|
||||
|
||||
110
application/basilisk/modules/RemotePrompt.jsm
Normal file
110
application/basilisk/modules/RemotePrompt.jsm
Normal 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);
|
||||
}
|
||||
}
|
||||
};
|
||||
22
application/basilisk/modules/Sanitizer.jsm
Normal file
22
application/basilisk/modules/Sanitizer.jsm
Normal 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;
|
||||
269
application/basilisk/modules/SitePermissions.jsm
Normal file
269
application/basilisk/modules/SitePermissions.jsm
Normal 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);
|
||||
24
application/basilisk/modules/TransientPrefs.jsm
Normal file
24
application/basilisk/modules/TransientPrefs.jsm
Normal 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);
|
||||
}
|
||||
};
|
||||
51
application/basilisk/modules/URLBarZoom.jsm
Normal file
51
application/basilisk/modules/URLBarZoom.jsm
Normal 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;
|
||||
}
|
||||
}
|
||||
53
application/basilisk/modules/Windows8WindowFrameColor.jsm
Normal file
53
application/basilisk/modules/Windows8WindowFrameColor.jsm
Normal 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];
|
||||
},
|
||||
};
|
||||
579
application/basilisk/modules/WindowsJumpLists.jsm
Normal file
579
application/basilisk/modules/WindowsJumpLists.jsm
Normal 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;
|
||||
}
|
||||
},
|
||||
};
|
||||
862
application/basilisk/modules/WindowsPreviewPerTab.jsm
Normal file
862
application/basilisk/modules/WindowsPreviewPerTab.jsm
Normal 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();
|
||||
47
application/basilisk/modules/moz.build
Normal file
47
application/basilisk/modules/moz.build
Normal 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',
|
||||
]
|
||||
20
application/basilisk/modules/offlineAppCache.jsm
Normal file
20
application/basilisk/modules/offlineAppCache.jsm
Normal 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) {}
|
||||
}
|
||||
};
|
||||
969
application/basilisk/modules/webrtcUI.jsm
Normal file
969
application/basilisk/modules/webrtcUI.jsm
Normal 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;
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue