summaryrefslogtreecommitdiffstats
path: root/toolkit/components/viewsource/content/viewSourceUtils.js
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/components/viewsource/content/viewSourceUtils.js')
-rw-r--r--toolkit/components/viewsource/content/viewSourceUtils.js420
1 files changed, 420 insertions, 0 deletions
diff --git a/toolkit/components/viewsource/content/viewSourceUtils.js b/toolkit/components/viewsource/content/viewSourceUtils.js
new file mode 100644
index 0000000000..69d3eff287
--- /dev/null
+++ b/toolkit/components/viewsource/content/viewSourceUtils.js
@@ -0,0 +1,420 @@
+// -*- 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/. */
+
+/*
+ * 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.BrowserViewSourceOfDocument) {
+ browserWin.BrowserViewSourceOfDocument(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;
+ },
+};