--- a/toolkit/components/viewsource/ViewSourceBrowser.jsm
+++ b/toolkit/components/viewsource/ViewSourceBrowser.jsm
@@ -50,23 +50,32 @@ 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.
*/
- // TODO: Some messages will appear here in a later patch
messages: [
+ "ViewSource:PromptAndGoToLine",
+ "ViewSource:GoToLine:Success",
+ "ViewSource:GoToLine:Failed",
],
/**
* 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) => {
@@ -86,18 +95,26 @@ ViewSourceBrowser.prototype = {
/**
* 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;
- // TODO: Some messages will appear here in a later patch
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;
default:
// Unhandled
return false;
}
},
/**
* Getter for the message manager of the view source browser.
@@ -537,9 +554,77 @@ ViewSourceBrowser.prototype = {
// replace chars in our charTable
str = str.replace(/[<>&"]/g, charTableLookup);
// replace chars > 0x7f via nsIEntityConverter
str = str.replace(/[^\0-\u007f]/g, convertEntity);
return str;
},
+
+ /**
+ * 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();
+ },
};
--- a/toolkit/components/viewsource/content/viewSource-content.js
+++ b/toolkit/components/viewsource/content/viewSource-content.js
@@ -7,16 +7,17 @@ const { utils: Cu, interfaces: Ci, class
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "BrowserUtils",
"resource://gre/modules/BrowserUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "DeferredTask",
"resource://gre/modules/DeferredTask.jsm");
+const NS_XHTML = "http://www.w3.org/1999/xhtml";
const BUNDLE_URL = "chrome://global/locale/viewSource.properties";
// These are markers used to delimit the selection during processing. They
// are removed from the final rendering.
// We use noncharacter Unicode codepoints to minimize the risk of clashing
// with anything that might legitimately be present in the document.
// U+FDD0..FDEF <noncharacters>
const MARK_SELECTION_START = "\uFDD0";
@@ -339,26 +340,38 @@ let ViewSourceContent = {
*/
loadSourceFromURL(URL) {
let loadFlags = Ci.nsIWebNavigation.LOAD_FLAGS_NONE;
let webNav = docShell.QueryInterface(Ci.nsIWebNavigation);
webNav.loadURI(URL, loadFlags, null, null, null);
},
/**
- * This handler is specifically for click events bubbling up from
- * error page content, which can show up if the user attempts to
- * view the source of an attack page.
+ * This handler is for click events from:
+ * * error page content, which can show up if the user attempts to view the
+ * source of an attack page.
+ * * in-page context menu actions
*/
onClick(event) {
+ let target = event.originalTarget;
+ // Check for content menu actions
+ if (target.id) {
+ this.contextMenuItems.forEach(itemSpec => {
+ if (itemSpec.id !== target.id) {
+ return;
+ }
+ itemSpec.handler.call(this, event);
+ event.stopPropagation();
+ });
+ }
+
// Don't trust synthetic events
if (!event.isTrusted || event.target.localName != "button")
return;
- let target = event.originalTarget;
let errorDoc = target.ownerDocument;
if (/^about:blocked/.test(errorDoc.documentURI)) {
// The event came from a button on a malware/phishing block page
if (target == errorDoc.getElementById("getMeOutButton")) {
// Instead of loading some safe page, just close the window
sendAsyncMessage("ViewSource:Close");
@@ -396,16 +409,18 @@ let ViewSourceContent = {
// 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:")) {
this.needsDrawSelection = false;
this.drawSelection();
}
+ this.injectContextMenu();
+
sendAsyncMessage("ViewSource:SourceLoaded");
},
/**
* Handler for the pagehide event.
*
* @param event
* The pagehide event being handled.
@@ -653,23 +668,22 @@ let ViewSourceContent = {
* or not long lines are wrapped.
*/
toggleWrapping() {
let body = content.document.body;
body.classList.toggle("wrap");
},
/**
- * Called when the parent has changed the syntax highlighting pref.
+ * Toggles the "highlight" class on the document body, which sets whether
+ * or not syntax highlighting is displayed.
*/
toggleSyntaxHighlighting() {
- // The parent process should have set the view_source.syntax_highlight
- // pref to the desired value. The reload brings that setting into
- // effect.
- this.reload();
+ let body = content.document.body;
+ body.classList.toggle("highlight");
},
/**
* Called when the parent has changed the character set to view the
* source with.
*
* @param charset
* The character set to use.
@@ -852,10 +866,85 @@ let ViewSourceContent = {
findService.replaceString = replaceString;
findInst.matchCase = matchCase;
findInst.entireWord = entireWord;
findInst.wrapFind = wrapFind;
findInst.findBackwards = findBackwards;
findInst.searchString = searchString;
},
+
+ /**
+ * In-page context menu items that are injected after page load.
+ */
+ contextMenuItems: [
+ {
+ id: "goToLine",
+ 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 menu = doc.createElementNS(NS_XHTML, "menu");
+ menu.setAttribute("type", "context");
+ menu.setAttribute("id", "actions");
+ doc.body.appendChild(menu);
+ doc.body.setAttribute("contextmenu", "actions");
+
+ this.contextMenuItems.forEach(itemSpec => {
+ let item = doc.createElementNS(NS_XHTML, "menuitem");
+ item.setAttribute("id", itemSpec.id);
+ let labelName = `context_${itemSpec.id}_label`;
+ let label = this.bundle.GetStringFromName(labelName);
+ item.setAttribute("label", label);
+ if ("checked" in itemSpec) {
+ item.setAttribute("type", "checkbox");
+ }
+ menu.appendChild(item);
+ });
+
+ this.updateContextMenu();
+ },
+
+ /**
+ * Update state of checkbox-style context menu items.
+ */
+ updateContextMenu() {
+ let doc = content.document;
+ this.contextMenuItems.forEach(itemSpec => {
+ if (!("checked" in itemSpec)) {
+ return;
+ }
+ let item = doc.getElementById(itemSpec.id);
+ if (itemSpec.checked) {
+ item.setAttribute("checked", true);
+ } else {
+ item.removeAttribute("checked");
+ }
+ });
+ },
};
ViewSourceContent.init();
--- a/toolkit/components/viewsource/content/viewSource.js
+++ b/toolkit/components/viewsource/content/viewSource.js
@@ -47,23 +47,16 @@ ViewSourceChrome.prototype = {
/**
* The <browser> that will be displaying the view source content.
*/
get browser() {
return gBrowser;
},
/**
- * 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,
-
- /**
* The context menu, when opened from the content process, sends
* up a chunk of serialized data describing the items that the
* context menu is being opened on. This allows us to avoid using
* CPOWs.
*/
contextMenuData: {},
/**
@@ -72,18 +65,16 @@ ViewSourceChrome.prototype = {
* will automatically have ViewSourceChrome listen for those messages,
* and remove the listeners on teardown.
*/
messages: ViewSourceBrowser.prototype.messages.concat([
"ViewSource:SourceLoaded",
"ViewSource:SourceUnloaded",
"ViewSource:Close",
"ViewSource:OpenURL",
- "ViewSource:GoToLine:Success",
- "ViewSource:GoToLine:Failed",
"ViewSource:UpdateStatus",
"ViewSource:ContextMenuOpening",
]),
/**
* This called via ViewSourceBrowser's constructor. This should be called as
* soon as the script loads. When this function executes, we can assume the
* DOM content has not yet loaded.
@@ -142,22 +133,16 @@ ViewSourceChrome.prototype = {
this.onSourceUnloaded();
break;
case "ViewSource:Close":
this.close();
break;
case "ViewSource:OpenURL":
this.openURL(data.URL);
break;
- case "ViewSource:GoToLine:Failed":
- this.onGoToLineFailed();
- break;
- case "ViewSource:GoToLine:Success":
- this.onGoToLineSuccess(data.lineNumber);
- break;
case "ViewSource:UpdateStatus":
this.updateStatus(data.label);
break;
case "ViewSource:ContextMenuOpening":
this.onContextMenuOpening(data.isLink, data.isEmail, data.href);
if (this.browser.isRemoteBrowser) {
this.openContextMenu(data.screenX, data.screenY);
}
@@ -611,84 +596,29 @@ ViewSourceChrome.prototype = {
updateStatus(label) {
let statusBarField = document.getElementById("statusbar-line-col");
if (statusBarField) {
statusBarField.label = label;
}
},
/**
- * 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 ok = Services.prompt.prompt(
- window,
- gViewSourceBundle.getString("goToLineTitle"),
- gViewSourceBundle.getString("goToLineText"),
- input,
- null,
- {value:0});
-
- if (!ok)
- return;
-
- let line = parseInt(input.value, 10);
-
- if (!(line > 0)) {
- Services.prompt.alert(window,
- gViewSourceBundle.getString("invalidInputTitle"),
- gViewSourceBundle.getString("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;
+ ViewSourceBrowser.prototype.onGoToLineSuccess.call(this, lineNumber);
document.getElementById("statusbar-line-col").label =
gViewSourceBundle.getFormattedString("statusBarLineCol", [lineNumber, 1]);
},
/**
- * 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() {
- Services.prompt.alert(window,
- gViewSourceBundle.getString("outOfRangeTitle"),
- gViewSourceBundle.getString("outOfRangeText"));
- this.promptAndGoToLine();
- },
-
- /**
* Reloads the browser, bypassing the network cache.
*/
reload() {
this.browser.reloadWithFlags(Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_PROXY |
Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_CACHE);
},
/**
@@ -713,18 +643,17 @@ ViewSourceChrome.prototype = {
/**
* Called when the user clicks on the "Syntax Highlighting" menu item, and
* flips the user preference for wrapping long lines in the view source
* browser.
*/
toggleSyntaxHighlighting() {
this.shouldHighlight = !this.shouldHighlight;
// We can't flip this value in the child, since prefs are read-only there.
- // We flip it here, and then cause a reload in the child to make the change
- // occur.
+ // We flip it here, and then toggle a class in the child.
Services.prefs.setBoolPref("view_source.syntax_highlight",
this.shouldHighlight);
this.sendAsyncMessage("ViewSource:ToggleSyntaxHighlighting");
},
/**
* Updates the "remote" attribute of the view source browser. This
* will remove the browser from the DOM, and then re-add it in the
--- a/toolkit/components/viewsource/test/browser/browser_viewsourceprefs.js
+++ b/toolkit/components/viewsource/test/browser/browser_viewsourceprefs.js
@@ -48,31 +48,24 @@ let exercisePrefs = Task.async(function*
yield checkStyle(win, "white-space", "pre-wrap");
simulateClick(wrapMenuItem);
is(wrapMenuItem.hasAttribute("checked"), false, "Wrap menu item unchecked");
is(SpecialPowers.getBoolPref("view_source.wrap_long_lines"), false, "Wrap pref set");
yield checkStyle(win, "white-space", "pre");
// Check that the Syntax Highlighting menu item works.
- let pageShowPromise = BrowserTestUtils.waitForEvent(win.gBrowser, "pageshow");
simulateClick(syntaxMenuItem);
- yield pageShowPromise;
-
is(syntaxMenuItem.hasAttribute("checked"), false, "Syntax menu item unchecked");
is(SpecialPowers.getBoolPref("view_source.syntax_highlight"), false, "Syntax highlighting pref set");
yield checkHighlight(win, false);
- pageShowPromise = BrowserTestUtils.waitForEvent(win.gBrowser, "pageshow");
simulateClick(syntaxMenuItem);
- yield pageShowPromise;
-
is(syntaxMenuItem.hasAttribute("checked"), true, "Syntax menu item checked");
is(SpecialPowers.getBoolPref("view_source.syntax_highlight"), true, "Syntax highlighting pref set");
-
yield checkHighlight(win, highlightable);
yield BrowserTestUtils.closeWindow(win);
// Open a new view-source window to check that the prefs are obeyed.
SpecialPowers.setIntPref("view_source.tab_size", 2);
SpecialPowers.setBoolPref("view_source.wrap_long_lines", true);
SpecialPowers.setBoolPref("view_source.syntax_highlight", false);
@@ -119,16 +112,15 @@ let checkStyle = Task.async(function* (w
let style = content.getComputedStyle(content.document.body, null);
return style.getPropertyValue(styleProperty);
});
is(value, expected, "Correct value of " + styleProperty);
});
let checkHighlight = Task.async(function* (win, expected) {
let browser = win.gBrowser;
- let highlighted = yield ContentTask.spawn(browser, {}, function* () {
let spans = content.document.getElementsByTagName("span");
return Array.some(spans, (span) => {
- return span.className != "";
+ let style = content.getComputedStyle(span, null);
+ return style.getPropertyValue("color") !== "rgb(0, 0, 0)";
});
- });
is(highlighted, expected, "Syntax highlighting " + (expected ? "on" : "off"));
});
--- a/toolkit/locales/en-US/chrome/global/viewSource.properties
+++ b/toolkit/locales/en-US/chrome/global/viewSource.properties
@@ -6,8 +6,12 @@ goToLineTitle = Go to line
goToLineText = Enter line number
invalidInputTitle = Invalid input
invalidInputText = The line number entered is invalid.
outOfRangeTitle = Line not found
outOfRangeText = The specified line was not found.
statusBarLineCol = Line %1$S, Col %2$S
viewSelectionSourceTitle = DOM Source of Selection
viewMathMLSourceTitle = DOM Source of MathML
+
+context_goToLine_label = Go to Lineā¦
+context_wrapLongLines_label = Wrap Long Lines
+context_highlightSyntax_label = Syntax Highlighting