/* 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/. */ var { AppConstants } = ChromeUtils.import( "resource://gre/modules/AppConstants.jsm" ); var { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); var { XPCOMUtils } = ChromeUtils.import( "resource://gre/modules/XPCOMUtils.jsm" ); XPCOMUtils.defineLazyModuleGetters(this, { BrowserUtils: "resource://gre/modules/BrowserUtils.jsm", Downloads: "resource://gre/modules/Downloads.jsm", DownloadPaths: "resource://gre/modules/DownloadPaths.jsm", DownloadLastDir: "resource://gre/modules/DownloadLastDir.jsm", FileUtils: "resource://gre/modules/FileUtils.jsm", OS: "resource://gre/modules/osfile.jsm", PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.jsm", Deprecated: "resource://gre/modules/Deprecated.jsm", NetUtil: "resource://gre/modules/NetUtil.jsm", }); var ContentAreaUtils = { get stringBundle() { delete this.stringBundle; return (this.stringBundle = Services.strings.createBundle( "chrome://global/locale/contentAreaCommands.properties" )); }, }; function urlSecurityCheck(aURL, aPrincipal, aFlags) { return BrowserUtils.urlSecurityCheck(aURL, aPrincipal, aFlags); } // Clientele: (Make sure you don't break any of these) // - File -> Save Page/Frame As... // - Context -> Save Page/Frame As... // - Context -> Save Link As... // - Alt-Click links in web pages // - Alt-Click links in the UI // // Try saving each of these types: // - A complete webpage using File->Save Page As, and Context->Save Page As // - A webpage as HTML only using the above methods // - A webpage as Text only using the above methods // - An image with an extension (e.g. .jpg) in its file name, using // Context->Save Image As... // - An image without an extension (e.g. a banner ad on cnn.com) using // the above method. // - A linked document using Save Link As... // - A linked document using Alt-click Save Link As... // function saveURL( aURL, aFileName, aFilePickerTitleKey, aShouldBypassCache, aSkipPrompt, aReferrerInfo, aCookieJarSettings, aSourceDocument, aIsContentWindowPrivate, aPrincipal ) { internalSave( aURL, null, aFileName, null, null, aShouldBypassCache, aFilePickerTitleKey, null, aReferrerInfo, aCookieJarSettings, aSourceDocument, aSkipPrompt, null, aIsContentWindowPrivate, aPrincipal ); } // Save the current document inside any browser/frame-like element, // whether in-process or out-of-process. function saveBrowser(aBrowser, aSkipPrompt, aBrowsingContext = null) { if (!aBrowser) { throw new Error("Must have a browser when calling saveBrowser"); } let persistable = aBrowser.frameLoader; // PDF.js has its own way to handle saving PDFs since it may need to // generate a new PDF to save modified form data. if (aBrowser.contentPrincipal.spec == "resource://pdf.js/web/viewer.html") { aBrowser.sendMessageToActor("PDFJS:Save", {}, "Pdfjs"); return; } let stack = Components.stack.caller; persistable.startPersistence(aBrowsingContext, { onDocumentReady(document) { if (!document || !(document instanceof Ci.nsIWebBrowserPersistDocument)) { throw new Error("Must have an nsIWebBrowserPersistDocument!"); } internalSave( document.documentURI, document, null, // file name document.contentDisposition, document.contentType, false, // bypass cache null, // file picker title key null, // chosen file data document.referrerInfo, document.cookieJarSettings, document, aSkipPrompt, document.cacheKey ); }, onError(status) { throw new Components.Exception( "saveBrowser failed asynchronously in startPersistence", status, stack ); }, }); } function DownloadListener(win, transfer) { function makeClosure(name) { return function() { transfer[name].apply(transfer, arguments); }; } this.window = win; // Now... we need to forward all calls to our transfer for (var i in transfer) { if (i != "QueryInterface") { this[i] = makeClosure(i); } } } DownloadListener.prototype = { QueryInterface: ChromeUtils.generateQI([ "nsIInterfaceRequestor", "nsIWebProgressListener", "nsIWebProgressListener2", ]), getInterface: function dl_gi(aIID) { if (aIID.equals(Ci.nsIAuthPrompt) || aIID.equals(Ci.nsIAuthPrompt2)) { var ww = Cc["@mozilla.org/embedcomp/window-watcher;1"].getService( Ci.nsIPromptFactory ); return ww.getPrompt(this.window, aIID); } throw Components.Exception("", Cr.NS_ERROR_NO_INTERFACE); }, }; const kSaveAsType_Complete = 0; // Save document with attached objects. XPCOMUtils.defineConstant(this, "kSaveAsType_Complete", 0); // const kSaveAsType_URL = 1; // Save document or URL by itself. const kSaveAsType_Text = 2; // Save document, converting to plain text. XPCOMUtils.defineConstant(this, "kSaveAsType_Text", kSaveAsType_Text); /** * internalSave: Used when saving a document or URL. * * If aChosenData is null, this method: * - Determines a local target filename to use * - Prompts the user to confirm the destination filename and save mode * (aContentType affects this) * - [Note] This process involves the parameters aURL, aReferrerInfo, * aDocument, aDefaultFileName, aFilePickerTitleKey, and aSkipPrompt. * * If aChosenData is non-null, this method: * - Uses the provided source URI and save file name * - Saves the document as complete DOM if possible (aDocument present and * right aContentType) * - [Note] The parameters aURL, aDefaultFileName, aFilePickerTitleKey, and * aSkipPrompt are ignored. * * In any case, this method: * - Creates a 'Persist' object (which will perform the saving in the * background) and then starts it. * - [Note] This part of the process only involves the parameters aDocument, * aShouldBypassCache and aReferrerInfo. The source, the save name and the * save mode are the ones determined previously. * * @param aURL * The String representation of the URL of the document being saved * @param aDocument * The document to be saved * @param aDefaultFileName * The caller-provided suggested filename if we don't * find a better one * @param aContentDisposition * The caller-provided content-disposition header to use. * @param aContentType * The caller-provided content-type to use * @param aShouldBypassCache * If true, the document will always be refetched from the server * @param aFilePickerTitleKey * Alternate title for the file picker * @param aChosenData * If non-null this contains an instance of object AutoChosen (see below) * which holds pre-determined data so that the user does not need to be * prompted for a target filename. * @param aReferrerInfo * the referrerInfo object to use, or null if no referrer should be sent. * @param aCookieJarSettings * the cookieJarSettings object to use. This will be used for the channel * used to save. * @param aInitiatingDocument [optional] * The document from which the save was initiated. * If this is omitted then aIsContentWindowPrivate has to be provided. * @param aSkipPrompt [optional] * If set to true, we will attempt to save the file to the * default downloads folder without prompting. * @param aCacheKey [optional] * If set will be passed to saveURI. See nsIWebBrowserPersist for * allowed values. * @param aIsContentWindowPrivate [optional] * This parameter is provided when the aInitiatingDocument is not a * real document object. Stores whether aInitiatingDocument.defaultView * was private or not. * @param aPrincipal [optional] * This parameter is provided when neither aDocument nor * aInitiatingDocument is provided. Used to determine what level of * privilege to load the URI with. */ function internalSave( aURL, aDocument, aDefaultFileName, aContentDisposition, aContentType, aShouldBypassCache, aFilePickerTitleKey, aChosenData, aReferrerInfo, aCookieJarSettings, aInitiatingDocument, aSkipPrompt, aCacheKey, aIsContentWindowPrivate, aPrincipal ) { if (aSkipPrompt == undefined) { aSkipPrompt = false; } if (aCacheKey == undefined) { aCacheKey = 0; } // Note: aDocument == null when this code is used by save-link-as... var saveMode = GetSaveModeForContentType(aContentType, aDocument); var file, sourceURI, saveAsType; let contentPolicyType = Ci.nsIContentPolicy.TYPE_SAVEAS_DOWNLOAD; // Find the URI object for aURL and the FileName/Extension to use when saving. // FileName/Extension will be ignored if aChosenData supplied. if (aChosenData) { file = aChosenData.file; sourceURI = aChosenData.uri; saveAsType = kSaveAsType_Complete; continueSave(); } else { var charset = null; if (aDocument) { charset = aDocument.characterSet; } var fileInfo = new FileInfo(aDefaultFileName); initFileInfo( fileInfo, aURL, charset, aDocument, aContentType, aContentDisposition ); sourceURI = fileInfo.uri; if (aContentType && aContentType.startsWith("image/")) { contentPolicyType = Ci.nsIContentPolicy.TYPE_IMAGE; } var fpParams = { fpTitleKey: aFilePickerTitleKey, fileInfo, contentType: aContentType, saveMode, saveAsType: kSaveAsType_Complete, file, }; // Find a URI to use for determining last-downloaded-to directory let relatedURI = aReferrerInfo ? aReferrerInfo.originalReferrer : sourceURI; promiseTargetFile(fpParams, aSkipPrompt, relatedURI) .then(aDialogAccepted => { if (!aDialogAccepted) { return; } saveAsType = fpParams.saveAsType; file = fpParams.file; continueSave(); }) .catch(Cu.reportError); } function continueSave() { // XXX We depend on the following holding true in appendFiltersForContentType(): // If we should save as a complete page, the saveAsType is kSaveAsType_Complete. // If we should save as text, the saveAsType is kSaveAsType_Text. var useSaveDocument = aDocument && ((saveMode & SAVEMODE_COMPLETE_DOM && saveAsType == kSaveAsType_Complete) || (saveMode & SAVEMODE_COMPLETE_TEXT && saveAsType == kSaveAsType_Text)); // If we're saving a document, and are saving either in complete mode or // as converted text, pass the document to the web browser persist component. // If we're just saving the HTML (second option in the list), send only the URI. let isPrivate = aIsContentWindowPrivate; if (isPrivate === undefined) { isPrivate = aInitiatingDocument.nodeType == 9 /* DOCUMENT_NODE */ ? PrivateBrowsingUtils.isContentWindowPrivate( aInitiatingDocument.defaultView ) : aInitiatingDocument.isPrivate; } // We have to cover the cases here where we were either passed an explicit // principal, or a 'real' document (with a nodePrincipal property), or an // nsIWebBrowserPersistDocument which has a principal property. let sourcePrincipal = aPrincipal || (aDocument && (aDocument.nodePrincipal || aDocument.principal)) || (aInitiatingDocument && aInitiatingDocument.nodePrincipal); var persistArgs = { sourceURI, sourcePrincipal, sourceReferrerInfo: aReferrerInfo, sourceDocument: useSaveDocument ? aDocument : null, targetContentType: saveAsType == kSaveAsType_Text ? "text/plain" : null, targetFile: file, sourceCacheKey: aCacheKey, sourcePostData: aDocument ? getPostData(aDocument) : null, bypassCache: aShouldBypassCache, contentPolicyType, cookieJarSettings: aCookieJarSettings, isPrivate, }; // Start the actual save process internalPersist(persistArgs); } } /** * internalPersist: Creates a 'Persist' object (which will perform the saving * in the background) and then starts it. * * @param persistArgs.sourceURI * The nsIURI of the document being saved * @param persistArgs.sourceCacheKey [optional] * If set will be passed to savePrivacyAwareURI * @param persistArgs.sourceDocument [optional] * The document to be saved, or null if not saving a complete document * @param persistArgs.sourceReferrerInfo * Required and used only when persistArgs.sourceDocument is NOT present, * the nsIReferrerInfo of the referrer info to use, or null if no * referrer should be sent. * @param persistArgs.sourcePostData * Required and used only when persistArgs.sourceDocument is NOT present, * represents the POST data to be sent along with the HTTP request, and * must be null if no POST data should be sent. * @param persistArgs.targetFile * The nsIFile of the file to create * @param persistArgs.contentPolicyType * The type of content we're saving. Will be used to determine what * content is accepted, enforce sniffing restrictions, etc. * @param persistArgs.cookieJarSettings [optional] * The nsICookieJarSettings that will be used for the saving channel, or * null that savePrivacyAwareURI will create one based on the current * state of the prefs/permissions * @param persistArgs.targetContentType * Required and used only when persistArgs.sourceDocument is present, * determines the final content type of the saved file, or null to use * the same content type as the source document. Currently only * "text/plain" is meaningful. * @param persistArgs.bypassCache * If true, the document will always be refetched from the server * @param persistArgs.isPrivate * Indicates whether this is taking place in a private browsing context. */ function internalPersist(persistArgs) { var persist = makeWebBrowserPersist(); // Calculate persist flags. const nsIWBP = Ci.nsIWebBrowserPersist; const flags = nsIWBP.PERSIST_FLAGS_REPLACE_EXISTING_FILES; if (persistArgs.bypassCache) { persist.persistFlags = flags | nsIWBP.PERSIST_FLAGS_BYPASS_CACHE; } else { persist.persistFlags = flags | nsIWBP.PERSIST_FLAGS_FROM_CACHE; } // Leave it to WebBrowserPersist to discover the encoding type (or lack thereof): persist.persistFlags |= nsIWBP.PERSIST_FLAGS_AUTODETECT_APPLY_CONVERSION; // Find the URI associated with the target file var targetFileURL = makeFileURI(persistArgs.targetFile); // Create download and initiate it (below) var tr = Cc["@mozilla.org/transfer;1"].createInstance(Ci.nsITransfer); tr.init( persistArgs.sourceURI, targetFileURL, "", null, null, null, persist, persistArgs.isPrivate, Ci.nsITransfer.DOWNLOAD_ACCEPTABLE ); persist.progressListener = new DownloadListener(window, tr); if (persistArgs.sourceDocument) { // Saving a Document, not a URI: var filesFolder = null; if (persistArgs.targetContentType != "text/plain") { // Create the local directory into which to save associated files. filesFolder = persistArgs.targetFile.clone(); var nameWithoutExtension = getFileBaseName(filesFolder.leafName); var filesFolderLeafName = ContentAreaUtils.stringBundle.formatStringFromName( "filesFolder", [nameWithoutExtension] ); filesFolder.leafName = filesFolderLeafName; } var encodingFlags = 0; if (persistArgs.targetContentType == "text/plain") { encodingFlags |= nsIWBP.ENCODE_FLAGS_FORMATTED; encodingFlags |= nsIWBP.ENCODE_FLAGS_ABSOLUTE_LINKS; encodingFlags |= nsIWBP.ENCODE_FLAGS_NOFRAMES_CONTENT; } else { encodingFlags |= nsIWBP.ENCODE_FLAGS_ENCODE_BASIC_ENTITIES; } const kWrapColumn = 80; persist.saveDocument( persistArgs.sourceDocument, targetFileURL, filesFolder, persistArgs.targetContentType, encodingFlags, kWrapColumn ); } else { persist.savePrivacyAwareURI( persistArgs.sourceURI, persistArgs.sourcePrincipal, persistArgs.sourceCacheKey, persistArgs.sourceReferrerInfo, persistArgs.cookieJarSettings, persistArgs.sourcePostData, null, targetFileURL, persistArgs.contentPolicyType || Ci.nsIContentPolicy.TYPE_SAVEAS_DOWNLOAD, persistArgs.isPrivate ); } } /** * Structure for holding info about automatically supplied parameters for * internalSave(...). This allows parameters to be supplied so the user does not * need to be prompted for file info. * @param aFileAutoChosen This is an nsIFile object that has been * pre-determined as the filename for the target to save to * @param aUriAutoChosen This is the nsIURI object for the target */ function AutoChosen(aFileAutoChosen, aUriAutoChosen) { this.file = aFileAutoChosen; this.uri = aUriAutoChosen; } /** * Structure for holding info about a URL and the target filename it should be * saved to. This structure is populated by initFileInfo(...). * @param aSuggestedFileName This is used by initFileInfo(...) when it * cannot 'discover' the filename from the url * @param aFileName The target filename * @param aFileBaseName The filename without the file extension * @param aFileExt The extension of the filename * @param aUri An nsIURI object for the url that is being saved */ function FileInfo( aSuggestedFileName, aFileName, aFileBaseName, aFileExt, aUri ) { this.suggestedFileName = aSuggestedFileName; this.fileName = aFileName; this.fileBaseName = aFileBaseName; this.fileExt = aFileExt; this.uri = aUri; } /** * Determine what the 'default' filename string is, its file extension and the * filename without the extension. This filename is used when prompting the user * for confirmation in the file picker dialog. * @param aFI A FileInfo structure into which we'll put the results of this method. * @param aURL The String representation of the URL of the document being saved * @param aURLCharset The charset of aURL. * @param aDocument The document to be saved * @param aContentType The content type we're saving, if it could be * determined by the caller. * @param aContentDisposition The content-disposition header for the object * we're saving, if it could be determined by the caller. */ function initFileInfo( aFI, aURL, aURLCharset, aDocument, aContentType, aContentDisposition ) { try { // Get an nsIURI object from aURL if possible: try { aFI.uri = makeURI(aURL, aURLCharset); // Assuming nsiUri is valid, calling QueryInterface(...) on it will // populate extra object fields (eg filename and file extension). var url = aFI.uri.QueryInterface(Ci.nsIURL); aFI.fileExt = url.fileExtension; } catch (e) {} // Get the default filename: aFI.fileName = getDefaultFileName( aFI.suggestedFileName || aFI.fileName, aFI.uri, aDocument, aContentDisposition ); // If aFI.fileExt is still blank, consider: aFI.suggestedFileName is supplied // if saveURL(...) was the original caller (hence both aContentType and // aDocument are blank). If they were saving a link to a website then make // the extension .htm . if ( !aFI.fileExt && !aDocument && !aContentType && /^http(s?):\/\//i.test(aURL) ) { aFI.fileExt = "htm"; aFI.fileBaseName = aFI.fileName; } else { aFI.fileExt = getDefaultExtension(aFI.fileName, aFI.uri, aContentType); aFI.fileBaseName = getFileBaseName(aFI.fileName); } } catch (e) {} } /** * Given the Filepicker Parameters (aFpP), show the file picker dialog, * prompting the user to confirm (or change) the fileName. * @param aFpP * A structure (see definition in internalSave(...) method) * containing all the data used within this method. * @param aSkipPrompt * If true, attempt to save the file automatically to the user's default * download directory, thus skipping the explicit prompt for a file name, * but only if the associated preference is set. * If false, don't save the file automatically to the user's * default download directory, even if the associated preference * is set, but ask for the target explicitly. * @param aRelatedURI * An nsIURI associated with the download. The last used * directory of the picker is retrieved from/stored in the * Content Pref Service using this URI. * @return Promise * @resolve a boolean. When true, it indicates that the file picker dialog * is accepted. */ function promiseTargetFile( aFpP, /* optional */ aSkipPrompt, /* optional */ aRelatedURI ) { return (async function() { let downloadLastDir = new DownloadLastDir(window); let prefBranch = Services.prefs.getBranch("browser.download."); let useDownloadDir = prefBranch.getBoolPref("useDownloadDir"); if (!aSkipPrompt) { useDownloadDir = false; } // Default to the user's default downloads directory configured // through download prefs. let dirPath = await Downloads.getPreferredDownloadsDirectory(); let dirExists = await OS.File.exists(dirPath); let dir = new FileUtils.File(dirPath); if (useDownloadDir && dirExists) { dir.append( getNormalizedLeafName(aFpP.fileInfo.fileName, aFpP.fileInfo.fileExt) ); aFpP.file = uniqueFile(dir); return true; } // We must prompt for the file name explicitly. // If we must prompt because we were asked to... let file = await new Promise(resolve => { if (useDownloadDir) { // Keep async behavior in both branches Services.tm.dispatchToMainThread(function() { resolve(null); }); } else { downloadLastDir.getFileAsync(aRelatedURI, function getFileAsyncCB( aFile ) { resolve(aFile); }); } }); if (file && (await OS.File.exists(file.path))) { dir = file; dirExists = true; } if (!dirExists) { // Default to desktop. dir = Services.dirsvc.get("Desk", Ci.nsIFile); } let fp = makeFilePicker(); let titleKey = aFpP.fpTitleKey || "SaveLinkTitle"; fp.init( window, ContentAreaUtils.stringBundle.GetStringFromName(titleKey), Ci.nsIFilePicker.modeSave ); fp.displayDirectory = dir; fp.defaultExtension = aFpP.fileInfo.fileExt; fp.defaultString = getNormalizedLeafName( aFpP.fileInfo.fileName, aFpP.fileInfo.fileExt ); appendFiltersForContentType( fp, aFpP.contentType, aFpP.fileInfo.fileExt, aFpP.saveMode ); // The index of the selected filter is only preserved and restored if there's // more than one filter in addition to "All Files". if (aFpP.saveMode != SAVEMODE_FILEONLY) { // eslint-disable-next-line mozilla/use-default-preference-values try { fp.filterIndex = prefBranch.getIntPref("save_converter_index"); } catch (e) {} } let result = await new Promise(resolve => { fp.open(function(aResult) { resolve(aResult); }); }); if (result == Ci.nsIFilePicker.returnCancel || !fp.file) { return false; } if (aFpP.saveMode != SAVEMODE_FILEONLY) { prefBranch.setIntPref("save_converter_index", fp.filterIndex); } // Do not store the last save directory as a pref inside the private browsing mode downloadLastDir.setFile(aRelatedURI, fp.file.parent); fp.file.leafName = validateFileName(fp.file.leafName); aFpP.saveAsType = fp.filterIndex; aFpP.file = fp.file; aFpP.fileURL = fp.fileURL; return true; })(); } // Since we're automatically downloading, we don't get the file picker's // logic to check for existing files, so we need to do that here. // // Note - this code is identical to that in // mozilla/toolkit/mozapps/downloads/src/nsHelperAppDlg.js.in // If you are updating this code, update that code too! We can't share code // here since that code is called in a js component. function uniqueFile(aLocalFile) { var collisionCount = 0; while (aLocalFile.exists()) { collisionCount++; if (collisionCount == 1) { // Append "(2)" before the last dot in (or at the end of) the filename // special case .ext.gz etc files so we don't wind up with .tar(2).gz if (aLocalFile.leafName.match(/\.[^\.]{1,3}\.(gz|bz2|Z)$/i)) { aLocalFile.leafName = aLocalFile.leafName.replace( /\.[^\.]{1,3}\.(gz|bz2|Z)$/i, "(2)$&" ); } else { aLocalFile.leafName = aLocalFile.leafName.replace( /(\.[^\.]*)?$/, "(2)$&" ); } } else { // replace the last (n) in the filename with (n+1) aLocalFile.leafName = aLocalFile.leafName.replace( /^(.*\()\d+\)/, "$1" + (collisionCount + 1) + ")" ); } } return aLocalFile; } /** * Download a URL using the Downloads API. * * @param aURL * the url to download * @param [optional] aFileName * the destination file name, if omitted will be obtained from the url. * @param aInitiatingDocument * The document from which the download was initiated. */ function DownloadURL(aURL, aFileName, aInitiatingDocument) { // For private browsing, try to get document out of the most recent browser // window, or provide our own if there's no browser window. let isPrivate = aInitiatingDocument.defaultView.docShell.QueryInterface( Ci.nsILoadContext ).usePrivateBrowsing; let fileInfo = new FileInfo(aFileName); initFileInfo(fileInfo, aURL, null, null, null, null); let filepickerParams = { fileInfo, saveMode: SAVEMODE_FILEONLY, }; (async function() { let accepted = await promiseTargetFile( filepickerParams, true, fileInfo.uri ); if (!accepted) { return; } let file = filepickerParams.file; let download = await Downloads.createDownload({ source: { url: aURL, isPrivate }, target: { path: file.path, partFilePath: file.path + ".part" }, }); download.tryToKeepPartialData = true; // Ignore errors because failures are reported through the download list. download.start().catch(() => {}); // Add the download to the list, allowing it to be managed. let list = await Downloads.getList(Downloads.ALL); list.add(download); })().catch(Cu.reportError); } // We have no DOM, and can only save the URL as is. const SAVEMODE_FILEONLY = 0x00; XPCOMUtils.defineConstant(this, "SAVEMODE_FILEONLY", SAVEMODE_FILEONLY); // We have a DOM and can save as complete. const SAVEMODE_COMPLETE_DOM = 0x01; XPCOMUtils.defineConstant(this, "SAVEMODE_COMPLETE_DOM", SAVEMODE_COMPLETE_DOM); // We have a DOM which we can serialize as text. const SAVEMODE_COMPLETE_TEXT = 0x02; XPCOMUtils.defineConstant( this, "SAVEMODE_COMPLETE_TEXT", SAVEMODE_COMPLETE_TEXT ); // If we are able to save a complete DOM, the 'save as complete' filter // must be the first filter appended. The 'save page only' counterpart // must be the second filter appended. And the 'save as complete text' // filter must be the third filter appended. function appendFiltersForContentType( aFilePicker, aContentType, aFileExtension, aSaveMode ) { // The bundle name for saving only a specific content type. var bundleName; // The corresponding filter string for a specific content type. var filterString; // Every case where GetSaveModeForContentType can return non-FILEONLY // modes must be handled here. if (aSaveMode != SAVEMODE_FILEONLY) { switch (aContentType) { case "text/html": bundleName = "WebPageHTMLOnlyFilter"; filterString = "*.htm; *.html"; break; case "application/xhtml+xml": bundleName = "WebPageXHTMLOnlyFilter"; filterString = "*.xht; *.xhtml"; break; case "image/svg+xml": bundleName = "WebPageSVGOnlyFilter"; filterString = "*.svg; *.svgz"; break; case "text/xml": case "application/xml": bundleName = "WebPageXMLOnlyFilter"; filterString = "*.xml"; break; } } if (!bundleName) { if (aSaveMode != SAVEMODE_FILEONLY) { throw new Error(`Invalid save mode for type '${aContentType}'`); } var mimeInfo = getMIMEInfoForType(aContentType, aFileExtension); if (mimeInfo) { var extString = ""; for (var extension of mimeInfo.getFileExtensions()) { if (extString) { extString += "; "; } // If adding more than one extension, // separate by semi-colon extString += "*." + extension; } if (extString) { aFilePicker.appendFilter(mimeInfo.description, extString); } } } if (aSaveMode & SAVEMODE_COMPLETE_DOM) { aFilePicker.appendFilter( ContentAreaUtils.stringBundle.GetStringFromName("WebPageCompleteFilter"), filterString ); // We should always offer a choice to save document only if // we allow saving as complete. aFilePicker.appendFilter( ContentAreaUtils.stringBundle.GetStringFromName(bundleName), filterString ); } if (aSaveMode & SAVEMODE_COMPLETE_TEXT) { aFilePicker.appendFilters(Ci.nsIFilePicker.filterText); } // Always append the all files (*) filter aFilePicker.appendFilters(Ci.nsIFilePicker.filterAll); } function getPostData(aDocument) { if (aDocument instanceof Ci.nsIWebBrowserPersistDocument) { return aDocument.postData; } try { // Find the session history entry corresponding to the given document. In // the current implementation, nsIWebPageDescriptor.currentDescriptor always // returns a session history entry. let sessionHistoryEntry = aDocument.defaultView.docShell .QueryInterface(Ci.nsIWebPageDescriptor) .currentDescriptor.QueryInterface(Ci.nsISHEntry); return sessionHistoryEntry.postData; } catch (e) {} return null; } function makeWebBrowserPersist() { const persistContractID = "@mozilla.org/embedding/browser/nsWebBrowserPersist;1"; const persistIID = Ci.nsIWebBrowserPersist; return Cc[persistContractID].createInstance(persistIID); } function makeURI(aURL, aOriginCharset, aBaseURI) { return Services.io.newURI(aURL, aOriginCharset, aBaseURI); } function makeFileURI(aFile) { return Services.io.newFileURI(aFile); } function makeFilePicker() { const fpContractID = "@mozilla.org/filepicker;1"; const fpIID = Ci.nsIFilePicker; return Cc[fpContractID].createInstance(fpIID); } function getMIMEService() { const mimeSvcContractID = "@mozilla.org/mime;1"; const mimeSvcIID = Ci.nsIMIMEService; const mimeSvc = Cc[mimeSvcContractID].getService(mimeSvcIID); return mimeSvc; } // Given aFileName, find the fileName without the extension on the end. function getFileBaseName(aFileName) { // Remove the file extension from aFileName: return aFileName.replace(/\.[^.]*$/, ""); } function getMIMETypeForURI(aURI) { try { return getMIMEService().getTypeFromURI(aURI); } catch (e) {} return null; } function getMIMEInfoForType(aMIMEType, aExtension) { if (aMIMEType || aExtension) { try { return getMIMEService().getFromTypeAndExtension(aMIMEType, aExtension); } catch (e) {} } return null; } function getDefaultFileName( aDefaultFileName, aURI, aDocument, aContentDisposition ) { // 1) look for a filename in the content-disposition header, if any if (aContentDisposition) { const mhpContractID = "@mozilla.org/network/mime-hdrparam;1"; const mhpIID = Ci.nsIMIMEHeaderParam; const mhp = Cc[mhpContractID].getService(mhpIID); var dummy = { value: null }; // Need an out param... var charset = getCharsetforSave(aDocument); var fileName = null; try { fileName = mhp.getParameter( aContentDisposition, "filename", charset, true, dummy ); } catch (e) { try { fileName = mhp.getParameter( aContentDisposition, "name", charset, true, dummy ); } catch (e) {} } if (fileName) { return validateFileName(Services.textToSubURI.unEscapeURIForUI(fileName)); } } let docTitle; if (aDocument && aDocument.title && aDocument.title.trim()) { // If the document looks like HTML or XML, try to use its original title. docTitle = validateFileName(aDocument.title); let contentType = aDocument.contentType; if ( contentType == "application/xhtml+xml" || contentType == "application/xml" || contentType == "image/svg+xml" || contentType == "text/html" || contentType == "text/xml" ) { // 2) Use the document title return docTitle; } } try { var url = aURI.QueryInterface(Ci.nsIURL); if (url.fileName != "") { // 3) Use the actual file name, if present return validateFileName( Services.textToSubURI.unEscapeURIForUI(url.fileName) ); } } catch (e) { // This is something like a data: and so forth URI... no filename here. } if (docTitle) { // 4) Use the document title return docTitle; } if (aDefaultFileName) { // 5) Use the caller-provided name, if any return validateFileName(aDefaultFileName); } // 6) If this is a directory, use the last directory name var path = aURI.pathQueryRef.match(/\/([^\/]+)\/$/); if (path && path.length > 1) { return validateFileName(path[1]); } try { if (aURI.host) { // 7) Use the host. return validateFileName(aURI.host); } } catch (e) { // Some files have no information at all, like Javascript generated pages } try { // 8) Use the default file name return ContentAreaUtils.stringBundle.GetStringFromName( "DefaultSaveFileName" ); } catch (e) { // in case localized string cannot be found } // 9) If all else fails, use "index" return "index"; } function validateFileName(aFileName) { let processed = DownloadPaths.sanitize(aFileName) || "_"; if (AppConstants.platform == "android") { // If a large part of the filename has been sanitized, then we // will use a default filename instead if (processed.replace(/_/g, "").length <= processed.length / 2) { // We purposefully do not use a localized default filename, // which we could have done using // ContentAreaUtils.stringBundle.GetStringFromName("DefaultSaveFileName") // since it may contain invalid characters. var original = processed; processed = "download"; // Preserve a suffix, if there is one if (original.includes(".")) { var suffix = original.split(".").slice(-1)[0]; if (suffix && !suffix.includes("_")) { processed += "." + suffix; } } } } return processed; } // This is the set of image extensions supported by extraMimeEntries in // nsExternalHelperAppService. const kImageExtensions = new Set([ "art", "bmp", "gif", "ico", "cur", "jpeg", "jpg", "jfif", "pjpeg", "pjp", "png", "apng", "tiff", "tif", "xbm", "svg", "webp", "avif", ]); function getNormalizedLeafName(aFile, aDefaultExtension) { if (!aDefaultExtension) { return aFile; } if (AppConstants.platform == "win") { // Remove trailing dots and spaces on windows aFile = aFile.replace(/[\s.]+$/, ""); } // Remove leading dots aFile = aFile.replace(/^\.+/, ""); // Include the default extension in the file name to which we're saving. var i = aFile.lastIndexOf("."); let previousExtension = aFile.substr(i + 1).toLowerCase(); if (previousExtension != aDefaultExtension.toLowerCase()) { // Suffixing the extension is the safe bet - it preserves the previous // extension in case that's meaningful (e.g. various text files served // with text/plain will end up as `foo.cpp.txt`, in the worst case). // However, for images, the extension derived from the URL should be // replaced if the content type indicates a different filetype - this makes // sure that we treat e.g. feature-tested webp/avif images correctly. if (kImageExtensions.has(previousExtension)) { return aFile.substr(0, i + 1) + aDefaultExtension; } return aFile + "." + aDefaultExtension; } return aFile; } function getDefaultExtension(aFilename, aURI, aContentType) { if ( aContentType == "text/plain" || aContentType == "application/octet-stream" || aURI.scheme == "ftp" ) { return ""; } // temporary fix for bug 120327 // First try the extension from the filename var url = Cc["@mozilla.org/network/standard-url-mutator;1"] .createInstance(Ci.nsIURIMutator) .setSpec("http://example.com") // construct the URL .setFilePath(aFilename) .finalize() .QueryInterface(Ci.nsIURL); var ext = url.fileExtension; // This mirrors some code in nsExternalHelperAppService::DoContent // Use the filename first and then the URI if that fails // For images, rely solely on the mime type if known. // All the extension is going to do is lie to us. var lookupExt = ext; if (aContentType?.startsWith("image/")) { lookupExt = ""; } var mimeInfo = getMIMEInfoForType(aContentType, lookupExt); if (ext && mimeInfo && mimeInfo.extensionExists(ext)) { return ext; } // Well, that failed. Now try the extension from the URI var urlext; try { url = aURI.QueryInterface(Ci.nsIURL); urlext = url.fileExtension; } catch (e) {} if (urlext && mimeInfo && mimeInfo.extensionExists(urlext)) { return urlext; } // That failed as well. If we could lookup the MIME use the primary // extension for that type. try { if (mimeInfo) { return mimeInfo.primaryExtension; } } catch (e) {} // Fall back on the extensions in the filename and URI for lack // of anything better. return ext || urlext; } function GetSaveModeForContentType(aContentType, aDocument) { // We can only save a complete page if we have a loaded document, if (!aDocument) { return SAVEMODE_FILEONLY; } // Find the possible save modes using the provided content type var saveMode = SAVEMODE_FILEONLY; switch (aContentType) { case "text/html": case "application/xhtml+xml": case "image/svg+xml": saveMode |= SAVEMODE_COMPLETE_TEXT; // Fall through case "text/xml": case "application/xml": saveMode |= SAVEMODE_COMPLETE_DOM; break; } return saveMode; } function getCharsetforSave(aDocument) { if (aDocument) { return aDocument.characterSet; } if (document.commandDispatcher.focusedWindow) { return document.commandDispatcher.focusedWindow.document.characterSet; } return window.content.document.characterSet; } /** * Open a URL from chrome, determining if we can handle it internally or need to * launch an external application to handle it. * @param aURL The URL to be opened * * WARNING: Please note that openURL() does not perform any content security checks!!! */ function openURL(aURL) { var uri = aURL instanceof Ci.nsIURI ? aURL : makeURI(aURL); var protocolSvc = Cc[ "@mozilla.org/uriloader/external-protocol-service;1" ].getService(Ci.nsIExternalProtocolService); let recentWindow = Services.wm.getMostRecentWindow("navigator:browser"); if (!protocolSvc.isExposedProtocol(uri.scheme)) { // If we're not a browser, use the external protocol service to load the URI. protocolSvc.loadURI(uri, recentWindow?.document.contentPrincipal); } else { if (recentWindow) { recentWindow.openWebLinkIn(uri.spec, "tab", { triggeringPrincipal: recentWindow.document.contentPrincipal, }); return; } var loadgroup = Cc["@mozilla.org/network/load-group;1"].createInstance( Ci.nsILoadGroup ); var appstartup = Services.startup; var loadListener = { onStartRequest: function ll_start(aRequest) { appstartup.enterLastWindowClosingSurvivalArea(); }, onStopRequest: function ll_stop(aRequest, aStatusCode) { appstartup.exitLastWindowClosingSurvivalArea(); }, QueryInterface: ChromeUtils.generateQI([ "nsIRequestObserver", "nsISupportsWeakReference", ]), }; loadgroup.groupObserver = loadListener; var uriListener = { doContent(ctype, preferred, request, handler) { return false; }, isPreferred(ctype, desired) { return false; }, canHandleContent(ctype, preferred, desired) { return false; }, loadCookie: null, parentContentListener: null, getInterface(iid) { if (iid.equals(Ci.nsIURIContentListener)) { return this; } if (iid.equals(Ci.nsILoadGroup)) { return loadgroup; } throw Components.Exception("", Cr.NS_ERROR_NO_INTERFACE); }, }; var channel = NetUtil.newChannel({ uri, loadUsingSystemPrincipal: true, }); if (channel) { channel.channelIsForDownload = true; } var uriLoader = Cc["@mozilla.org/uriloader;1"].getService(Ci.nsIURILoader); uriLoader.openURI( channel, Ci.nsIURILoader.IS_CONTENT_PREFERRED, uriListener ); } }