diff options
Diffstat (limited to 'toolkit/components/viewsource/content/viewSourceUtils.js')
-rw-r--r-- | toolkit/components/viewsource/content/viewSourceUtils.js | 420 |
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; + }, +}; |