gecko-dev/toolkit/content/browser-content.js

605 строки
20 KiB
JavaScript

/* -*- 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/. */
/* eslint-env mozilla/frame-script */
/* eslint no-unused-vars: ["error", {args: "none"}] */
/* global sendAsyncMessage */
ChromeUtils.import("resource://gre/modules/Services.jsm");
ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
ChromeUtils.defineModuleGetter(this, "AutoCompletePopup",
"resource://gre/modules/AutoCompletePopupContent.jsm");
ChromeUtils.defineModuleGetter(this, "AutoScrollController",
"resource://gre/modules/AutoScrollController.jsm");
ChromeUtils.defineModuleGetter(this, "BrowserUtils",
"resource://gre/modules/BrowserUtils.jsm");
ChromeUtils.defineModuleGetter(this, "SelectContentHelper",
"resource://gre/modules/SelectContentHelper.jsm");
ChromeUtils.defineModuleGetter(this, "FindContent",
"resource://gre/modules/FindContent.jsm");
ChromeUtils.defineModuleGetter(this, "PrintingContent",
"resource://gre/modules/PrintingContent.jsm");
ChromeUtils.defineModuleGetter(this, "RemoteFinder",
"resource://gre/modules/RemoteFinder.jsm");
XPCOMUtils.defineLazyServiceGetter(this, "formFill",
"@mozilla.org/satchel/form-fill-controller;1",
"nsIFormFillController");
var global = this;
XPCOMUtils.defineLazyProxy(this, "PopupBlocking", () => {
let tmp = {};
ChromeUtils.import("resource://gre/modules/PopupBlocking.jsm", tmp);
return new tmp.PopupBlocking(global);
});
XPCOMUtils.defineLazyProxy(this, "SelectionSourceContent",
"resource://gre/modules/SelectionSourceContent.jsm");
XPCOMUtils.defineLazyProxy(this, "DateTimePickerContent", () => {
let tmp = {};
ChromeUtils.import("resource://gre/modules/DateTimePickerContent.jsm", tmp);
return new tmp.DateTimePickerContent(this);
});
// Lazily load the finder code
addMessageListener("Finder:Initialize", function() {
let {RemoteFinderListener} = ChromeUtils.import("resource://gre/modules/RemoteFinder.jsm", {});
new RemoteFinderListener(global);
});
var AutoScrollListener = {
handleEvent(event) {
if (event.isTrusted &
!event.defaultPrevented &&
event.button == 1) {
if (!this._controller) {
this._controller = new AutoScrollController(global);
}
this._controller.handleEvent(event);
}
}
};
Services.els.addSystemEventListener(global, "mousedown", AutoScrollListener, true);
addEventListener("MozOpenDateTimePicker", DateTimePickerContent);
addEventListener("DOMPopupBlocked", PopupBlocking, true);
var Printing = {
MESSAGES: [
"Printing:Preview:Enter",
"Printing:Preview:Exit",
"Printing:Preview:Navigate",
"Printing:Preview:ParseDocument",
"Printing:Print",
],
init() {
this.MESSAGES.forEach(msgName => addMessageListener(msgName, this));
addEventListener("PrintingError", this, true);
addEventListener("printPreviewUpdate", this, true);
this.init = null;
},
handleEvent(event) {
return PrintingContent.handleEvent(global, event);
},
receiveMessage(message) {
return PrintingContent.receiveMessage(global, message);
},
};
Printing.init();
function SwitchDocumentDirection(aWindow) {
// document.dir can also be "auto", in which case it won't change
if (aWindow.document.dir == "ltr" || aWindow.document.dir == "") {
aWindow.document.dir = "rtl";
} else if (aWindow.document.dir == "rtl") {
aWindow.document.dir = "ltr";
}
for (let run = 0; run < aWindow.frames.length; run++) {
SwitchDocumentDirection(aWindow.frames[run]);
}
}
addMessageListener("SwitchDocumentDirection", () => {
SwitchDocumentDirection(content.window);
});
var FindBar = {
/* Please keep in sync with toolkit/content/widgets/findbar.xml */
FIND_NORMAL: 0,
FIND_TYPEAHEAD: 1,
FIND_LINKS: 2,
_findMode: 0,
/**
* _findKey and _findModifiers are used to determine whether a keypress
* is a user attempting to use the find shortcut, after which we'll
* route keypresses to the parent until we know the findbar has focus
* there. To do this, we need shortcut data from the parent.
*/
_findKey: null,
_findModifiers: null,
init() {
addMessageListener("Findbar:UpdateState", this);
Services.els.addSystemEventListener(global, "keypress", this, false);
Services.els.addSystemEventListener(global, "mouseup", this, false);
this._initShortcutData();
this.init = null;
},
receiveMessage(msg) {
switch (msg.name) {
case "Findbar:UpdateState":
this._findMode = msg.data.findMode;
this._quickFindTimeout = msg.data.hasQuickFindTimeout;
if (msg.data.isOpenAndFocused) {
this._keepPassingUntilToldOtherwise = false;
}
break;
case "Findbar:ShortcutData":
// Set us up to never need this again for the lifetime of this process,
// and remove the listener.
Services.cpmm.initialProcessData.findBarShortcutData = msg.data;
Services.cpmm.removeMessageListener("Findbar:ShortcutData", this);
this._initShortcutData(msg.data);
break;
}
},
handleEvent(event) {
switch (event.type) {
case "keypress":
this._onKeypress(event);
break;
case "mouseup":
this._onMouseup(event);
break;
}
},
/**
* Use initial process data for find key/modifier data if we have it.
* Otherwise, add a listener so we get the data when the parent process has
* it.
*/
_initShortcutData(data = Services.cpmm.initialProcessData.findBarShortcutData) {
if (data) {
this._findKey = data.key;
this._findModifiers = data.modifiers;
} else {
Services.cpmm.addMessageListener("Findbar:ShortcutData", this);
}
},
/**
* Check whether this key event will start the findbar in the parent,
* in which case we should pass any further key events to the parent to avoid
* them being lost.
* @param aEvent the key event to check.
*/
_eventMatchesFindShortcut(aEvent) {
let modifiers = this._findModifiers;
if (!modifiers) {
return false;
}
return aEvent.ctrlKey == modifiers.ctrlKey && aEvent.altKey == modifiers.altKey &&
aEvent.shiftKey == modifiers.shiftKey && aEvent.metaKey == modifiers.metaKey &&
aEvent.key == this._findKey;
},
/**
* Returns whether FAYT can be used for the given event in
* the current content state.
*/
_canAndShouldFastFind() {
let should = false;
let can = BrowserUtils.canFastFind(content);
if (can) {
// XXXgijs: why all these shenanigans? Why not use the event's target?
let focusedWindow = {};
let elt = Services.focus.getFocusedElementForWindow(content, true, focusedWindow);
let win = focusedWindow.value;
should = BrowserUtils.shouldFastFind(elt, win);
}
return { can, should };
},
_onKeypress(event) {
const FAYT_LINKS_KEY = "'";
const FAYT_TEXT_KEY = "/";
if (this._eventMatchesFindShortcut(event)) {
this._keepPassingUntilToldOtherwise = true;
}
// Useless keys:
if (event.ctrlKey || event.altKey || event.metaKey || event.defaultPrevented) {
return;
}
// Check the focused element etc.
let fastFind = this._canAndShouldFastFind();
// Can we even use find in this page at all?
if (!fastFind.can) {
return;
}
if (this._keepPassingUntilToldOtherwise) {
this._passKeyToParent(event);
return;
}
if (!fastFind.should) {
return;
}
let charCode = event.charCode;
// If the find bar is open and quick find is on, send the key to the parent.
if (this._findMode != this.FIND_NORMAL && this._quickFindTimeout) {
if (!charCode)
return;
this._passKeyToParent(event);
} else {
let key = charCode ? String.fromCharCode(charCode) : null;
let manualstartFAYT = (key == FAYT_LINKS_KEY || key == FAYT_TEXT_KEY) && RemoteFinder._manualFAYT;
let autostartFAYT = !manualstartFAYT && RemoteFinder._findAsYouType && key && key != " ";
if (manualstartFAYT || autostartFAYT) {
let mode = (key == FAYT_LINKS_KEY || (autostartFAYT && RemoteFinder._typeAheadLinksOnly)) ?
this.FIND_LINKS : this.FIND_TYPEAHEAD;
// Set _findMode immediately (without waiting for child->parent->child roundtrip)
// to ensure we pass any further keypresses, too.
this._findMode = mode;
this._passKeyToParent(event);
}
}
},
_passKeyToParent(event) {
event.preventDefault();
// These are the properties required to dispatch another 'real' event
// to the findbar in the parent in _dispatchKeypressEvent in findbar.xml .
// If you make changes here, verify that that method can still do its job.
const kRequiredProps = [
"type", "bubbles", "cancelable", "ctrlKey", "altKey", "shiftKey",
"metaKey", "keyCode", "charCode",
];
let fakeEvent = {};
for (let prop of kRequiredProps) {
fakeEvent[prop] = event[prop];
}
sendAsyncMessage("Findbar:Keypress", fakeEvent);
},
_onMouseup(event) {
if (this._findMode != this.FIND_NORMAL)
sendAsyncMessage("Findbar:Mouseup");
},
};
FindBar.init();
let WebChannelMessageToChromeListener = {
// Preference containing the list (space separated) of origins that are
// allowed to send non-string values through a WebChannel, mainly for
// backwards compatability. See bug 1238128 for more information.
URL_WHITELIST_PREF: "webchannel.allowObject.urlWhitelist",
// Cached list of whitelisted principals, we avoid constructing this if the
// value in `_lastWhitelistValue` hasn't changed since we constructed it last.
_cachedWhitelist: [],
_lastWhitelistValue: "",
init() {
addEventListener("WebChannelMessageToChrome", e => {
this._onMessageToChrome(e);
}, true, true);
},
_getWhitelistedPrincipals() {
let whitelist = Services.prefs.getCharPref(this.URL_WHITELIST_PREF);
if (whitelist != this._lastWhitelistValue) {
let urls = whitelist.split(/\s+/);
this._cachedWhitelist = urls.map(origin =>
Services.scriptSecurityManager.createCodebasePrincipalFromOrigin(origin));
}
return this._cachedWhitelist;
},
_onMessageToChrome(e) {
// If target is window then we want the document principal, otherwise fallback to target itself.
let principal = e.target.nodePrincipal ? e.target.nodePrincipal : e.target.document.nodePrincipal;
if (e.detail) {
if (typeof e.detail != "string") {
// Check if the principal is one of the ones that's allowed to send
// non-string values for e.detail. They're whitelisted by site origin,
// so we compare on originNoSuffix in order to avoid other origin attributes
// that are not relevant here, such as containers or private browsing.
let objectsAllowed = this._getWhitelistedPrincipals().some(whitelisted =>
principal.originNoSuffix == whitelisted.originNoSuffix);
if (!objectsAllowed) {
Cu.reportError("WebChannelMessageToChrome sent with an object from a non-whitelisted principal");
return;
}
}
sendAsyncMessage("WebChannelMessageToChrome", e.detail, { eventTarget: e.target }, principal);
} else {
Cu.reportError("WebChannel message failed. No message detail.");
}
}
};
WebChannelMessageToChromeListener.init();
// This should be kept in sync with /browser/base/content.js.
// Add message listener for "WebChannelMessageToContent" messages from chrome scripts.
addMessageListener("WebChannelMessageToContent", function(e) {
if (e.data) {
// e.objects.eventTarget will be defined if sending a response to
// a WebChannelMessageToChrome event. An unsolicited send
// may not have an eventTarget defined, in this case send to the
// main content window.
let eventTarget = e.objects.eventTarget || content;
// Use nodePrincipal if available, otherwise fallback to document principal.
let targetPrincipal = eventTarget instanceof Ci.nsIDOMWindow ? eventTarget.document.nodePrincipal : eventTarget.nodePrincipal;
if (e.principal.subsumes(targetPrincipal)) {
// If eventTarget is a window, use it as the targetWindow, otherwise
// find the window that owns the eventTarget.
let targetWindow = eventTarget instanceof Ci.nsIDOMWindow ? eventTarget : eventTarget.ownerGlobal;
eventTarget.dispatchEvent(new targetWindow.CustomEvent("WebChannelMessageToContent", {
detail: Cu.cloneInto({
id: e.data.id,
message: e.data.message,
}, targetWindow),
}));
} else {
Cu.reportError("WebChannel message failed. Principal mismatch.");
}
} else {
Cu.reportError("WebChannel message failed. No message data.");
}
});
var AudioPlaybackListener = {
QueryInterface: ChromeUtils.generateQI([Ci.nsIObserver]),
init() {
Services.obs.addObserver(this, "audio-playback");
addMessageListener("AudioPlayback", this);
addEventListener("unload", () => {
AudioPlaybackListener.uninit();
});
this.init = null;
},
uninit() {
Services.obs.removeObserver(this, "audio-playback");
removeMessageListener("AudioPlayback", this);
},
handleMediaControlMessage(msg) {
let utils = global.content.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIDOMWindowUtils);
let suspendTypes = Ci.nsISuspendedTypes;
switch (msg) {
case "mute":
utils.audioMuted = true;
break;
case "unmute":
utils.audioMuted = false;
break;
case "lostAudioFocus":
utils.mediaSuspend = suspendTypes.SUSPENDED_PAUSE_DISPOSABLE;
break;
case "lostAudioFocusTransiently":
utils.mediaSuspend = suspendTypes.SUSPENDED_PAUSE;
break;
case "gainAudioFocus":
utils.mediaSuspend = suspendTypes.NONE_SUSPENDED;
break;
case "mediaControlPaused":
utils.mediaSuspend = suspendTypes.SUSPENDED_PAUSE_DISPOSABLE;
break;
case "mediaControlStopped":
utils.mediaSuspend = suspendTypes.SUSPENDED_STOP_DISPOSABLE;
break;
case "resumeMedia":
// User has clicked the tab audio indicator to play a delayed
// media. That's clear user intent to play, so gesture activate
// the content document tree so that the block-autoplay logic
// allows the media to autoplay.
content.document.notifyUserGestureActivation();
utils.mediaSuspend = suspendTypes.NONE_SUSPENDED;
break;
default:
dump("Error : wrong media control msg!\n");
break;
}
},
observe(subject, topic, data) {
if (topic === "audio-playback") {
if (subject && subject.top == global.content) {
let name = "AudioPlayback:";
if (data === "activeMediaBlockStart") {
name += "ActiveMediaBlockStart";
} else if (data === "activeMediaBlockStop") {
name += "ActiveMediaBlockStop";
} else {
name += (data === "active") ? "Start" : "Stop";
}
sendAsyncMessage(name);
}
}
},
receiveMessage(msg) {
if (msg.name == "AudioPlayback") {
this.handleMediaControlMessage(msg.data.type);
}
},
};
AudioPlaybackListener.init();
var UnselectedTabHoverObserver = {
init() {
addMessageListener("Browser:UnselectedTabHover", this);
addEventListener("UnselectedTabHover:Enable", this);
addEventListener("UnselectedTabHover:Disable", this);
this.init = null;
},
receiveMessage(message) {
Services.obs.notifyObservers(content.window, "unselected-tab-hover",
message.data.hovered);
},
handleEvent(event) {
sendAsyncMessage("UnselectedTabHover:Toggle",
{ enable: event.type == "UnselectedTabHover:Enable" });
}
};
UnselectedTabHoverObserver.init();
addMessageListener("Browser:PurgeSessionHistory", function BrowserPurgeHistory() {
let sessionHistory = docShell.QueryInterface(Ci.nsIWebNavigation).sessionHistory;
if (!sessionHistory) {
return;
}
// place the entry at current index at the end of the history list, so it won't get removed
if (sessionHistory.index < sessionHistory.count - 1) {
let legacy = sessionHistory.legacySHistory;
legacy.QueryInterface(Ci.nsISHistoryInternal);
let indexEntry = legacy.getEntryAtIndex(sessionHistory.index, false);
indexEntry.QueryInterface(Ci.nsISHEntry);
legacy.addEntry(indexEntry, true);
}
let purge = sessionHistory.count;
if (global.content.location.href != "about:blank") {
--purge; // Don't remove the page the user's staring at from shistory
}
if (purge > 0) {
sessionHistory.legacySHistory.PurgeHistory(purge);
}
});
addMessageListener("ViewSource:GetSelection", SelectionSourceContent);
addEventListener("MozApplicationManifest", function(e) {
let doc = e.target;
let info = {
uri: doc.documentURI,
characterSet: doc.characterSet,
manifest: doc.documentElement.getAttribute("manifest"),
principal: doc.nodePrincipal,
};
sendAsyncMessage("MozApplicationManifest", info);
}, false);
let AutoComplete = {
_connected: false,
init() {
addEventListener("unload", this, {once: true});
addEventListener("DOMContentLoaded", this, {once: true});
// WebExtension browserAction is preloaded and does not receive DCL, wait
// on pageshow so we can hookup the formfill controller.
addEventListener("pageshow", this, {capture: true, once: true});
XPCOMUtils.defineLazyProxy(this, "popup", () => new AutoCompletePopup(global),
{QueryInterface: null});
this.init = null;
},
handleEvent(event) {
switch (event.type) {
case "DOMContentLoaded":
case "pageshow":
// We need to wait for a content viewer to be available
// before we can attach our AutoCompletePopup handler,
// since nsFormFillController assumes one will exist
// when we call attachToBrowser.
if (!this._connected) {
formFill.attachToBrowser(docShell, this.popup);
this._connected = true;
}
break;
case "unload":
if (this._connected) {
formFill.detachFromBrowser(docShell);
this._connected = false;
}
break;
}
},
};
AutoComplete.init();
addEventListener("mozshowdropdown", event => {
if (!event.isTrusted)
return;
if (!SelectContentHelper.open) {
new SelectContentHelper(event.target, {isOpenedViaTouch: false}, this);
}
});
addEventListener("mozshowdropdown-sourcetouch", event => {
if (!event.isTrusted)
return;
if (!SelectContentHelper.open) {
new SelectContentHelper(event.target, {isOpenedViaTouch: true}, this);
}
});
let ExtFind = {
init() {
addMessageListener("ext-Finder:CollectResults", this);
addMessageListener("ext-Finder:HighlightResults", this);
addMessageListener("ext-Finder:clearHighlighting", this);
this.init = null;
},
_findContent: null,
async receiveMessage(message) {
if (!this._findContent) {
this._findContent = new FindContent(docShell);
}
let data;
switch (message.name) {
case "ext-Finder:CollectResults":
this.finderInited = true;
data = await this._findContent.findRanges(message.data);
sendAsyncMessage("ext-Finder:CollectResultsFinished", data);
break;
case "ext-Finder:HighlightResults":
data = this._findContent.highlightResults(message.data);
sendAsyncMessage("ext-Finder:HighlightResultsFinished", data);
break;
case "ext-Finder:clearHighlighting":
this._findContent.highlighter.highlight(false);
break;
}
},
};
ExtFind.init();