Bug 1505915, move view source components to use JSWindowActor. This allows view frame source to work in out of process child frames, r=mconley

Differential Revision: https://phabricator.services.mozilla.com/D60253

--HG--
rename : toolkit/components/viewsource/content/viewSource-content.js => toolkit/actors/ViewSourceChild.jsm
rename : toolkit/components/viewsource/content/viewSource-content.js => toolkit/actors/ViewSourcePageChild.jsm
rename : toolkit/components/viewsource/ViewSourceBrowser.jsm => toolkit/actors/ViewSourcePageParent.jsm
extra : moz-landing-system : lando
This commit is contained in:
Neil Deakin 2020-01-24 19:53:55 +00:00
parent 6dbc210f59
commit 0355257f83
15 changed files with 579 additions and 772 deletions

View File

@ -1174,7 +1174,10 @@ class nsContextMenu {
return viewSourceBrowser;
};
top.gViewSourceUtils.viewPartialSourceInBrowser(browser, openSelectionFn);
top.gViewSourceUtils.viewPartialSourceInBrowser(
this.actor.browsingContext,
openSelectionFn
);
}
// Open new "view source" window with the frame's URL.

View File

@ -208,7 +208,6 @@ XPCOMUtils.defineLazyModuleGetters(this, {
TabStateCache: "resource:///modules/sessionstore/TabStateCache.jsm",
TabStateFlusher: "resource:///modules/sessionstore/TabStateFlusher.jsm",
Utils: "resource://gre/modules/sessionstore/Utils.jsm",
ViewSourceBrowser: "resource://gre/modules/ViewSourceBrowser.jsm",
setTimeout: "resource://gre/modules/Timer.jsm",
});
@ -4710,12 +4709,6 @@ var SessionStoreInternal = {
});
}
// If the restored browser wants to show view source content, start up a
// view source browser that will load the required frame script.
if (uri && ViewSourceBrowser.isViewSource(uri)) {
new ViewSourceBrowser(browser);
}
browser.messageManager.sendAsyncMessage("SessionStore:restoreTabContent", {
loadArguments,
isRemotenessUpdate,

View File

@ -3,28 +3,183 @@
* 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/. */
var EXPORTED_SYMBOLS = ["SelectionSourceChild"];
var EXPORTED_SYMBOLS = ["ViewSourceChild"];
const { ActorChild } = ChromeUtils.import(
"resource://gre/modules/ActorChild.jsm"
);
const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
class SelectionSourceChild extends ActorChild {
receiveMessage(message) {
const global = message.target;
ChromeUtils.defineModuleGetter(
this,
"ViewSourcePageChild",
"resource://gre/actors/ViewSourcePageChild.jsm"
);
if (message.name == "ViewSource:GetSelection") {
let selectionDetails;
try {
selectionDetails = this.getSelection(global);
} finally {
global.sendAsyncMessage(
"ViewSource:GetSelectionDone",
selectionDetails
class ViewSourceChild extends JSWindowActorChild {
receiveMessage(message) {
let data = message.data;
switch (message.name) {
case "ViewSource:LoadSource":
this.viewSource(
data.URL,
data.outerWindowID,
data.lineNumber,
data.shouldWrap
);
break;
case "ViewSource:LoadSourceWithSelection":
this.viewSourceWithSelection(
data.URL,
data.drawSelection,
data.baseURI
);
break;
case "ViewSource:GetSelection":
let selectionDetails;
try {
selectionDetails = this.getSelection(this.document.ownerGlobal);
} catch (e) {}
return selectionDetails;
}
return undefined;
}
/**
* Called when the parent sends a message to view some source code.
*
* @param URL (required)
* The URL string of the source to be shown.
* @param outerWindowID (optional)
* The outerWindowID of the content window that has hosted
* the document, in case we want to retrieve it from the network
* cache.
* @param lineNumber (optional)
* The line number to focus as soon as the source has finished
* loading.
*/
viewSource(URL, outerWindowID, lineNumber) {
let pageDescriptor, forcedCharSet;
if (outerWindowID) {
let contentWindow = Services.wm.getOuterWindowWithId(outerWindowID);
if (contentWindow) {
let otherDocShell = contentWindow.docShell;
try {
pageDescriptor = otherDocShell.QueryInterface(Ci.nsIWebPageDescriptor)
.currentDescriptor;
} catch (e) {
// We couldn't get the page descriptor, so we'll probably end up re-retrieving
// this document off of the network.
}
let utils = contentWindow.windowUtils;
let doc = contentWindow.document;
forcedCharSet = utils.docCharsetIsForced ? doc.characterSet : null;
}
}
this.loadSource(URL, pageDescriptor, lineNumber, forcedCharSet);
}
/**
* Loads a view source selection showing the given view-source url and
* highlight the selection.
*
* @param uri view-source uri to show
* @param drawSelection true to highlight the selection
* @param baseURI base URI of the original document
*/
viewSourceWithSelection(uri, drawSelection, baseURI) {
// This isn't ideal, but set a global in the view source page actor
// that indicates that a selection should be drawn. It will be read
// when by the page's pageshow listener. This should work as the
// view source page is always loaded in the same process.
ViewSourcePageChild.setNeedsDrawSelection(drawSelection);
// all our content is held by the data:URI and URIs are internally stored as utf-8 (see nsIURI.idl)
let loadFlags = Ci.nsIWebNavigation.LOAD_FLAGS_NONE;
let webNav = this.docShell.QueryInterface(Ci.nsIWebNavigation);
let loadURIOptions = {
triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
loadFlags,
baseURI: Services.io.newURI(baseURI),
};
webNav.loadURI(uri, loadURIOptions);
}
/**
* Common utility function used by both the current and deprecated APIs
* for loading source.
*
* @param URL (required)
* The URL string of the source to be shown.
* @param pageDescriptor (optional)
* The currentDescriptor off of an nsIWebPageDescriptor, in the
* event that the caller wants to try to load the source out of
* the network cache.
* @param lineNumber (optional)
* The line number to focus as soon as the source has finished
* loading.
* @param forcedCharSet (optional)
* The document character set to use instead of the default one.
*/
loadSource(URL, pageDescriptor, lineNumber, forcedCharSet) {
const viewSrcURL = "view-source:" + URL;
if (forcedCharSet) {
try {
this.docShell.charset = forcedCharSet;
} catch (e) {
/* invalid charset */
}
}
ViewSourcePageChild.setInitialLineNumber(lineNumber);
if (!pageDescriptor) {
this.loadSourceFromURL(viewSrcURL);
return;
}
try {
let pageLoader = this.docShell.QueryInterface(Ci.nsIWebPageDescriptor);
pageLoader.loadPage(
pageDescriptor,
Ci.nsIWebPageDescriptor.DISPLAY_AS_SOURCE
);
} catch (e) {
// We were not able to load the source from the network cache.
this.loadSourceFromURL(viewSrcURL);
return;
}
let shEntrySource = pageDescriptor.QueryInterface(Ci.nsISHEntry);
let shistory = this.docShell.QueryInterface(Ci.nsIWebNavigation)
.sessionHistory.legacySHistory;
let shEntry = shistory.createEntry();
shEntry.URI = Services.io.newURI(viewSrcURL);
shEntry.title = viewSrcURL;
let systemPrincipal = Services.scriptSecurityManager.getSystemPrincipal();
shEntry.triggeringPrincipal = systemPrincipal;
shEntry.setLoadTypeAsHistory();
shEntry.cacheKey = shEntrySource.cacheKey;
shistory.addEntry(shEntry, true);
}
/**
* Load some URL in the browser.
*
* @param URL
* The URL string to load.
*/
loadSourceFromURL(URL) {
let loadFlags = Ci.nsIWebNavigation.LOAD_FLAGS_NONE;
let webNav = this.docShell.QueryInterface(Ci.nsIWebNavigation);
let loadURIOptions = {
triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
loadFlags,
};
webNav.loadURI(URL, loadURIOptions);
}
/**
@ -213,7 +368,7 @@ class SelectionSourceChild extends ActorChild {
tmpNode.appendChild(ancestorContainer);
return {
uri:
URL:
(isHTML
? "view-source:data:text/html;charset=utf-8,"
: "view-source:data:application/xml;charset=utf-8,") +

View File

@ -9,11 +9,7 @@ const { XPCOMUtils } = ChromeUtils.import(
"resource://gre/modules/XPCOMUtils.jsm"
);
ChromeUtils.defineModuleGetter(
this,
"DeferredTask",
"resource://gre/modules/DeferredTask.jsm"
);
var EXPORTED_SYMBOLS = ["ViewSourcePageChild"];
XPCOMUtils.defineLazyGlobalGetters(this, ["NodeFilter"]);
@ -28,280 +24,105 @@ const BUNDLE_URL = "chrome://global/locale/viewSource.properties";
const MARK_SELECTION_START = "\uFDD0";
const MARK_SELECTION_END = "\uFDEF";
var global = this;
/**
* When showing selection source, chrome will construct a page fragment to
* show, and then instruct content to draw a selection after load. This is
* set true when there is a pending request to draw selection.
*/
let gNeedsDrawSelection = false;
/**
* ViewSourceContent should be loaded in the <xul:browser> of the
* view source window, and initialized as soon as it has loaded.
* Start at a specific line number.
*/
var ViewSourceContent = {
/**
* These are the messages that ViewSourceContent is prepared to listen
* for. If you need ViewSourceContent to handle more messages, add them
* here.
*/
messages: [
"ViewSource:LoadSource",
"ViewSource:LoadSourceWithSelection",
"ViewSource:GoToLine",
],
let gInitialLineNumber = -1;
/**
* When showing selection source, chrome will construct a page fragment to
* show, and then instruct content to draw a selection after load. This is
* set true when there is a pending request to draw selection.
*/
needsDrawSelection: false,
get isViewSource() {
let uri = content.document.documentURI;
return uri.startsWith("view-source:");
/**
* In-page context menu items that are injected after page load.
*/
let gContextMenuItems = [
{
id: "goToLine",
accesskey: true,
handler(actor) {
actor.sendAsyncMessage("ViewSource:PromptAndGoToLine");
},
},
get isAboutBlank() {
let uri = content.document.documentURI;
return uri == "about:blank";
{
id: "wrapLongLines",
get checked() {
return Services.prefs.getBoolPref("view_source.wrap_long_lines");
},
handler(actor) {
actor.toggleWrapping();
},
},
{
id: "highlightSyntax",
get checked() {
return Services.prefs.getBoolPref("view_source.syntax_highlight");
},
handler(actor) {
actor.toggleSyntaxHighlighting();
},
},
];
/**
* This should be called as soon as this frame script has loaded.
*/
init() {
this.messages.forEach(msgName => {
addMessageListener(msgName, this);
class ViewSourcePageChild extends JSWindowActorChild {
constructor() {
super();
XPCOMUtils.defineLazyGetter(this, "bundle", function() {
return Services.strings.createBundle(BUNDLE_URL);
});
}
addEventListener("pagehide", this, true);
addEventListener("pageshow", this, true);
addEventListener("click", this);
addEventListener("unload", this);
Services.els.addSystemEventListener(global, "contextmenu", this, false);
},
static setNeedsDrawSelection(value) {
gNeedsDrawSelection = value;
}
/**
* This should be called when the frame script is being unloaded,
* and the browser is tearing down.
*/
uninit() {
this.messages.forEach(msgName => {
removeMessageListener(msgName, this);
});
static setInitialLineNumber(value) {
gInitialLineNumber = value;
}
removeEventListener("pagehide", this, true);
removeEventListener("pageshow", this, true);
removeEventListener("click", this);
removeEventListener("unload", this);
Services.els.removeSystemEventListener(global, "contextmenu", this, false);
},
/**
* Anything added to the messages array will get handled here, and should
* get dispatched to a specific function for the message name.
*/
receiveMessage(msg) {
if (!this.isViewSource && !this.isAboutBlank) {
return;
if (msg.name == "ViewSource:GoToLine") {
this.goToLine(msg.data.lineNumber);
}
let data = msg.data;
switch (msg.name) {
case "ViewSource:LoadSource":
this.viewSource(
data.URL,
data.outerWindowID,
data.lineNumber,
data.shouldWrap
);
break;
case "ViewSource:LoadSourceWithSelection":
this.viewSourceWithSelection(
data.URL,
data.drawSelection,
data.baseURI
);
break;
case "ViewSource:GoToLine":
this.goToLine(data.lineNumber);
break;
}
},
}
/**
* Any events should get handled here, and should get dispatched to
* a specific function for the event type.
*/
handleEvent(event) {
if (!this.isViewSource) {
return;
}
switch (event.type) {
case "pagehide":
this.onPageHide(event);
break;
case "pageshow":
this.onPageShow(event);
break;
case "click":
this.onClick(event);
break;
case "unload":
this.uninit();
break;
case "contextmenu":
this.onContextMenu(event);
break;
}
},
/**
* A getter for the view source string bundle.
*/
get bundle() {
delete this.bundle;
this.bundle = Services.strings.createBundle(BUNDLE_URL);
return this.bundle;
},
}
/**
* A shortcut to the nsISelectionController for the content.
*/
get selectionController() {
return docShell
return this.docShell
.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsISelectionDisplay)
.QueryInterface(Ci.nsISelectionController);
},
}
/**
* A shortcut to the nsIWebBrowserFind for the content.
*/
get webBrowserFind() {
return docShell
return this.docShell
.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIWebBrowserFind);
},
/**
* Called when the parent sends a message to view some source code.
*
* @param URL (required)
* The URL string of the source to be shown.
* @param outerWindowID (optional)
* The outerWindowID of the content window that has hosted
* the document, in case we want to retrieve it from the network
* cache.
* @param lineNumber (optional)
* The line number to focus as soon as the source has finished
* loading.
*/
viewSource(URL, outerWindowID, lineNumber) {
let pageDescriptor, forcedCharSet;
if (outerWindowID) {
let contentWindow = Services.wm.getOuterWindowWithId(outerWindowID);
let otherDocShell = contentWindow.docShell;
try {
pageDescriptor = otherDocShell.QueryInterface(Ci.nsIWebPageDescriptor)
.currentDescriptor;
} catch (e) {
// We couldn't get the page descriptor, so we'll probably end up re-retrieving
// this document off of the network.
}
let utils = contentWindow.windowUtils;
let doc = contentWindow.document;
forcedCharSet = utils.docCharsetIsForced ? doc.characterSet : null;
}
this.loadSource(URL, pageDescriptor, lineNumber, forcedCharSet);
},
/**
* Common utility function used by both the current and deprecated APIs
* for loading source.
*
* @param URL (required)
* The URL string of the source to be shown.
* @param pageDescriptor (optional)
* The currentDescriptor off of an nsIWebPageDescriptor, in the
* event that the caller wants to try to load the source out of
* the network cache.
* @param lineNumber (optional)
* The line number to focus as soon as the source has finished
* loading.
* @param forcedCharSet (optional)
* The document character set to use instead of the default one.
*/
loadSource(URL, pageDescriptor, lineNumber, forcedCharSet) {
const viewSrcURL = "view-source:" + URL;
if (forcedCharSet) {
try {
docShell.charset = forcedCharSet;
} catch (e) {
/* invalid charset */
}
}
if (lineNumber && lineNumber > 0) {
let doneLoading = event => {
// Ignore possible initial load of about:blank
if (this.isAboutBlank || !content.document.body) {
return;
}
this.goToLine(lineNumber);
removeEventListener("pageshow", doneLoading);
};
addEventListener("pageshow", doneLoading);
}
if (!pageDescriptor) {
this.loadSourceFromURL(viewSrcURL);
return;
}
try {
let pageLoader = docShell.QueryInterface(Ci.nsIWebPageDescriptor);
pageLoader.loadPage(
pageDescriptor,
Ci.nsIWebPageDescriptor.DISPLAY_AS_SOURCE
);
} catch (e) {
// We were not able to load the source from the network cache.
this.loadSourceFromURL(viewSrcURL);
return;
}
let shEntrySource = pageDescriptor.QueryInterface(Ci.nsISHEntry);
let shistory = docShell.QueryInterface(Ci.nsIWebNavigation).sessionHistory
.legacySHistory;
let shEntry = shistory.createEntry();
shEntry.URI = Services.io.newURI(viewSrcURL);
shEntry.title = viewSrcURL;
let systemPrincipal = Services.scriptSecurityManager.getSystemPrincipal();
shEntry.triggeringPrincipal = systemPrincipal;
shEntry.setLoadTypeAsHistory();
shEntry.cacheKey = shEntrySource.cacheKey;
shistory.addEntry(shEntry, true);
},
/**
* Load some URL in the browser.
*
* @param URL
* The URL string to load.
*/
loadSourceFromURL(URL) {
let loadFlags = Ci.nsIWebNavigation.LOAD_FLAGS_NONE;
let webNav = docShell.QueryInterface(Ci.nsIWebNavigation);
let loadURIOptions = {
triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
loadFlags,
};
webNav.loadURI(URL, loadURIOptions);
},
}
/**
* This handler is for click events from:
@ -313,11 +134,11 @@ var ViewSourceContent = {
let target = event.originalTarget;
// Check for content menu actions
if (target.id) {
this.contextMenuItems.forEach(itemSpec => {
gContextMenuItems.forEach(itemSpec => {
if (itemSpec.id !== target.id) {
return;
}
itemSpec.handler.call(this, event);
itemSpec.handler(this);
event.stopPropagation();
});
}
@ -334,10 +155,10 @@ var ViewSourceContent = {
if (target == errorDoc.getElementById("goBackButton")) {
// Instead of loading some safe page, just close the window
sendAsyncMessage("ViewSource:Close");
this.sendAsyncMessage("ViewSource:Close");
}
}
},
}
/**
* Handler for the pageshow event.
@ -346,57 +167,27 @@ var ViewSourceContent = {
* The pageshow event being handled.
*/
onPageShow(event) {
content.focus();
this.contentWindow.focus();
// If we need to draw the selection, wait until an actual view source page
// has loaded, instead of about:blank.
if (
this.needsDrawSelection &&
content.document.documentURI.startsWith("view-source:")
gNeedsDrawSelection &&
this.document.documentURI.startsWith("view-source:")
) {
this.needsDrawSelection = false;
gNeedsDrawSelection = false;
this.drawSelection();
}
if (content.document.body) {
if (gInitialLineNumber >= 0) {
this.goToLine(gInitialLineNumber);
gInitialLineNumber = -1;
}
if (this.document.body) {
this.injectContextMenu();
}
sendAsyncMessage("ViewSource:SourceLoaded");
},
/**
* Handler for the pagehide event.
*
* @param event
* The pagehide event being handled.
*/
onPageHide(event) {
sendAsyncMessage("ViewSource:SourceUnloaded");
},
onContextMenu(event) {
let node = event.target;
let result = {
isEmail: false,
isLink: false,
href: "",
// We have to pass these in the event that we're running in
// a remote browser, so that ViewSourceChrome knows where to
// open the context menu.
screenX: event.screenX,
screenY: event.screenY,
};
if (node && node.localName == "a") {
result.isLink = node.href.startsWith("view-source:");
result.isEmail = node.href.startsWith("mailto:");
result.href = node.href.substring(node.href.indexOf(":") + 1);
}
sendSyncMessage("ViewSource:ContextMenuOpening", result);
},
}
/**
* Attempts to go to a particular line in the source code being
@ -409,7 +200,7 @@ var ViewSourceContent = {
* The line number to attempt to go to.
*/
goToLine(lineNumber) {
let body = content.document.body;
let body = this.document.body;
// The source document is made up of a number of pre elements with
// id attributes in the format <pre id="line123">, meaning that
@ -439,11 +230,11 @@ var ViewSourceContent = {
let found = this.findLocation(pre, lineNumber, null, -1, false, result);
if (!found) {
sendAsyncMessage("ViewSource:GoToLine:Failed");
this.sendAsyncMessage("ViewSource:GoToLine:Failed");
return;
}
let selection = content.getSelection();
let selection = this.document.defaultView.getSelection();
selection.removeAllRanges();
// In our case, the range's startOffset is after "\n" on the previous line.
@ -484,8 +275,8 @@ var ViewSourceContent = {
true
);
sendAsyncMessage("ViewSource:GoToLine:Success", { lineNumber });
},
this.sendAsyncMessage("ViewSource:GoToLine:Success", { lineNumber });
}
/**
* Some old code from the original view source implementation. Original
@ -514,7 +305,7 @@ var ViewSourceContent = {
let curLine = pre.id ? parseInt(pre.id.substring(4)) : 1;
// Walk through each of the text nodes and count newlines.
let treewalker = content.document.createTreeWalker(
let treewalker = this.document.createTreeWalker(
pre,
NodeFilter.SHOW_TEXT,
null
@ -573,7 +364,7 @@ var ViewSourceContent = {
break;
}
} else if (curLine == lineNumber && !("range" in result)) {
result.range = content.document.createRange();
result.range = this.document.createRange();
result.range.setStart(textNode, curPos);
// This will always be overridden later, except when we look for
@ -589,17 +380,17 @@ var ViewSourceContent = {
}
return found || "range" in result;
},
}
/**
* Toggles the "wrap" class on the document body, which sets whether
* or not long lines are wrapped. Notifies parent to update the pref.
*/
toggleWrapping() {
let body = content.document.body;
let body = this.document.body;
let state = body.classList.toggle("wrap");
sendAsyncMessage("ViewSource:StoreWrapping", { state });
},
this.sendAsyncMessage("ViewSource:StoreWrapping", { state });
}
/**
* Toggles the "highlight" class on the document body, which sets whether
@ -607,32 +398,10 @@ var ViewSourceContent = {
* pref.
*/
toggleSyntaxHighlighting() {
let body = content.document.body;
let body = this.document.body;
let state = body.classList.toggle("highlight");
sendAsyncMessage("ViewSource:StoreSyntaxHighlighting", { state });
},
/**
* Loads a view source selection showing the given view-source url and
* highlight the selection.
*
* @param uri view-source uri to show
* @param drawSelection true to highlight the selection
* @param baseURI base URI of the original document
*/
viewSourceWithSelection(uri, drawSelection, baseURI) {
this.needsDrawSelection = drawSelection;
// all our content is held by the data:URI and URIs are internally stored as utf-8 (see nsIURI.idl)
let loadFlags = Ci.nsIWebNavigation.LOAD_FLAGS_NONE;
let webNav = docShell.QueryInterface(Ci.nsIWebNavigation);
let loadURIOptions = {
triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
loadFlags,
baseURI: Services.io.newURI(baseURI),
};
webNav.loadURI(uri, loadURIOptions);
},
this.sendAsyncMessage("ViewSource:StoreSyntaxHighlighting", { state });
}
/**
* Using special markers left in the serialized source, this helper makes the
@ -640,7 +409,7 @@ var ViewSourceContent = {
* selected on the inflated view-source DOM.
*/
drawSelection() {
content.document.title = this.bundle.GetStringFromName(
this.document.title = this.bundle.GetStringFromName(
"viewSelectionSourceTitle"
);
@ -677,7 +446,7 @@ var ViewSourceContent = {
var startLength = MARK_SELECTION_START.length;
findInst.findNext();
var selection = content.getSelection();
var selection = this.document.defaultView.getSelection();
if (!selection.rangeCount) {
return;
}
@ -732,44 +501,13 @@ var ViewSourceContent = {
findInst.wrapFind = wrapFind;
findInst.findBackwards = findBackwards;
findInst.searchString = searchString;
},
/**
* In-page context menu items that are injected after page load.
*/
contextMenuItems: [
{
id: "goToLine",
accesskey: true,
handler() {
sendAsyncMessage("ViewSource:PromptAndGoToLine");
},
},
{
id: "wrapLongLines",
get checked() {
return Services.prefs.getBoolPref("view_source.wrap_long_lines");
},
handler() {
this.toggleWrapping();
},
},
{
id: "highlightSyntax",
get checked() {
return Services.prefs.getBoolPref("view_source.syntax_highlight");
},
handler() {
this.toggleSyntaxHighlighting();
},
},
],
}
/**
* Add context menu items for view source specific actions.
*/
injectContextMenu() {
let doc = content.document;
let doc = this.document;
let menu = doc.createElementNS(NS_XHTML, "menu");
menu.setAttribute("type", "context");
@ -777,7 +515,7 @@ var ViewSourceContent = {
doc.body.appendChild(menu);
doc.body.setAttribute("contextmenu", "actions");
this.contextMenuItems.forEach(itemSpec => {
gContextMenuItems.forEach(itemSpec => {
let item = doc.createElementNS(NS_XHTML, "menuitem");
item.setAttribute("id", itemSpec.id);
let labelName = `context_${itemSpec.id}_label`;
@ -797,14 +535,14 @@ var ViewSourceContent = {
});
this.updateContextMenu();
},
}
/**
* Update state of checkbox-style context menu items.
*/
updateContextMenu() {
let doc = content.document;
this.contextMenuItems.forEach(itemSpec => {
let doc = this.document;
gContextMenuItems.forEach(itemSpec => {
if (!("checked" in itemSpec)) {
return;
}
@ -815,6 +553,5 @@ var ViewSourceContent = {
item.removeAttribute("checked");
}
});
},
};
ViewSourceContent.init();
}
}

View File

@ -0,0 +1,159 @@
// -*- 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/. */
ChromeUtils.defineModuleGetter(
this,
"Services",
"resource://gre/modules/Services.jsm"
);
const BUNDLE_URL = "chrome://global/locale/viewSource.properties";
var EXPORTED_SYMBOLS = ["ViewSourcePageParent"];
/**
* ViewSourcePageParent manages the view source <browser> from the chrome side.
*/
class ViewSourcePageParent extends JSWindowActorParent {
constructor() {
super();
/**
* Holds the value of the last line found via the "Go to line"
* command, to pre-populate the prompt the next time it is
* opened.
*/
this.lastLineFound = null;
}
/**
* Anything added to the messages array will get handled here, and should
* get dispatched to a specific function for the message name.
*/
receiveMessage(message) {
let data = message.data;
switch (message.name) {
case "ViewSource:PromptAndGoToLine":
this.promptAndGoToLine();
break;
case "ViewSource:GoToLine:Success":
this.onGoToLineSuccess(data.lineNumber);
break;
case "ViewSource:GoToLine:Failed":
this.onGoToLineFailed();
break;
case "ViewSource:StoreWrapping":
this.storeWrapping(data.state);
break;
case "ViewSource:StoreSyntaxHighlighting":
this.storeSyntaxHighlighting(data.state);
break;
}
}
/**
* A getter for the view source string bundle.
*/
get bundle() {
if (this._bundle) {
return this._bundle;
}
return (this._bundle = Services.strings.createBundle(BUNDLE_URL));
}
/**
* Opens the "Go to line" prompt for a user to hop to a particular line
* of the source code they're viewing. This will keep prompting until the
* user either cancels out of the prompt, or enters a valid line number.
*/
promptAndGoToLine() {
let input = { value: this.lastLineFound };
let window = Services.wm.getMostRecentWindow(null);
let ok = Services.prompt.prompt(
window,
this.bundle.GetStringFromName("goToLineTitle"),
this.bundle.GetStringFromName("goToLineText"),
input,
null,
{ value: 0 }
);
if (!ok) {
return;
}
let line = parseInt(input.value, 10);
if (!(line > 0)) {
Services.prompt.alert(
window,
this.bundle.GetStringFromName("invalidInputTitle"),
this.bundle.GetStringFromName("invalidInputText")
);
this.promptAndGoToLine();
} else {
this.goToLine(line);
}
}
/**
* Go to a particular line of the source code. This act is asynchronous.
*
* @param lineNumber
* The line number to try to go to to.
*/
goToLine(lineNumber) {
this.sendAsyncMessage("ViewSource:GoToLine", { lineNumber });
}
/**
* Called when the frame script reports that a line was successfully gotten
* to.
*
* @param lineNumber
* The line number that we successfully got to.
*/
onGoToLineSuccess(lineNumber) {
// We'll pre-populate the "Go to line" prompt with this value the next
// time it comes up.
this.lastLineFound = lineNumber;
}
/**
* Called when the child reports that we failed to go to a particular
* line. This informs the user that their selection was likely out of range,
* and then reprompts the user to try again.
*/
onGoToLineFailed() {
let window = Services.wm.getMostRecentWindow(null);
Services.prompt.alert(
window,
this.bundle.GetStringFromName("outOfRangeTitle"),
this.bundle.GetStringFromName("outOfRangeText")
);
this.promptAndGoToLine();
}
/**
* Update the wrapping pref based on the child's current state.
* @param state
* Whether wrapping is currently enabled in the child.
*/
storeWrapping(state) {
Services.prefs.setBoolPref("view_source.wrap_long_lines", state);
}
/**
* Update the syntax highlighting pref based on the child's current state.
* @param state
* Whether syntax highlighting is currently enabled in the child.
*/
storeSyntaxHighlighting(state) {
Services.prefs.setBoolPref("view_source.syntax_highlight", state);
}
}

View File

@ -48,11 +48,13 @@ FINAL_TARGET_FILES.actors += [
'PrintingChild.jsm',
'PurgeSessionHistoryChild.jsm',
'SelectChild.jsm',
'SelectionSourceChild.jsm',
'SelectParent.jsm',
'ThumbnailsChild.jsm',
'UAWidgetsChild.jsm',
'UnselectedTabHoverChild.jsm',
'ViewSourceChild.jsm',
'ViewSourcePageChild.jsm',
'ViewSourcePageParent.jsm',
'WebChannelChild.jsm',
'WebChannelParent.jsm',
'WebNavigationChild.jsm',

View File

@ -1,335 +0,0 @@
// -*- 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/. */
ChromeUtils.defineModuleGetter(
this,
"Services",
"resource://gre/modules/Services.jsm"
);
const BUNDLE_URL = "chrome://global/locale/viewSource.properties";
const FRAME_SCRIPT = "chrome://global/content/viewSource-content.js";
var EXPORTED_SYMBOLS = ["ViewSourceBrowser"];
// Keep a set of browsers we've seen before, so we can load our frame script as
// needed into any new ones.
var gKnownBrowsers = new WeakSet();
/**
* ViewSourceBrowser manages the view source <browser> from the chrome side.
* It's companion frame script, viewSource-content.js, needs to be loaded as a
* frame script into the browser being managed.
*
* For a view source tab (or some other non-window case), an instance of this is
* created by viewSourceUtils.js to wrap the <browser>. The frame script will
* be loaded by this module at construction time.
*/
function ViewSourceBrowser(aBrowser) {
this._browser = aBrowser;
this.init();
}
ViewSourceBrowser.prototype = {
/**
* The <browser> that will be displaying the view source content.
*/
get browser() {
return this._browser;
},
/**
* Holds the value of the last line found via the "Go to line"
* command, to pre-populate the prompt the next time it is
* opened.
*/
lastLineFound: null,
/**
* These are the messages that ViewSourceBrowser will listen for
* from the frame script it injects. Any message names added here
* will automatically have ViewSourceBrowser listen for those messages,
* and remove the listeners on teardown.
*/
messages: [
"ViewSource:PromptAndGoToLine",
"ViewSource:GoToLine:Success",
"ViewSource:GoToLine:Failed",
"ViewSource:StoreWrapping",
"ViewSource:StoreSyntaxHighlighting",
],
/**
* This should be called as soon as the script loads. When this function
* executes, we can assume the DOM content has not yet loaded.
*/
init() {
this.messages.forEach(msgName => {
this.mm.addMessageListener(msgName, this);
});
this.loadFrameScript();
},
/**
* This should be called when the window is closing. This function should
* clean up event and message listeners.
*/
uninit() {
this.messages.forEach(msgName => {
this.mm.removeMessageListener(msgName, this);
});
},
/**
* For a new browser we've not seen before, load the frame script.
*/
loadFrameScript() {
// Check for a browser first. There won't be one for the window case
// (still used by other applications like Thunderbird), as the element
// does not exist until the XUL document loads.
if (!this.browser) {
return;
}
if (!gKnownBrowsers.has(this.browser)) {
gKnownBrowsers.add(this.browser);
this.mm.loadFrameScript(FRAME_SCRIPT, false);
}
},
/**
* Anything added to the messages array will get handled here, and should
* get dispatched to a specific function for the message name.
*/
receiveMessage(message) {
let data = message.data;
switch (message.name) {
case "ViewSource:PromptAndGoToLine":
this.promptAndGoToLine();
break;
case "ViewSource:GoToLine:Success":
this.onGoToLineSuccess(data.lineNumber);
break;
case "ViewSource:GoToLine:Failed":
this.onGoToLineFailed();
break;
case "ViewSource:StoreWrapping":
this.storeWrapping(data.state);
break;
case "ViewSource:StoreSyntaxHighlighting":
this.storeSyntaxHighlighting(data.state);
break;
}
},
/**
* Getter for the message manager of the view source browser.
*/
get mm() {
return this.browser.messageManager;
},
/**
* Send a message to the view source browser.
*/
sendAsyncMessage(...args) {
this.browser.messageManager.sendAsyncMessage(...args);
},
/**
* A getter for the view source string bundle.
*/
get bundle() {
if (this._bundle) {
return this._bundle;
}
return (this._bundle = Services.strings.createBundle(BUNDLE_URL));
},
/**
* Loads the source for a URL while applying some optional features if
* enabled.
*
* For view source in a specific browser, this is manually called after
* this object is constructed.
*
* This takes a single object argument containing:
*
* URL (required):
* A string URL for the page we'd like to view the source of.
* browser:
* The browser containing the document that we would like to view the
* source of. This argument is optional if outerWindowID is not passed.
* outerWindowID (optional):
* The outerWindowID of the content window containing the document that
* we want to view the source of. This is the only way of attempting to
* load the source out of the network cache.
* lineNumber (optional):
* The line number to focus on once the source is loaded.
*/
loadViewSource({ URL, browser, outerWindowID, lineNumber }) {
if (!URL) {
throw new Error("Must supply a URL when opening view source.");
}
if (browser) {
this.browser.sameProcessAsFrameLoader = browser.frameLoader;
// If we're dealing with a remote browser, then the browser
// for view source needs to be remote as well.
this.updateBrowserRemoteness(browser.remoteType);
} else if (outerWindowID) {
throw new Error("Must supply the browser if passing the outerWindowID");
}
this.sendAsyncMessage("ViewSource:LoadSource", {
URL,
outerWindowID,
lineNumber,
});
},
/**
* Loads a view source selection showing the given view-source url and
* highlight the selection.
*
* @param uri view-source uri to show
* @param drawSelection true to highlight the selection
* @param baseURI base URI of the original document
*/
loadViewSourceFromSelection(URL, drawSelection, baseURI) {
this.sendAsyncMessage("ViewSource:LoadSourceWithSelection", {
URL,
drawSelection,
baseURI,
});
},
/**
* Updates the "remote" attribute of the view source browser. This
* will remove the browser from the DOM, and then re-add it in the
* same place it was taken from.
*
* @param shouldBeRemote
* True if the browser should be made remote. If the browsers
* remoteness already matches this value, this function does
* nothing.
* @param remoteType
* The type of remote browser process.
*/
updateBrowserRemoteness(remoteType) {
if (this.browser.remoteType != remoteType) {
// In this base case, where we are handed a <browser> someone else is
// managing, we don't know for sure that it's safe to toggle remoteness.
// For view source in a window, this is overridden to actually do the
// flip if needed.
throw new Error("View source browser's remoteness mismatch");
}
},
/**
* Opens the "Go to line" prompt for a user to hop to a particular line
* of the source code they're viewing. This will keep prompting until the
* user either cancels out of the prompt, or enters a valid line number.
*/
promptAndGoToLine() {
let input = { value: this.lastLineFound };
let window = Services.wm.getMostRecentWindow(null);
let ok = Services.prompt.prompt(
window,
this.bundle.GetStringFromName("goToLineTitle"),
this.bundle.GetStringFromName("goToLineText"),
input,
null,
{ value: 0 }
);
if (!ok) {
return;
}
let line = parseInt(input.value, 10);
if (!(line > 0)) {
Services.prompt.alert(
window,
this.bundle.GetStringFromName("invalidInputTitle"),
this.bundle.GetStringFromName("invalidInputText")
);
this.promptAndGoToLine();
} else {
this.goToLine(line);
}
},
/**
* Go to a particular line of the source code. This act is asynchronous.
*
* @param lineNumber
* The line number to try to go to to.
*/
goToLine(lineNumber) {
this.sendAsyncMessage("ViewSource:GoToLine", { lineNumber });
},
/**
* Called when the frame script reports that a line was successfully gotten
* to.
*
* @param lineNumber
* The line number that we successfully got to.
*/
onGoToLineSuccess(lineNumber) {
// We'll pre-populate the "Go to line" prompt with this value the next
// time it comes up.
this.lastLineFound = lineNumber;
},
/**
* Called when the frame script reports that we failed to go to a particular
* line. This informs the user that their selection was likely out of range,
* and then reprompts the user to try again.
*/
onGoToLineFailed() {
let window = Services.wm.getMostRecentWindow(null);
Services.prompt.alert(
window,
this.bundle.GetStringFromName("outOfRangeTitle"),
this.bundle.GetStringFromName("outOfRangeText")
);
this.promptAndGoToLine();
},
/**
* Update the wrapping pref based on the child's current state.
* @param state
* Whether wrapping is currently enabled in the child.
*/
storeWrapping(state) {
Services.prefs.setBoolPref("view_source.wrap_long_lines", state);
},
/**
* Update the syntax highlighting pref based on the child's current state.
* @param state
* Whether syntax highlighting is currently enabled in the child.
*/
storeSyntaxHighlighting(state) {
Services.prefs.setBoolPref("view_source.syntax_highlight", state);
},
};
/**
* Helper to decide if a URI maps to view source content.
* @param uri
* String containing the URI
*/
ViewSourceBrowser.isViewSource = function(uri) {
return uri.startsWith("view-source:");
};

View File

@ -14,11 +14,6 @@
var { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
ChromeUtils.defineModuleGetter(
this,
"ViewSourceBrowser",
"resource://gre/modules/ViewSourceBrowser.jsm"
);
ChromeUtils.defineModuleGetter(
this,
"PrivateBrowsingUtils",
@ -30,6 +25,11 @@ var gViewSourceUtils = {
mnsIWebProgress: Ci.nsIWebProgress,
mnsIWebPageDescriptor: Ci.nsIWebPageDescriptor,
// Get the ViewSource actor for a browsing context.
getViewSourceActor(aBrowsingContext) {
return aBrowsingContext.currentWindowGlobal.getActor("ViewSource");
},
/**
* Opens the view source window.
*
@ -106,9 +106,41 @@ var gViewSourceUtils = {
* lineNumber (optional):
* The line number to focus on once the source is loaded.
*/
viewSourceInBrowser(aArgs) {
let viewSourceBrowser = new ViewSourceBrowser(aArgs.viewSourceBrowser);
viewSourceBrowser.loadViewSource(aArgs);
viewSourceInBrowser({
URL,
viewSourceBrowser,
browser,
outerWindowID,
lineNumber,
}) {
if (!URL) {
throw new Error("Must supply a URL when opening view source.");
}
if (browser) {
viewSourceBrowser.sameProcessAsFrameLoader = browser.frameLoader;
// If we're dealing with a remote browser, then the browser
// for view source needs to be remote as well.
if (viewSourceBrowser.remoteType != browser.remoteType) {
// In this base case, where we are handed a <browser> someone else is
// managing, we don't know for sure that it's safe to toggle remoteness.
// For view source in a window, this is overridden to actually do the
// flip if needed.
throw new Error("View source browser's remoteness mismatch");
}
} else if (outerWindowID) {
throw new Error("Must supply the browser if passing the outerWindowID");
}
let viewSourceActor = this.getViewSourceActor(
viewSourceBrowser.browsingContext
);
viewSourceActor.sendAsyncMessage("ViewSource:LoadSource", {
URL,
outerWindowID,
lineNumber,
});
},
/**
@ -116,31 +148,21 @@ var gViewSourceUtils = {
* <browser>. This allows for non-window display methods, such as a tab from
* Firefox.
*
* @param aViewSourceInBrowser
* The browser containing the page to view the source of.
* @param aBrowsingContext:
* The child browsing context containing the document to view the source of.
* @param aGetBrowserFn
* A function that will return a browser to open the source in.
*/
viewPartialSourceInBrowser(aViewSourceInBrowser, aGetBrowserFn) {
let mm = aViewSourceInBrowser.messageManager;
mm.addMessageListener("ViewSource:GetSelectionDone", function gotSelection(
message
) {
mm.removeMessageListener("ViewSource:GetSelectionDone", gotSelection);
async viewPartialSourceInBrowser(aBrowsingContext, aGetBrowserFn) {
let sourceActor = this.getViewSourceActor(aBrowsingContext);
if (sourceActor) {
let data = await sourceActor.sendQuery("ViewSource:GetSelection", {});
if (!message.data) {
return;
}
let viewSourceBrowser = new ViewSourceBrowser(aGetBrowserFn());
viewSourceBrowser.loadViewSourceFromSelection(
message.data.uri,
message.data.drawSelection,
message.data.baseURI
let targetActor = this.getViewSourceActor(
aGetBrowserFn().browsingContext
);
});
mm.sendAsyncMessage("ViewSource:GetSelection");
targetActor.sendAsyncMessage("ViewSource:LoadSourceWithSelection", data);
}
},
buildEditorArgs(aPath, aLineNumber) {

View File

@ -4,4 +4,3 @@
toolkit.jar:
content/global/viewSourceUtils.js (content/viewSourceUtils.js)
content/global/viewSource-content.js (content/viewSource-content.js)

View File

@ -9,9 +9,5 @@ MOCHITEST_CHROME_MANIFESTS += ['test/chrome.ini']
JAR_MANIFESTS += ['jar.mn']
EXTRA_JS_MODULES += [
'ViewSourceBrowser.jsm',
]
with Files('**'):
BUG_COMPONENT = ('Toolkit', 'View Source')

View File

@ -9,5 +9,6 @@ support-files = head.js
skip-if = (os == "win" && processor == "aarch64") # disabled on aarch64 due to 1531590
[browser_gotoline.js]
[browser_open_docgroup.js]
[browser_partialsource.js]
[browser_srcdoc.js]
[browser_viewsourceprefs.js]

View File

@ -24,9 +24,13 @@ var checkViewSource = async function(aTab) {
});
for (let i = 1; i <= 3; i++) {
browser.messageManager.sendAsyncMessage("ViewSource:GoToLine", {
lineNumber: i,
});
browser.sendMessageToActor(
"ViewSource:GoToLine",
{
lineNumber: i,
},
"ViewSourcePage"
);
await SpecialPowers.spawn(browser, [i], async function(i) {
let selection = content.getSelection();
Assert.equal(selection.toString(), "line " + i, "Correct text selected");

View File

@ -0,0 +1,46 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/
*/
const frameSource =
"<a href='about:mozilla'>some text</a><a id='other' href='about:about'>other text</a>";
const sources = [
`<html><iframe id="f" srcdoc="${frameSource}"></iframe></html>`,
`<html><iframe id="f" src="https://example.com/document-builder.sjs?html=${frameSource}"></iframe></html>`,
];
add_task(async function partial_source() {
for (let source of sources) {
let tab = await BrowserTestUtils.openNewForegroundTab(
gBrowser,
"data:text/html," + source
);
let frameBC = gBrowser.selectedBrowser.browsingContext.getChildren()[0];
await SpecialPowers.spawn(frameBC, [], () => {
let element = content.document.getElementById("other");
content.focus();
content.getSelection().selectAllChildren(element);
});
let sourceTab = await openViewPartialSource("#other", frameBC);
let browser = gBrowser.selectedBrowser;
let textContent = await SpecialPowers.spawn(browser, [], async function() {
return content.document.body.textContent;
});
is(
textContent,
'<a id="other" href="about:about">other text</a>',
"Correct content loaded"
);
let selection = await SpecialPowers.spawn(browser, [], async function() {
return String(content.getSelection());
});
is(selection, "other text", "Correct text selected");
gBrowser.removeTab(sourceTab);
gBrowser.removeTab(tab);
}
});

View File

@ -87,9 +87,13 @@ async function openViewSource() {
* @param aCSSSelector - used to specify a node within the selection to
* view the source of. It is expected that this node is
* within an existing selection.
* @param aBrowsingContext - browsing context containing a subframe (optional).
* @returns the new tab which shows the source.
*/
async function openViewPartialSource(aCSSSelector) {
async function openViewPartialSource(
aCSSSelector,
aBrowsingContext = gBrowser.selectedBrowser
) {
let contentAreaContextMenuPopup = document.getElementById(
"contentAreaContextMenu"
);
@ -100,7 +104,7 @@ async function openViewPartialSource(aCSSSelector) {
await BrowserTestUtils.synthesizeMouseAtCenter(
aCSSSelector,
{ type: "contextmenu", button: 2 },
gBrowser.selectedBrowser
aBrowsingContext
);
await popupShownPromise;
@ -161,13 +165,12 @@ async function openViewFrameSourceTab(aCSSSelector) {
* complete.
*/
function waitForSourceLoaded(tab) {
return new Promise(resolve => {
let mm = tab.linkedBrowser.messageManager;
mm.addMessageListener("ViewSource:SourceLoaded", function sourceLoaded() {
mm.removeMessageListener("ViewSource:SourceLoaded", sourceLoaded);
setTimeout(resolve, 0);
});
});
return BrowserTestUtils.waitForContentEvent(
tab.linkedBrowser,
"pageshow",
false,
event => String(event.target.location).startsWith("view-source")
);
}
/**

View File

@ -300,6 +300,35 @@ let ACTORS = {
allFrames: true,
},
// This actor is available for all pages that one can
// view the source of, however it won't be created until a
// request to view the source is made via the message
// 'ViewSource:LoadSource' or 'ViewSource:LoadSourceWithSelection'.
ViewSource: {
child: {
moduleURI: "resource://gre/actors/ViewSourceChild.jsm",
},
allFrames: true,
},
// This actor is for the view-source page itself.
ViewSourcePage: {
parent: {
moduleURI: "resource://gre/actors/ViewSourcePageParent.jsm",
},
child: {
moduleURI: "resource://gre/actors/ViewSourcePageChild.jsm",
events: {
pageshow: { capture: true },
click: {},
},
},
matches: ["view-source:*"],
allFrames: true,
},
WebChannel: {
parent: {
moduleURI: "resource://gre/actors/WebChannelParent.jsm",
@ -485,13 +514,6 @@ let LEGACY_ACTORS = {
},
},
SelectionSource: {
child: {
module: "resource://gre/actors/SelectionSourceChild.jsm",
messages: ["ViewSource:GetSelection"],
},
},
UnselectedTabHover: {
child: {
module: "resource://gre/actors/UnselectedTabHoverChild.jsm",