1 /* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this file,
3 * You can obtain one at http://mozilla.org/MPL/2.0/. */
5 var { AppConstants } = ChromeUtils.importESModule(
6 "resource://gre/modules/AppConstants.sys.mjs"
8 var { XPCOMUtils } = ChromeUtils.importESModule(
9 "resource://gre/modules/XPCOMUtils.sys.mjs"
12 ChromeUtils.defineESModuleGetters(this, {
13 BrowserUtils: "resource://gre/modules/BrowserUtils.sys.mjs",
14 Deprecated: "resource://gre/modules/Deprecated.sys.mjs",
15 FileUtils: "resource://gre/modules/FileUtils.sys.mjs",
16 PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs",
19 XPCOMUtils.defineLazyModuleGetters(this, {
20 Downloads: "resource://gre/modules/Downloads.jsm",
21 DownloadPaths: "resource://gre/modules/DownloadPaths.jsm",
22 DownloadLastDir: "resource://gre/modules/DownloadLastDir.jsm",
23 NetUtil: "resource://gre/modules/NetUtil.jsm",
26 var ContentAreaUtils = {
28 delete this.stringBundle;
29 return (this.stringBundle = Services.strings.createBundle(
30 "chrome://global/locale/contentAreaCommands.properties"
35 function urlSecurityCheck(
38 aFlags = Services.scriptSecurityManager
40 if (aURL instanceof Ci.nsIURI) {
41 Services.scriptSecurityManager.checkLoadURIWithPrincipal(
47 Services.scriptSecurityManager.checkLoadURIStrWithPrincipal(
55 // Clientele: (Make sure you don't break any of these)
56 // - File -> Save Page/Frame As...
57 // - Context -> Save Page/Frame As...
58 // - Context -> Save Link As...
59 // - Alt-Click links in web pages
60 // - Alt-Click links in the UI
62 // Try saving each of these types:
63 // - A complete webpage using File->Save Page As, and Context->Save Page As
64 // - A webpage as HTML only using the above methods
65 // - A webpage as Text only using the above methods
66 // - An image with an extension (e.g. .jpg) in its file name, using
67 // Context->Save Image As...
68 // - An image without an extension (e.g. a banner ad on cnn.com) using
70 // - A linked document using Save Link As...
71 // - A linked document using Alt-click Save Link As...
83 aIsContentWindowPrivate,
101 aIsContentWindowPrivate,
106 // Save the current document inside any browser/frame-like element,
107 // whether in-process or out-of-process.
108 function saveBrowser(aBrowser, aSkipPrompt, aBrowsingContext = null) {
110 throw new Error("Must have a browser when calling saveBrowser");
112 let persistable = aBrowser.frameLoader;
113 // PDF.js has its own way to handle saving PDFs since it may need to
114 // generate a new PDF to save modified form data.
115 if (aBrowser.contentPrincipal.spec == "resource://pdf.js/web/viewer.html") {
116 aBrowser.sendMessageToActor("PDFJS:Save", {}, "Pdfjs");
119 let stack = Components.stack.caller;
120 persistable.startPersistence(aBrowsingContext, {
121 onDocumentReady(document) {
122 if (!document || !(document instanceof Ci.nsIWebBrowserPersistDocument)) {
123 throw new Error("Must have an nsIWebBrowserPersistDocument!");
127 document.documentURI,
131 document.contentDisposition,
132 document.contentType,
133 false, // bypass cache
134 null, // file picker title key
135 null, // chosen file data
136 document.referrerInfo,
137 document.cookieJarSettings,
144 throw new Components.Exception(
145 "saveBrowser failed asynchronously in startPersistence",
153 function DownloadListener(win, transfer) {
154 function makeClosure(name) {
156 transfer[name].apply(transfer, arguments);
162 // Now... we need to forward all calls to our transfer
163 for (var i in transfer) {
164 if (i != "QueryInterface") {
165 this[i] = makeClosure(i);
170 DownloadListener.prototype = {
171 QueryInterface: ChromeUtils.generateQI([
172 "nsIInterfaceRequestor",
173 "nsIWebProgressListener",
174 "nsIWebProgressListener2",
177 getInterface: function dl_gi(aIID) {
178 if (aIID.equals(Ci.nsIAuthPrompt) || aIID.equals(Ci.nsIAuthPrompt2)) {
179 var ww = Cc["@mozilla.org/embedcomp/window-watcher;1"].getService(
182 return ww.getPrompt(this.window, aIID);
185 throw Components.Exception("", Cr.NS_ERROR_NO_INTERFACE);
189 const kSaveAsType_Complete = 0; // Save document with attached objects.
190 XPCOMUtils.defineConstant(this, "kSaveAsType_Complete", 0);
191 // const kSaveAsType_URL = 1; // Save document or URL by itself.
192 const kSaveAsType_Text = 2; // Save document, converting to plain text.
193 XPCOMUtils.defineConstant(this, "kSaveAsType_Text", kSaveAsType_Text);
196 * internalSave: Used when saving a document or URL.
198 * If aChosenData is null, this method:
199 * - Determines a local target filename to use
200 * - Prompts the user to confirm the destination filename and save mode
201 * (aContentType affects this)
202 * - [Note] This process involves the parameters aURL, aReferrerInfo,
203 * aDocument, aDefaultFileName, aFilePickerTitleKey, and aSkipPrompt.
205 * If aChosenData is non-null, this method:
206 * - Uses the provided source URI and save file name
207 * - Saves the document as complete DOM if possible (aDocument present and
208 * right aContentType)
209 * - [Note] The parameters aURL, aDefaultFileName, aFilePickerTitleKey, and
210 * aSkipPrompt are ignored.
212 * In any case, this method:
213 * - Creates a 'Persist' object (which will perform the saving in the
214 * background) and then starts it.
215 * - [Note] This part of the process only involves the parameters aDocument,
216 * aShouldBypassCache and aReferrerInfo. The source, the save name and the
217 * save mode are the ones determined previously.
220 * The String representation of the URL of the document being saved
221 * @param aOriginalURL
222 * The String representation of the original URL of the document being
223 * saved. It can useful in case aURL is a blob.
225 * The document to be saved
226 * @param aDefaultFileName
227 * The caller-provided suggested filename if we don't
229 * @param aContentDisposition
230 * The caller-provided content-disposition header to use.
231 * @param aContentType
232 * The caller-provided content-type to use
233 * @param aShouldBypassCache
234 * If true, the document will always be refetched from the server
235 * @param aFilePickerTitleKey
236 * Alternate title for the file picker
238 * If non-null this contains an instance of object AutoChosen (see below)
239 * which holds pre-determined data so that the user does not need to be
240 * prompted for a target filename.
241 * @param aReferrerInfo
242 * the referrerInfo object to use, or null if no referrer should be sent.
243 * @param aCookieJarSettings
244 * the cookieJarSettings object to use. This will be used for the channel
246 * @param aInitiatingDocument [optional]
247 * The document from which the save was initiated.
248 * If this is omitted then aIsContentWindowPrivate has to be provided.
249 * @param aSkipPrompt [optional]
250 * If set to true, we will attempt to save the file to the
251 * default downloads folder without prompting.
252 * @param aCacheKey [optional]
253 * If set will be passed to saveURI. See nsIWebBrowserPersist for
255 * @param aIsContentWindowPrivate [optional]
256 * This parameter is provided when the aInitiatingDocument is not a
257 * real document object. Stores whether aInitiatingDocument.defaultView
258 * was private or not.
259 * @param aPrincipal [optional]
260 * This parameter is provided when neither aDocument nor
261 * aInitiatingDocument is provided. Used to determine what level of
262 * privilege to load the URI with.
264 function internalSave(
279 aIsContentWindowPrivate,
282 if (aSkipPrompt == undefined) {
286 if (aCacheKey == undefined) {
290 // Note: aDocument == null when this code is used by save-link-as...
291 var saveMode = GetSaveModeForContentType(aContentType, aDocument);
293 var file, sourceURI, saveAsType;
294 let contentPolicyType = Ci.nsIContentPolicy.TYPE_SAVEAS_DOWNLOAD;
295 // Find the URI object for aURL and the FileName/Extension to use when saving.
296 // FileName/Extension will be ignored if aChosenData supplied.
298 file = aChosenData.file;
299 sourceURI = aChosenData.uri;
300 saveAsType = kSaveAsType_Complete;
306 charset = aDocument.characterSet;
308 var fileInfo = new FileInfo(aDefaultFileName);
317 sourceURI = fileInfo.uri;
319 if (aContentType && aContentType.startsWith("image/")) {
320 contentPolicyType = Ci.nsIContentPolicy.TYPE_IMAGE;
323 fpTitleKey: aFilePickerTitleKey,
325 contentType: aContentType,
327 saveAsType: kSaveAsType_Complete,
331 // Find a URI to use for determining last-downloaded-to directory
332 let relatedURI = aReferrerInfo?.originalReferrer || sourceURI;
334 promiseTargetFile(fpParams, aSkipPrompt, relatedURI)
335 .then(aDialogAccepted => {
336 if (!aDialogAccepted) {
340 saveAsType = fpParams.saveAsType;
341 file = fpParams.file;
345 .catch(Cu.reportError);
348 function continueSave() {
349 // XXX We depend on the following holding true in appendFiltersForContentType():
350 // If we should save as a complete page, the saveAsType is kSaveAsType_Complete.
351 // If we should save as text, the saveAsType is kSaveAsType_Text.
352 var useSaveDocument =
354 ((saveMode & SAVEMODE_COMPLETE_DOM &&
355 saveAsType == kSaveAsType_Complete) ||
356 (saveMode & SAVEMODE_COMPLETE_TEXT && saveAsType == kSaveAsType_Text));
357 // If we're saving a document, and are saving either in complete mode or
358 // as converted text, pass the document to the web browser persist component.
359 // If we're just saving the HTML (second option in the list), send only the URI.
361 let isPrivate = aIsContentWindowPrivate;
362 if (isPrivate === undefined) {
364 aInitiatingDocument.nodeType == 9 /* DOCUMENT_NODE */
365 ? PrivateBrowsingUtils.isContentWindowPrivate(
366 aInitiatingDocument.defaultView
368 : aInitiatingDocument.isPrivate;
371 // We have to cover the cases here where we were either passed an explicit
372 // principal, or a 'real' document (with a nodePrincipal property), or an
373 // nsIWebBrowserPersistDocument which has a principal property.
374 let sourcePrincipal =
376 (aDocument && (aDocument.nodePrincipal || aDocument.principal)) ||
377 (aInitiatingDocument && aInitiatingDocument.nodePrincipal);
379 let sourceOriginalURI = aOriginalURL ? makeURI(aOriginalURL) : null;
385 sourceReferrerInfo: aReferrerInfo,
386 sourceDocument: useSaveDocument ? aDocument : null,
387 targetContentType: saveAsType == kSaveAsType_Text ? "text/plain" : null,
389 sourceCacheKey: aCacheKey,
390 sourcePostData: aDocument ? getPostData(aDocument) : null,
391 bypassCache: aShouldBypassCache,
393 cookieJarSettings: aCookieJarSettings,
397 // Start the actual save process
398 internalPersist(persistArgs);
403 * internalPersist: Creates a 'Persist' object (which will perform the saving
404 * in the background) and then starts it.
406 * @param persistArgs.sourceURI
407 * The nsIURI of the document being saved
408 * @param persistArgs.sourceCacheKey [optional]
409 * If set will be passed to savePrivacyAwareURI
410 * @param persistArgs.sourceDocument [optional]
411 * The document to be saved, or null if not saving a complete document
412 * @param persistArgs.sourceReferrerInfo
413 * Required and used only when persistArgs.sourceDocument is NOT present,
414 * the nsIReferrerInfo of the referrer info to use, or null if no
415 * referrer should be sent.
416 * @param persistArgs.sourcePostData
417 * Required and used only when persistArgs.sourceDocument is NOT present,
418 * represents the POST data to be sent along with the HTTP request, and
419 * must be null if no POST data should be sent.
420 * @param persistArgs.targetFile
421 * The nsIFile of the file to create
422 * @param persistArgs.contentPolicyType
423 * The type of content we're saving. Will be used to determine what
424 * content is accepted, enforce sniffing restrictions, etc.
425 * @param persistArgs.cookieJarSettings [optional]
426 * The nsICookieJarSettings that will be used for the saving channel, or
427 * null that savePrivacyAwareURI will create one based on the current
428 * state of the prefs/permissions
429 * @param persistArgs.targetContentType
430 * Required and used only when persistArgs.sourceDocument is present,
431 * determines the final content type of the saved file, or null to use
432 * the same content type as the source document. Currently only
433 * "text/plain" is meaningful.
434 * @param persistArgs.bypassCache
435 * If true, the document will always be refetched from the server
436 * @param persistArgs.isPrivate
437 * Indicates whether this is taking place in a private browsing context.
439 function internalPersist(persistArgs) {
440 var persist = makeWebBrowserPersist();
442 // Calculate persist flags.
443 const nsIWBP = Ci.nsIWebBrowserPersist;
444 const flags = nsIWBP.PERSIST_FLAGS_REPLACE_EXISTING_FILES;
445 if (persistArgs.bypassCache) {
446 persist.persistFlags = flags | nsIWBP.PERSIST_FLAGS_BYPASS_CACHE;
448 persist.persistFlags = flags | nsIWBP.PERSIST_FLAGS_FROM_CACHE;
451 // Leave it to WebBrowserPersist to discover the encoding type (or lack thereof):
452 persist.persistFlags |= nsIWBP.PERSIST_FLAGS_AUTODETECT_APPLY_CONVERSION;
454 // Find the URI associated with the target file
455 var targetFileURL = makeFileURI(persistArgs.targetFile);
457 // Create download and initiate it (below)
458 var tr = Cc["@mozilla.org/transfer;1"].createInstance(Ci.nsITransfer);
460 persistArgs.sourceURI,
461 persistArgs.sourceOriginalURI,
468 persistArgs.isPrivate,
469 Ci.nsITransfer.DOWNLOAD_ACCEPTABLE,
470 persistArgs.sourceReferrerInfo
472 persist.progressListener = new DownloadListener(window, tr);
474 if (persistArgs.sourceDocument) {
475 // Saving a Document, not a URI:
476 var filesFolder = null;
477 if (persistArgs.targetContentType != "text/plain") {
478 // Create the local directory into which to save associated files.
479 filesFolder = persistArgs.targetFile.clone();
481 var nameWithoutExtension = getFileBaseName(filesFolder.leafName);
482 var filesFolderLeafName = ContentAreaUtils.stringBundle.formatStringFromName(
484 [nameWithoutExtension]
487 filesFolder.leafName = filesFolderLeafName;
490 var encodingFlags = 0;
491 if (persistArgs.targetContentType == "text/plain") {
492 encodingFlags |= nsIWBP.ENCODE_FLAGS_FORMATTED;
493 encodingFlags |= nsIWBP.ENCODE_FLAGS_ABSOLUTE_LINKS;
494 encodingFlags |= nsIWBP.ENCODE_FLAGS_NOFRAMES_CONTENT;
496 encodingFlags |= nsIWBP.ENCODE_FLAGS_ENCODE_BASIC_ENTITIES;
499 const kWrapColumn = 80;
500 persist.saveDocument(
501 persistArgs.sourceDocument,
504 persistArgs.targetContentType,
509 persist.savePrivacyAwareURI(
510 persistArgs.sourceURI,
511 persistArgs.sourcePrincipal,
512 persistArgs.sourceCacheKey,
513 persistArgs.sourceReferrerInfo,
514 persistArgs.cookieJarSettings,
515 persistArgs.sourcePostData,
518 persistArgs.contentPolicyType || Ci.nsIContentPolicy.TYPE_SAVEAS_DOWNLOAD,
519 persistArgs.isPrivate
525 * Structure for holding info about automatically supplied parameters for
526 * internalSave(...). This allows parameters to be supplied so the user does not
527 * need to be prompted for file info.
528 * @param aFileAutoChosen This is an nsIFile object that has been
529 * pre-determined as the filename for the target to save to
530 * @param aUriAutoChosen This is the nsIURI object for the target
532 function AutoChosen(aFileAutoChosen, aUriAutoChosen) {
533 this.file = aFileAutoChosen;
534 this.uri = aUriAutoChosen;
538 * Structure for holding info about a URL and the target filename it should be
539 * saved to. This structure is populated by initFileInfo(...).
540 * @param aSuggestedFileName This is used by initFileInfo(...) when it
541 * cannot 'discover' the filename from the url
542 * @param aFileName The target filename
543 * @param aFileBaseName The filename without the file extension
544 * @param aFileExt The extension of the filename
545 * @param aUri An nsIURI object for the url that is being saved
554 this.suggestedFileName = aSuggestedFileName;
555 this.fileName = aFileName;
556 this.fileBaseName = aFileBaseName;
557 this.fileExt = aFileExt;
562 * Determine what the 'default' filename string is, its file extension and the
563 * filename without the extension. This filename is used when prompting the user
564 * for confirmation in the file picker dialog.
565 * @param aFI A FileInfo structure into which we'll put the results of this method.
566 * @param aURL The String representation of the URL of the document being saved
567 * @param aURLCharset The charset of aURL.
568 * @param aDocument The document to be saved
569 * @param aContentType The content type we're saving, if it could be
570 * determined by the caller.
571 * @param aContentDisposition The content-disposition header for the object
572 * we're saving, if it could be determined by the caller.
574 function initFileInfo(
584 // Get an nsIURI object from aURL if possible:
586 aFI.uri = makeURI(aURL, aURLCharset);
587 // Assuming nsiUri is valid, calling QueryInterface(...) on it will
588 // populate extra object fields (eg filename and file extension).
589 uriExt = aFI.uri.QueryInterface(Ci.nsIURL).fileExtension;
592 // Get the default filename:
593 let fileName = getDefaultFileName(
594 aFI.suggestedFileName || aFI.fileName,
600 let mimeService = this.getMIMEService();
601 aFI.fileName = mimeService.validateFileNameForSaving(
604 mimeService.VALIDATE_FORCE_APPEND_EXTENSION
607 // If uriExt is blank, consider: aFI.suggestedFileName is supplied if
608 // saveURL(...) was the original caller (hence both aContentType and
609 // aDocument are blank). If they were saving a link to a website then make
610 // the extension .htm .
615 /^http(s?):\/\//i.test(aURL)
618 aFI.fileBaseName = aFI.fileName;
620 let idx = aFI.fileName.lastIndexOf(".");
622 idx >= 0 ? aFI.fileName.substring(0, idx) : aFI.fileName;
623 aFI.fileExt = idx >= 0 ? aFI.fileName.substring(idx + 1) : null;
629 * Given the Filepicker Parameters (aFpP), show the file picker dialog,
630 * prompting the user to confirm (or change) the fileName.
632 * A structure (see definition in internalSave(...) method)
633 * containing all the data used within this method.
635 * If true, attempt to save the file automatically to the user's default
636 * download directory, thus skipping the explicit prompt for a file name,
637 * but only if the associated preference is set.
638 * If false, don't save the file automatically to the user's
639 * default download directory, even if the associated preference
640 * is set, but ask for the target explicitly.
642 * An nsIURI associated with the download. The last used
643 * directory of the picker is retrieved from/stored in the
644 * Content Pref Service using this URI.
646 * @resolve a boolean. When true, it indicates that the file picker dialog
649 function promiseTargetFile(
651 /* optional */ aSkipPrompt,
652 /* optional */ aRelatedURI
654 return (async function() {
655 let downloadLastDir = new DownloadLastDir(window);
656 let prefBranch = Services.prefs.getBranch("browser.download.");
657 let useDownloadDir = prefBranch.getBoolPref("useDownloadDir");
660 useDownloadDir = false;
663 // Default to the user's default downloads directory configured
664 // through download prefs.
665 let dirPath = await Downloads.getPreferredDownloadsDirectory();
666 let dirExists = await IOUtils.exists(dirPath);
667 let dir = new FileUtils.File(dirPath);
669 if (useDownloadDir && dirExists) {
670 dir.append(aFpP.fileInfo.fileName);
671 aFpP.file = uniqueFile(dir);
675 // We must prompt for the file name explicitly.
676 // If we must prompt because we were asked to...
677 let file = await new Promise(resolve => {
678 if (useDownloadDir) {
679 // Keep async behavior in both branches
680 Services.tm.dispatchToMainThread(function() {
684 downloadLastDir.getFileAsync(aRelatedURI, function getFileAsyncCB(
691 if (file && (await IOUtils.exists(file.path))) {
697 // Default to desktop.
698 dir = Services.dirsvc.get("Desk", Ci.nsIFile);
701 let fp = makeFilePicker();
702 let titleKey = aFpP.fpTitleKey || "SaveLinkTitle";
705 ContentAreaUtils.stringBundle.GetStringFromName(titleKey),
706 Ci.nsIFilePicker.modeSave
709 fp.displayDirectory = dir;
710 fp.defaultExtension = aFpP.fileInfo.fileExt;
711 fp.defaultString = aFpP.fileInfo.fileName;
712 appendFiltersForContentType(
715 aFpP.fileInfo.fileExt,
719 // The index of the selected filter is only preserved and restored if there's
720 // more than one filter in addition to "All Files".
721 if (aFpP.saveMode != SAVEMODE_FILEONLY) {
722 // eslint-disable-next-line mozilla/use-default-preference-values
724 fp.filterIndex = prefBranch.getIntPref("save_converter_index");
728 let result = await new Promise(resolve => {
729 fp.open(function(aResult) {
733 if (result == Ci.nsIFilePicker.returnCancel || !fp.file) {
737 if (aFpP.saveMode != SAVEMODE_FILEONLY) {
738 prefBranch.setIntPref("save_converter_index", fp.filterIndex);
741 // Do not store the last save directory as a pref inside the private browsing mode
742 downloadLastDir.setFile(aRelatedURI, fp.file.parent);
744 fp.file.leafName = validateFileName(fp.file.leafName);
746 aFpP.saveAsType = fp.filterIndex;
748 aFpP.fileURL = fp.fileURL;
754 // Since we're automatically downloading, we don't get the file picker's
755 // logic to check for existing files, so we need to do that here.
757 // Note - this code is identical to that in
758 // mozilla/toolkit/mozapps/downloads/src/nsHelperAppDlg.js.in
759 // If you are updating this code, update that code too! We can't share code
760 // here since that code is called in a js component.
761 function uniqueFile(aLocalFile) {
762 var collisionCount = 0;
763 while (aLocalFile.exists()) {
765 if (collisionCount == 1) {
766 // Append "(2)" before the last dot in (or at the end of) the filename
767 // special case .ext.gz etc files so we don't wind up with .tar(2).gz
768 if (aLocalFile.leafName.match(/\.[^\.]{1,3}\.(gz|bz2|Z)$/i)) {
769 aLocalFile.leafName = aLocalFile.leafName.replace(
770 /\.[^\.]{1,3}\.(gz|bz2|Z)$/i,
774 aLocalFile.leafName = aLocalFile.leafName.replace(
780 // replace the last (n) in the filename with (n+1)
781 aLocalFile.leafName = aLocalFile.leafName.replace(
783 "$1" + (collisionCount + 1) + ")"
791 * Download a URL using the Downloads API.
794 * the url to download
795 * @param [optional] aFileName
796 * the destination file name, if omitted will be obtained from the url.
797 * @param aInitiatingDocument
798 * The document from which the download was initiated.
800 function DownloadURL(aURL, aFileName, aInitiatingDocument) {
801 // For private browsing, try to get document out of the most recent browser
802 // window, or provide our own if there's no browser window.
803 let isPrivate = aInitiatingDocument.defaultView.docShell.QueryInterface(
805 ).usePrivateBrowsing;
807 let fileInfo = new FileInfo(aFileName);
808 initFileInfo(fileInfo, aURL, null, null, null, null);
810 let filepickerParams = {
812 saveMode: SAVEMODE_FILEONLY,
816 let accepted = await promiseTargetFile(
825 let file = filepickerParams.file;
826 let download = await Downloads.createDownload({
827 source: { url: aURL, isPrivate },
828 target: { path: file.path, partFilePath: file.path + ".part" },
830 download.tryToKeepPartialData = true;
832 // Ignore errors because failures are reported through the download list.
833 download.start().catch(() => {});
835 // Add the download to the list, allowing it to be managed.
836 let list = await Downloads.getList(Downloads.ALL);
838 })().catch(Cu.reportError);
841 // We have no DOM, and can only save the URL as is.
842 const SAVEMODE_FILEONLY = 0x00;
843 XPCOMUtils.defineConstant(this, "SAVEMODE_FILEONLY", SAVEMODE_FILEONLY);
844 // We have a DOM and can save as complete.
845 const SAVEMODE_COMPLETE_DOM = 0x01;
846 XPCOMUtils.defineConstant(this, "SAVEMODE_COMPLETE_DOM", SAVEMODE_COMPLETE_DOM);
847 // We have a DOM which we can serialize as text.
848 const SAVEMODE_COMPLETE_TEXT = 0x02;
849 XPCOMUtils.defineConstant(
851 "SAVEMODE_COMPLETE_TEXT",
852 SAVEMODE_COMPLETE_TEXT
855 // If we are able to save a complete DOM, the 'save as complete' filter
856 // must be the first filter appended. The 'save page only' counterpart
857 // must be the second filter appended. And the 'save as complete text'
858 // filter must be the third filter appended.
859 function appendFiltersForContentType(
865 // The bundle name for saving only a specific content type.
867 // The corresponding filter string for a specific content type.
870 // Every case where GetSaveModeForContentType can return non-FILEONLY
871 // modes must be handled here.
872 if (aSaveMode != SAVEMODE_FILEONLY) {
873 switch (aContentType) {
875 bundleName = "WebPageHTMLOnlyFilter";
876 filterString = "*.htm; *.html";
879 case "application/xhtml+xml":
880 bundleName = "WebPageXHTMLOnlyFilter";
881 filterString = "*.xht; *.xhtml";
884 case "image/svg+xml":
885 bundleName = "WebPageSVGOnlyFilter";
886 filterString = "*.svg; *.svgz";
890 case "application/xml":
891 bundleName = "WebPageXMLOnlyFilter";
892 filterString = "*.xml";
898 if (aSaveMode != SAVEMODE_FILEONLY) {
899 throw new Error(`Invalid save mode for type '${aContentType}'`);
902 var mimeInfo = getMIMEInfoForType(aContentType, aFileExtension);
905 for (var extension of mimeInfo.getFileExtensions()) {
908 } // If adding more than one extension,
909 // separate by semi-colon
910 extString += "*." + extension;
914 aFilePicker.appendFilter(mimeInfo.description, extString);
919 if (aSaveMode & SAVEMODE_COMPLETE_DOM) {
920 aFilePicker.appendFilter(
921 ContentAreaUtils.stringBundle.GetStringFromName("WebPageCompleteFilter"),
924 // We should always offer a choice to save document only if
925 // we allow saving as complete.
926 aFilePicker.appendFilter(
927 ContentAreaUtils.stringBundle.GetStringFromName(bundleName),
932 if (aSaveMode & SAVEMODE_COMPLETE_TEXT) {
933 aFilePicker.appendFilters(Ci.nsIFilePicker.filterText);
936 // Always append the all files (*) filter
937 aFilePicker.appendFilters(Ci.nsIFilePicker.filterAll);
940 function getPostData(aDocument) {
941 if (aDocument instanceof Ci.nsIWebBrowserPersistDocument) {
942 return aDocument.postData;
945 // Find the session history entry corresponding to the given document. In
946 // the current implementation, nsIWebPageDescriptor.currentDescriptor always
947 // returns a session history entry.
948 let sessionHistoryEntry = aDocument.defaultView.docShell
949 .QueryInterface(Ci.nsIWebPageDescriptor)
950 .currentDescriptor.QueryInterface(Ci.nsISHEntry);
951 return sessionHistoryEntry.postData;
956 function makeWebBrowserPersist() {
957 const persistContractID =
958 "@mozilla.org/embedding/browser/nsWebBrowserPersist;1";
959 const persistIID = Ci.nsIWebBrowserPersist;
960 return Cc[persistContractID].createInstance(persistIID);
963 function makeURI(aURL, aOriginCharset, aBaseURI) {
964 return Services.io.newURI(aURL, aOriginCharset, aBaseURI);
967 function makeFileURI(aFile) {
968 return Services.io.newFileURI(aFile);
971 function makeFilePicker() {
972 const fpContractID = "@mozilla.org/filepicker;1";
973 const fpIID = Ci.nsIFilePicker;
974 return Cc[fpContractID].createInstance(fpIID);
977 function getMIMEService() {
978 const mimeSvcContractID = "@mozilla.org/mime;1";
979 const mimeSvcIID = Ci.nsIMIMEService;
980 const mimeSvc = Cc[mimeSvcContractID].getService(mimeSvcIID);
984 // Given aFileName, find the fileName without the extension on the end.
985 function getFileBaseName(aFileName) {
986 // Remove the file extension from aFileName:
987 return aFileName.replace(/\.[^.]*$/, "");
990 function getMIMETypeForURI(aURI) {
992 return getMIMEService().getTypeFromURI(aURI);
997 function getMIMEInfoForType(aMIMEType, aExtension) {
998 if (aMIMEType || aExtension) {
1000 return getMIMEService().getFromTypeAndExtension(aMIMEType, aExtension);
1006 function getDefaultFileName(
1012 // 1) look for a filename in the content-disposition header, if any
1013 if (aContentDisposition) {
1014 const mhpContractID = "@mozilla.org/network/mime-hdrparam;1";
1015 const mhpIID = Ci.nsIMIMEHeaderParam;
1016 const mhp = Cc[mhpContractID].getService(mhpIID);
1017 var dummy = { value: null }; // Need an out param...
1018 var charset = getCharsetforSave(aDocument);
1020 var fileName = null;
1022 fileName = mhp.getParameter(
1023 aContentDisposition,
1031 fileName = mhp.getParameter(
1032 aContentDisposition,
1041 return Services.textToSubURI.unEscapeURIForUI(
1043 /* dontEscape = */ true
1049 if (aDocument && aDocument.title && aDocument.title.trim()) {
1050 // If the document looks like HTML or XML, try to use its original title.
1051 let contentType = aDocument.contentType;
1053 contentType == "application/xhtml+xml" ||
1054 contentType == "application/xml" ||
1055 contentType == "image/svg+xml" ||
1056 contentType == "text/html" ||
1057 contentType == "text/xml"
1059 // 2) Use the document title
1060 return aDocument.title;
1065 var url = aURI.QueryInterface(Ci.nsIURL);
1066 if (url.fileName != "") {
1067 // 3) Use the actual file name, if present
1068 return Services.textToSubURI.unEscapeURIForUI(
1070 /* dontEscape = */ true
1074 // This is something like a data: and so forth URI... no filename here.
1077 // Don't use the title if it's from a data URI
1078 if (docTitle && aURI?.scheme != "data") {
1079 // 4) Use the document title
1083 if (aDefaultFileName) {
1084 // 5) Use the caller-provided name, if any
1085 return aDefaultFileName;
1094 // Some files have no information at all, like Javascript generated pages
1100 // This is only used after the user has entered a filename.
1101 function validateFileName(aFileName) {
1102 let processed = DownloadPaths.sanitize(aFileName) || "_";
1103 if (AppConstants.platform == "android") {
1104 // If a large part of the filename has been sanitized, then we
1105 // will use a default filename instead
1106 if (processed.replace(/_/g, "").length <= processed.length / 2) {
1107 // We purposefully do not use a localized default filename,
1108 // which we could have done using
1109 // ContentAreaUtils.stringBundle.GetStringFromName("DefaultSaveFileName")
1110 // since it may contain invalid characters.
1111 var original = processed;
1112 processed = "download";
1114 // Preserve a suffix, if there is one
1115 if (original.includes(".")) {
1116 var suffix = original.split(".").slice(-1)[0];
1117 if (suffix && !suffix.includes("_")) {
1118 processed += "." + suffix;
1126 function GetSaveModeForContentType(aContentType, aDocument) {
1127 // We can only save a complete page if we have a loaded document,
1129 return SAVEMODE_FILEONLY;
1132 // Find the possible save modes using the provided content type
1133 var saveMode = SAVEMODE_FILEONLY;
1134 switch (aContentType) {
1136 case "application/xhtml+xml":
1137 case "image/svg+xml":
1138 saveMode |= SAVEMODE_COMPLETE_TEXT;
1141 case "application/xml":
1142 saveMode |= SAVEMODE_COMPLETE_DOM;
1149 function getCharsetforSave(aDocument) {
1151 return aDocument.characterSet;
1154 if (document.commandDispatcher.focusedWindow) {
1155 return document.commandDispatcher.focusedWindow.document.characterSet;
1158 return window.content.document.characterSet;
1162 * Open a URL from chrome, determining if we can handle it internally or need to
1163 * launch an external application to handle it.
1164 * @param aURL The URL to be opened
1166 * WARNING: Please note that openURL() does not perform any content security checks!!!
1168 function openURL(aURL) {
1169 var uri = aURL instanceof Ci.nsIURI ? aURL : makeURI(aURL);
1171 var protocolSvc = Cc[
1172 "@mozilla.org/uriloader/external-protocol-service;1"
1173 ].getService(Ci.nsIExternalProtocolService);
1175 let recentWindow = Services.wm.getMostRecentWindow("navigator:browser");
1177 if (!protocolSvc.isExposedProtocol(uri.scheme)) {
1178 // If we're not a browser, use the external protocol service to load the URI.
1179 protocolSvc.loadURI(uri, recentWindow?.document.contentPrincipal);
1182 recentWindow.openWebLinkIn(uri.spec, "tab", {
1183 triggeringPrincipal: recentWindow.document.contentPrincipal,
1188 var loadgroup = Cc["@mozilla.org/network/load-group;1"].createInstance(
1191 var appstartup = Services.startup;
1193 var loadListener = {
1194 onStartRequest: function ll_start(aRequest) {
1195 appstartup.enterLastWindowClosingSurvivalArea();
1197 onStopRequest: function ll_stop(aRequest, aStatusCode) {
1198 appstartup.exitLastWindowClosingSurvivalArea();
1200 QueryInterface: ChromeUtils.generateQI([
1201 "nsIRequestObserver",
1202 "nsISupportsWeakReference",
1205 loadgroup.groupObserver = loadListener;
1208 doContent(ctype, preferred, request, handler) {
1211 isPreferred(ctype, desired) {
1214 canHandleContent(ctype, preferred, desired) {
1218 parentContentListener: null,
1220 if (iid.equals(Ci.nsIURIContentListener)) {
1223 if (iid.equals(Ci.nsILoadGroup)) {
1226 throw Components.Exception("", Cr.NS_ERROR_NO_INTERFACE);
1230 var channel = NetUtil.newChannel({
1232 loadUsingSystemPrincipal: true,
1236 channel.channelIsForDownload = true;
1239 var uriLoader = Cc["@mozilla.org/uriloader;1"].getService(Ci.nsIURILoader);
1242 Ci.nsIURILoader.IS_CONTENT_PREFERRED,