Source code
Revision control
Copy as Markdown
Other Tools
// -*- 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
/*
* To keep the global namespace safe, don't define global variables and
* functions in this file.
*
* This file silently depends on contentAreaUtils.js for getDefaultFileName
*/
ChromeUtils.defineESModuleGetters(this, {
PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs",
});
var gViewSourceUtils = {
mnsIWebBrowserPersist: Ci.nsIWebBrowserPersist,
mnsIWebProgress: Ci.nsIWebProgress,
mnsIWebPageDescriptor: Ci.nsIWebPageDescriptor,
// Get the ViewSource actor for a browsing context.
getViewSourceActor(aBrowsingContext) {
return aBrowsingContext.currentWindowGlobal.getActor("ViewSource");
},
/**
* Get the ViewSourcePage actor.
* @param object An object with `browsingContext` field
*/
getPageActor({ browsingContext }) {
return browsingContext.currentWindowGlobal.getActor("ViewSourcePage");
},
/**
* Opens the view source window.
*
* @param aArgs (required)
* This Object can include the following properties:
*
* URL (required):
* A string URL for the page we'd like to view the source of.
* browser (optional):
* The browser containing the document that we would like to view the
* source of. This is required if outerWindowID is passed.
* outerWindowID (optional):
* The outerWindowID of the content window containing the document that
* we want to view the source of. Pass this if you want to attempt to
* load the document source out of the network cache.
* lineNumber (optional):
* The line number to focus on once the source is loaded.
*/
async viewSource(aArgs) {
// Check if external view source is enabled. If so, try it. If it fails,
// fallback to internal view source.
if (Services.prefs.getBoolPref("view_source.editor.external")) {
try {
await this.openInExternalEditor(aArgs);
return;
} catch (data) {}
}
// Try existing browsers first.
let browserWin = Services.wm.getMostRecentWindow("navigator:browser");
if (browserWin && browserWin.BrowserCommands.viewSourceOfDocument) {
browserWin.BrowserCommands.viewSourceOfDocument(aArgs);
return;
}
// No browser window created yet, try to create one.
let utils = this;
Services.ww.registerNotification(function onOpen(win, topic) {
if (
win.document.documentURI !== "about:blank" ||
topic !== "domwindowopened"
) {
return;
}
Services.ww.unregisterNotification(onOpen);
win.addEventListener(
"load",
() => {
aArgs.viewSourceBrowser = win.gBrowser.selectedTab.linkedBrowser;
utils.viewSourceInBrowser(aArgs);
},
{ once: true }
);
});
window.top.openWebLinkIn("about:blank", "current");
},
/**
* Displays view source in the provided <browser>. This allows for non-window
* display methods, such as a tab from Firefox.
*
* @param aArgs
* An object with the following properties:
*
* URL (required):
* A string URL for the page we'd like to view the source of.
* viewSourceBrowser (required):
* The browser to display the view source in.
* browser (optional):
* The browser containing the document that we would like to view the
* source of. This is required if outerWindowID is passed.
* outerWindowID (optional):
* The outerWindowID of the content window containing the document that
* we want to view the source of. Pass this if you want to attempt to
* load the document source out of the network cache.
* lineNumber (optional):
* The line number to focus on once the source is loaded.
*/
viewSourceInBrowser({
URL,
viewSourceBrowser,
browser,
outerWindowID,
lineNumber,
}) {
if (!URL) {
throw new Error("Must supply a URL when opening view source.");
}
if (browser) {
// 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,
});
},
/**
* Displays view source for a selection from some document in the provided
* <browser>. This allows for non-window display methods, such as a tab from
* Firefox.
*
* @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.
*/
async viewPartialSourceInBrowser(aBrowsingContext, aGetBrowserFn) {
let sourceActor = this.getViewSourceActor(aBrowsingContext);
if (sourceActor) {
let data = await sourceActor.sendQuery("ViewSource:GetSelection", {});
let targetActor = this.getViewSourceActor(
aGetBrowserFn().browsingContext
);
targetActor.sendAsyncMessage("ViewSource:LoadSourceWithSelection", data);
}
},
buildEditorArgs(aPath, aLineNumber) {
// Determine the command line arguments to pass to the editor.
// We currently support a %LINE% placeholder which is set to the passed
// line number (or to 0 if there's none)
var editorArgs = [];
var args = Services.prefs.getCharPref("view_source.editor.args");
if (args) {
args = args.replace("%LINE%", aLineNumber || "0");
// add the arguments to the array (keeping quoted strings intact)
const argumentRE = /"([^"]+)"|(\S+)/g;
while (argumentRE.test(args)) {
editorArgs.push(RegExp.$1 || RegExp.$2);
}
}
editorArgs.push(aPath);
return editorArgs;
},
/**
* Opens an external editor with the view source content.
*
* @param aArgs (required)
* This Object can include the following properties:
*
* URL (required):
* A string URL for the page we'd like to view the source of.
* browser (optional):
* The browser containing the document that we would like to view the
* source of. This is required if outerWindowID is passed.
* outerWindowID (optional):
* The outerWindowID of the content window containing the document that
* we want to view the source of. Pass this if you want to attempt to
* load the document source out of the network cache.
* lineNumber (optional):
* The line number to focus on once the source is loaded.
*
* @return Promise
* The promise will be resolved or rejected based on whether the
* external editor attempt was successful. Either way, the data object
* is passed along as well.
*/
openInExternalEditor(aArgs) {
return new Promise((resolve, reject) => {
let data;
let { URL, browser, lineNumber } = aArgs;
data = {
url: URL,
lineNumber,
isPrivate: false,
};
if (browser) {
data.doc = {
characterSet: browser.characterSet,
contentType: browser.documentContentType,
title: browser.contentTitle,
cookieJarSettings: browser.cookieJarSettings,
};
data.isPrivate = PrivateBrowsingUtils.isBrowserPrivate(browser);
}
try {
var editor = this.getExternalViewSourceEditor();
if (!editor) {
reject(data);
return;
}
// make a uri
var charset = data.doc ? data.doc.characterSet : null;
var uri = Services.io.newURI(data.url, charset);
data.uri = uri;
var path;
var contentType = data.doc ? data.doc.contentType : null;
var cookieJarSettings = data.doc ? data.doc.cookieJarSettings : null;
if (uri.scheme == "file") {
// it's a local file; we can open it directly
path = uri.QueryInterface(Ci.nsIFileURL).file.path;
var editorArgs = this.buildEditorArgs(path, data.lineNumber);
editor.runw(false, editorArgs, editorArgs.length);
resolve(data);
} else {
// set up the progress listener with what we know so far
this.viewSourceProgressListener.contentLoaded = false;
this.viewSourceProgressListener.editor = editor;
this.viewSourceProgressListener.resolve = resolve;
this.viewSourceProgressListener.reject = reject;
this.viewSourceProgressListener.data = data;
// without a page descriptor, loadPage has no chance of working. download the file.
var file = this.getTemporaryFile(uri, data.doc, contentType);
this.viewSourceProgressListener.file = file;
var webBrowserPersist = Cc[
"@mozilla.org/embedding/browser/nsWebBrowserPersist;1"
].createInstance(this.mnsIWebBrowserPersist);
// the default setting is to not decode. we need to decode.
webBrowserPersist.persistFlags =
this.mnsIWebBrowserPersist.PERSIST_FLAGS_REPLACE_EXISTING_FILES;
webBrowserPersist.progressListener = this.viewSourceProgressListener;
let ssm = Services.scriptSecurityManager;
let principal = ssm.createContentPrincipal(
data.uri,
browser.contentPrincipal.originAttributes
);
webBrowserPersist.saveURI(
uri,
principal,
null,
null,
cookieJarSettings,
null,
null,
file,
Ci.nsIContentPolicy.TYPE_SAVEAS_DOWNLOAD,
data.isPrivate
);
let helperService = Cc[
"@mozilla.org/uriloader/external-helper-app-service;1"
].getService(Ci.nsPIExternalAppLauncher);
if (data.isPrivate) {
// register the file to be deleted when possible
helperService.deleteTemporaryPrivateFileWhenPossible(file);
} else {
// register the file to be deleted on app exit
helperService.deleteTemporaryFileOnExit(file);
}
}
} catch (ex) {
// we failed loading it with the external editor.
console.error(ex);
reject(data);
}
});
},
// Returns nsIProcess of the external view source editor or null
getExternalViewSourceEditor() {
try {
let viewSourceAppPath = Services.prefs.getComplexValue(
"view_source.editor.path",
Ci.nsIFile
);
let editor = Cc["@mozilla.org/process/util;1"].createInstance(
Ci.nsIProcess
);
editor.init(viewSourceAppPath);
return editor;
} catch (ex) {
console.error(ex);
}
return null;
},
viewSourceProgressListener: {
mnsIWebProgressListener: Ci.nsIWebProgressListener,
QueryInterface: ChromeUtils.generateQI([
"nsIWebProgressListener",
"nsISupportsWeakReference",
]),
destroy() {
this.editor = null;
this.resolve = null;
this.reject = null;
this.data = null;
this.file = null;
},
// This listener is used both for tracking the progress of an HTML parse
// in one case and for tracking the progress of nsIWebBrowserPersist in
// another case.
onStateChange(aProgress, aRequest, aFlag, aStatus) {
// once it's done loading...
if (aFlag & this.mnsIWebProgressListener.STATE_STOP && aStatus == 0) {
// We aren't waiting for the parser. Instead, we are waiting for
// an nsIWebBrowserPersist.
this.onContentLoaded();
}
return 0;
},
onContentLoaded() {
// The progress listener may call this multiple times, so be sure we only
// run once.
if (this.contentLoaded) {
return;
}
try {
if (!this.file) {
throw new Error("View-source progress listener should have a file!");
}
var editorArgs = gViewSourceUtils.buildEditorArgs(
this.file.path,
this.data.lineNumber
);
this.editor.runw(false, editorArgs, editorArgs.length);
this.contentLoaded = true;
this.resolve(this.data);
} catch (ex) {
// we failed loading it with the external editor.
console.error(ex);
this.reject(this.data);
} finally {
this.destroy();
}
},
editor: null,
resolve: null,
reject: null,
data: null,
file: null,
},
// returns an nsIFile for the passed document in the system temp directory
getTemporaryFile(aURI, aDocument, aContentType) {
// include contentAreaUtils.js in our own context when we first need it
if (!this._caUtils) {
this._caUtils = {};
Services.scriptloader.loadSubScript(
"chrome://global/content/contentAreaUtils.js",
this._caUtils
);
}
var fileName = this._caUtils.getDefaultFileName(
null,
aURI,
aDocument,
null
);
const mimeService = Cc["@mozilla.org/mime;1"].getService(Ci.nsIMIMEService);
fileName = mimeService.validateFileNameForSaving(
fileName,
aContentType,
mimeService.VALIDATE_DEFAULT
);
var tempFile = Services.dirsvc.get("TmpD", Ci.nsIFile);
tempFile.append(fileName);
return tempFile;
},
};