/* -*- 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); }, 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(); }, 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(); }); }, 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); }, 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}); }, 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); }, _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();