// -*- 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 . 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 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 * . 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; }, };