Bug 1796551 [wpt PR 36570] - WebKit export of https://bugs.webkit.org/show_bug.cgi...
[gecko.git] / toolkit / content / contentAreaUtils.js
blobfafc2e92cf76d8aabcffae6ae7a5031da8ceaa92
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"
7 );
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",
17 });
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",
24 });
26 var ContentAreaUtils = {
27   get stringBundle() {
28     delete this.stringBundle;
29     return (this.stringBundle = Services.strings.createBundle(
30       "chrome://global/locale/contentAreaCommands.properties"
31     ));
32   },
35 function urlSecurityCheck(
36   aURL,
37   aPrincipal,
38   aFlags = Services.scriptSecurityManager
39 ) {
40   if (aURL instanceof Ci.nsIURI) {
41     Services.scriptSecurityManager.checkLoadURIWithPrincipal(
42       aPrincipal,
43       aURL,
44       aFlags
45     );
46   } else {
47     Services.scriptSecurityManager.checkLoadURIStrWithPrincipal(
48       aPrincipal,
49       aURL,
50       aFlags
51     );
52   }
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
69 //   the above method.
70 // - A linked document using Save Link As...
71 // - A linked document using Alt-click Save Link As...
73 function saveURL(
74   aURL,
75   aOriginalURL,
76   aFileName,
77   aFilePickerTitleKey,
78   aShouldBypassCache,
79   aSkipPrompt,
80   aReferrerInfo,
81   aCookieJarSettings,
82   aSourceDocument,
83   aIsContentWindowPrivate,
84   aPrincipal
85 ) {
86   internalSave(
87     aURL,
88     aOriginalURL,
89     null,
90     aFileName,
91     null,
92     null,
93     aShouldBypassCache,
94     aFilePickerTitleKey,
95     null,
96     aReferrerInfo,
97     aCookieJarSettings,
98     aSourceDocument,
99     aSkipPrompt,
100     null,
101     aIsContentWindowPrivate,
102     aPrincipal
103   );
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) {
109   if (!aBrowser) {
110     throw new Error("Must have a browser when calling saveBrowser");
111   }
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");
117     return;
118   }
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!");
124       }
126       internalSave(
127         document.documentURI,
128         null, // originalURL
129         document,
130         null, // file name
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,
138         document,
139         aSkipPrompt,
140         document.cacheKey
141       );
142     },
143     onError(status) {
144       throw new Components.Exception(
145         "saveBrowser failed asynchronously in startPersistence",
146         status,
147         stack
148       );
149     },
150   });
153 function DownloadListener(win, transfer) {
154   function makeClosure(name) {
155     return function() {
156       transfer[name].apply(transfer, arguments);
157     };
158   }
160   this.window = win;
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);
166     }
167   }
170 DownloadListener.prototype = {
171   QueryInterface: ChromeUtils.generateQI([
172     "nsIInterfaceRequestor",
173     "nsIWebProgressListener",
174     "nsIWebProgressListener2",
175   ]),
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(
180         Ci.nsIPromptFactory
181       );
182       return ww.getPrompt(this.window, aIID);
183     }
185     throw Components.Exception("", Cr.NS_ERROR_NO_INTERFACE);
186   },
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.
219  * @param aURL
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.
224  * @param aDocument
225  *        The document to be saved
226  * @param aDefaultFileName
227  *        The caller-provided suggested filename if we don't
228  *        find a better one
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
237  * @param aChosenData
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
245  *        used to save.
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
254  *        allowed values.
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.
263  */
264 function internalSave(
265   aURL,
266   aOriginalURL,
267   aDocument,
268   aDefaultFileName,
269   aContentDisposition,
270   aContentType,
271   aShouldBypassCache,
272   aFilePickerTitleKey,
273   aChosenData,
274   aReferrerInfo,
275   aCookieJarSettings,
276   aInitiatingDocument,
277   aSkipPrompt,
278   aCacheKey,
279   aIsContentWindowPrivate,
280   aPrincipal
281 ) {
282   if (aSkipPrompt == undefined) {
283     aSkipPrompt = false;
284   }
286   if (aCacheKey == undefined) {
287     aCacheKey = 0;
288   }
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.
297   if (aChosenData) {
298     file = aChosenData.file;
299     sourceURI = aChosenData.uri;
300     saveAsType = kSaveAsType_Complete;
302     continueSave();
303   } else {
304     var charset = null;
305     if (aDocument) {
306       charset = aDocument.characterSet;
307     }
308     var fileInfo = new FileInfo(aDefaultFileName);
309     initFileInfo(
310       fileInfo,
311       aURL,
312       charset,
313       aDocument,
314       aContentType,
315       aContentDisposition
316     );
317     sourceURI = fileInfo.uri;
319     if (aContentType && aContentType.startsWith("image/")) {
320       contentPolicyType = Ci.nsIContentPolicy.TYPE_IMAGE;
321     }
322     var fpParams = {
323       fpTitleKey: aFilePickerTitleKey,
324       fileInfo,
325       contentType: aContentType,
326       saveMode,
327       saveAsType: kSaveAsType_Complete,
328       file,
329     };
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) {
337           return;
338         }
340         saveAsType = fpParams.saveAsType;
341         file = fpParams.file;
343         continueSave();
344       })
345       .catch(Cu.reportError);
346   }
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 =
353       aDocument &&
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) {
363       isPrivate =
364         aInitiatingDocument.nodeType == 9 /* DOCUMENT_NODE */
365           ? PrivateBrowsingUtils.isContentWindowPrivate(
366               aInitiatingDocument.defaultView
367             )
368           : aInitiatingDocument.isPrivate;
369     }
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 =
375       aPrincipal ||
376       (aDocument && (aDocument.nodePrincipal || aDocument.principal)) ||
377       (aInitiatingDocument && aInitiatingDocument.nodePrincipal);
379     let sourceOriginalURI = aOriginalURL ? makeURI(aOriginalURL) : null;
381     var persistArgs = {
382       sourceURI,
383       sourceOriginalURI,
384       sourcePrincipal,
385       sourceReferrerInfo: aReferrerInfo,
386       sourceDocument: useSaveDocument ? aDocument : null,
387       targetContentType: saveAsType == kSaveAsType_Text ? "text/plain" : null,
388       targetFile: file,
389       sourceCacheKey: aCacheKey,
390       sourcePostData: aDocument ? getPostData(aDocument) : null,
391       bypassCache: aShouldBypassCache,
392       contentPolicyType,
393       cookieJarSettings: aCookieJarSettings,
394       isPrivate,
395     };
397     // Start the actual save process
398     internalPersist(persistArgs);
399   }
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.
438  */
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;
447   } else {
448     persist.persistFlags = flags | nsIWBP.PERSIST_FLAGS_FROM_CACHE;
449   }
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);
459   tr.init(
460     persistArgs.sourceURI,
461     persistArgs.sourceOriginalURI,
462     targetFileURL,
463     "",
464     null,
465     null,
466     null,
467     persist,
468     persistArgs.isPrivate,
469     Ci.nsITransfer.DOWNLOAD_ACCEPTABLE,
470     persistArgs.sourceReferrerInfo
471   );
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(
483         "filesFolder",
484         [nameWithoutExtension]
485       );
487       filesFolder.leafName = filesFolderLeafName;
488     }
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;
495     } else {
496       encodingFlags |= nsIWBP.ENCODE_FLAGS_ENCODE_BASIC_ENTITIES;
497     }
499     const kWrapColumn = 80;
500     persist.saveDocument(
501       persistArgs.sourceDocument,
502       targetFileURL,
503       filesFolder,
504       persistArgs.targetContentType,
505       encodingFlags,
506       kWrapColumn
507     );
508   } else {
509     persist.savePrivacyAwareURI(
510       persistArgs.sourceURI,
511       persistArgs.sourcePrincipal,
512       persistArgs.sourceCacheKey,
513       persistArgs.sourceReferrerInfo,
514       persistArgs.cookieJarSettings,
515       persistArgs.sourcePostData,
516       null,
517       targetFileURL,
518       persistArgs.contentPolicyType || Ci.nsIContentPolicy.TYPE_SAVEAS_DOWNLOAD,
519       persistArgs.isPrivate
520     );
521   }
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
531  */
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
546  */
547 function FileInfo(
548   aSuggestedFileName,
549   aFileName,
550   aFileBaseName,
551   aFileExt,
552   aUri
553 ) {
554   this.suggestedFileName = aSuggestedFileName;
555   this.fileName = aFileName;
556   this.fileBaseName = aFileBaseName;
557   this.fileExt = aFileExt;
558   this.uri = aUri;
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.
573  */
574 function initFileInfo(
575   aFI,
576   aURL,
577   aURLCharset,
578   aDocument,
579   aContentType,
580   aContentDisposition
581 ) {
582   try {
583     let uriExt = null;
584     // Get an nsIURI object from aURL if possible:
585     try {
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;
590     } catch (e) {}
592     // Get the default filename:
593     let fileName = getDefaultFileName(
594       aFI.suggestedFileName || aFI.fileName,
595       aFI.uri,
596       aDocument,
597       aContentDisposition
598     );
600     let mimeService = this.getMIMEService();
601     aFI.fileName = mimeService.validateFileNameForSaving(
602       fileName,
603       aContentType,
604       mimeService.VALIDATE_FORCE_APPEND_EXTENSION
605     );
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 .
611     if (
612       !uriExt &&
613       !aDocument &&
614       !aContentType &&
615       /^http(s?):\/\//i.test(aURL)
616     ) {
617       aFI.fileExt = "htm";
618       aFI.fileBaseName = aFI.fileName;
619     } else {
620       let idx = aFI.fileName.lastIndexOf(".");
621       aFI.fileBaseName =
622         idx >= 0 ? aFI.fileName.substring(0, idx) : aFI.fileName;
623       aFI.fileExt = idx >= 0 ? aFI.fileName.substring(idx + 1) : null;
624     }
625   } catch (e) {}
629  * Given the Filepicker Parameters (aFpP), show the file picker dialog,
630  * prompting the user to confirm (or change) the fileName.
631  * @param aFpP
632  *        A structure (see definition in internalSave(...) method)
633  *        containing all the data used within this method.
634  * @param aSkipPrompt
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.
641  * @param aRelatedURI
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.
645  * @return Promise
646  * @resolve a boolean. When true, it indicates that the file picker dialog
647  *          is accepted.
648  */
649 function promiseTargetFile(
650   aFpP,
651   /* optional */ aSkipPrompt,
652   /* optional */ aRelatedURI
653 ) {
654   return (async function() {
655     let downloadLastDir = new DownloadLastDir(window);
656     let prefBranch = Services.prefs.getBranch("browser.download.");
657     let useDownloadDir = prefBranch.getBoolPref("useDownloadDir");
659     if (!aSkipPrompt) {
660       useDownloadDir = false;
661     }
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);
672       return true;
673     }
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() {
681           resolve(null);
682         });
683       } else {
684         downloadLastDir.getFileAsync(aRelatedURI, function getFileAsyncCB(
685           aFile
686         ) {
687           resolve(aFile);
688         });
689       }
690     });
691     if (file && (await IOUtils.exists(file.path))) {
692       dir = file;
693       dirExists = true;
694     }
696     if (!dirExists) {
697       // Default to desktop.
698       dir = Services.dirsvc.get("Desk", Ci.nsIFile);
699     }
701     let fp = makeFilePicker();
702     let titleKey = aFpP.fpTitleKey || "SaveLinkTitle";
703     fp.init(
704       window,
705       ContentAreaUtils.stringBundle.GetStringFromName(titleKey),
706       Ci.nsIFilePicker.modeSave
707     );
709     fp.displayDirectory = dir;
710     fp.defaultExtension = aFpP.fileInfo.fileExt;
711     fp.defaultString = aFpP.fileInfo.fileName;
712     appendFiltersForContentType(
713       fp,
714       aFpP.contentType,
715       aFpP.fileInfo.fileExt,
716       aFpP.saveMode
717     );
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
723       try {
724         fp.filterIndex = prefBranch.getIntPref("save_converter_index");
725       } catch (e) {}
726     }
728     let result = await new Promise(resolve => {
729       fp.open(function(aResult) {
730         resolve(aResult);
731       });
732     });
733     if (result == Ci.nsIFilePicker.returnCancel || !fp.file) {
734       return false;
735     }
737     if (aFpP.saveMode != SAVEMODE_FILEONLY) {
738       prefBranch.setIntPref("save_converter_index", fp.filterIndex);
739     }
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;
747     aFpP.file = fp.file;
748     aFpP.fileURL = fp.fileURL;
750     return true;
751   })();
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()) {
764     collisionCount++;
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,
771           "(2)$&"
772         );
773       } else {
774         aLocalFile.leafName = aLocalFile.leafName.replace(
775           /(\.[^\.]*)?$/,
776           "(2)$&"
777         );
778       }
779     } else {
780       // replace the last (n) in the filename with (n+1)
781       aLocalFile.leafName = aLocalFile.leafName.replace(
782         /^(.*\()\d+\)/,
783         "$1" + (collisionCount + 1) + ")"
784       );
785     }
786   }
787   return aLocalFile;
791  * Download a URL using the Downloads API.
793  * @param aURL
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.
799  */
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(
804     Ci.nsILoadContext
805   ).usePrivateBrowsing;
807   let fileInfo = new FileInfo(aFileName);
808   initFileInfo(fileInfo, aURL, null, null, null, null);
810   let filepickerParams = {
811     fileInfo,
812     saveMode: SAVEMODE_FILEONLY,
813   };
815   (async function() {
816     let accepted = await promiseTargetFile(
817       filepickerParams,
818       true,
819       fileInfo.uri
820     );
821     if (!accepted) {
822       return;
823     }
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" },
829     });
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);
837     list.add(download);
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(
850   this,
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(
860   aFilePicker,
861   aContentType,
862   aFileExtension,
863   aSaveMode
864 ) {
865   // The bundle name for saving only a specific content type.
866   var bundleName;
867   // The corresponding filter string for a specific content type.
868   var filterString;
870   // Every case where GetSaveModeForContentType can return non-FILEONLY
871   // modes must be handled here.
872   if (aSaveMode != SAVEMODE_FILEONLY) {
873     switch (aContentType) {
874       case "text/html":
875         bundleName = "WebPageHTMLOnlyFilter";
876         filterString = "*.htm; *.html";
877         break;
879       case "application/xhtml+xml":
880         bundleName = "WebPageXHTMLOnlyFilter";
881         filterString = "*.xht; *.xhtml";
882         break;
884       case "image/svg+xml":
885         bundleName = "WebPageSVGOnlyFilter";
886         filterString = "*.svg; *.svgz";
887         break;
889       case "text/xml":
890       case "application/xml":
891         bundleName = "WebPageXMLOnlyFilter";
892         filterString = "*.xml";
893         break;
894     }
895   }
897   if (!bundleName) {
898     if (aSaveMode != SAVEMODE_FILEONLY) {
899       throw new Error(`Invalid save mode for type '${aContentType}'`);
900     }
902     var mimeInfo = getMIMEInfoForType(aContentType, aFileExtension);
903     if (mimeInfo) {
904       var extString = "";
905       for (var extension of mimeInfo.getFileExtensions()) {
906         if (extString) {
907           extString += "; ";
908         } // If adding more than one extension,
909         // separate by semi-colon
910         extString += "*." + extension;
911       }
913       if (extString) {
914         aFilePicker.appendFilter(mimeInfo.description, extString);
915       }
916     }
917   }
919   if (aSaveMode & SAVEMODE_COMPLETE_DOM) {
920     aFilePicker.appendFilter(
921       ContentAreaUtils.stringBundle.GetStringFromName("WebPageCompleteFilter"),
922       filterString
923     );
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),
928       filterString
929     );
930   }
932   if (aSaveMode & SAVEMODE_COMPLETE_TEXT) {
933     aFilePicker.appendFilters(Ci.nsIFilePicker.filterText);
934   }
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;
943   }
944   try {
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;
952   } catch (e) {}
953   return null;
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);
981   return mimeSvc;
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) {
991   try {
992     return getMIMEService().getTypeFromURI(aURI);
993   } catch (e) {}
994   return null;
997 function getMIMEInfoForType(aMIMEType, aExtension) {
998   if (aMIMEType || aExtension) {
999     try {
1000       return getMIMEService().getFromTypeAndExtension(aMIMEType, aExtension);
1001     } catch (e) {}
1002   }
1003   return null;
1006 function getDefaultFileName(
1007   aDefaultFileName,
1008   aURI,
1009   aDocument,
1010   aContentDisposition
1011 ) {
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;
1021     try {
1022       fileName = mhp.getParameter(
1023         aContentDisposition,
1024         "filename",
1025         charset,
1026         true,
1027         dummy
1028       );
1029     } catch (e) {
1030       try {
1031         fileName = mhp.getParameter(
1032           aContentDisposition,
1033           "name",
1034           charset,
1035           true,
1036           dummy
1037         );
1038       } catch (e) {}
1039     }
1040     if (fileName) {
1041       return Services.textToSubURI.unEscapeURIForUI(
1042         fileName,
1043         /* dontEscape = */ true
1044       );
1045     }
1046   }
1048   let docTitle;
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;
1052     if (
1053       contentType == "application/xhtml+xml" ||
1054       contentType == "application/xml" ||
1055       contentType == "image/svg+xml" ||
1056       contentType == "text/html" ||
1057       contentType == "text/xml"
1058     ) {
1059       // 2) Use the document title
1060       return aDocument.title;
1061     }
1062   }
1064   try {
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(
1069         url.fileName,
1070         /* dontEscape = */ true
1071       );
1072     }
1073   } catch (e) {
1074     // This is something like a data: and so forth URI... no filename here.
1075   }
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
1080     return docTitle;
1081   }
1083   if (aDefaultFileName) {
1084     // 5) Use the caller-provided name, if any
1085     return aDefaultFileName;
1086   }
1088   try {
1089     if (aURI.host) {
1090       // 6) Use the host.
1091       return aURI.host;
1092     }
1093   } catch (e) {
1094     // Some files have no information at all, like Javascript generated pages
1095   }
1097   return "";
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;
1119         }
1120       }
1121     }
1122   }
1123   return processed;
1126 function GetSaveModeForContentType(aContentType, aDocument) {
1127   // We can only save a complete page if we have a loaded document,
1128   if (!aDocument) {
1129     return SAVEMODE_FILEONLY;
1130   }
1132   // Find the possible save modes using the provided content type
1133   var saveMode = SAVEMODE_FILEONLY;
1134   switch (aContentType) {
1135     case "text/html":
1136     case "application/xhtml+xml":
1137     case "image/svg+xml":
1138       saveMode |= SAVEMODE_COMPLETE_TEXT;
1139     // Fall through
1140     case "text/xml":
1141     case "application/xml":
1142       saveMode |= SAVEMODE_COMPLETE_DOM;
1143       break;
1144   }
1146   return saveMode;
1149 function getCharsetforSave(aDocument) {
1150   if (aDocument) {
1151     return aDocument.characterSet;
1152   }
1154   if (document.commandDispatcher.focusedWindow) {
1155     return document.commandDispatcher.focusedWindow.document.characterSet;
1156   }
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!!!
1167  */
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);
1180   } else {
1181     if (recentWindow) {
1182       recentWindow.openWebLinkIn(uri.spec, "tab", {
1183         triggeringPrincipal: recentWindow.document.contentPrincipal,
1184       });
1185       return;
1186     }
1188     var loadgroup = Cc["@mozilla.org/network/load-group;1"].createInstance(
1189       Ci.nsILoadGroup
1190     );
1191     var appstartup = Services.startup;
1193     var loadListener = {
1194       onStartRequest: function ll_start(aRequest) {
1195         appstartup.enterLastWindowClosingSurvivalArea();
1196       },
1197       onStopRequest: function ll_stop(aRequest, aStatusCode) {
1198         appstartup.exitLastWindowClosingSurvivalArea();
1199       },
1200       QueryInterface: ChromeUtils.generateQI([
1201         "nsIRequestObserver",
1202         "nsISupportsWeakReference",
1203       ]),
1204     };
1205     loadgroup.groupObserver = loadListener;
1207     var uriListener = {
1208       doContent(ctype, preferred, request, handler) {
1209         return false;
1210       },
1211       isPreferred(ctype, desired) {
1212         return false;
1213       },
1214       canHandleContent(ctype, preferred, desired) {
1215         return false;
1216       },
1217       loadCookie: null,
1218       parentContentListener: null,
1219       getInterface(iid) {
1220         if (iid.equals(Ci.nsIURIContentListener)) {
1221           return this;
1222         }
1223         if (iid.equals(Ci.nsILoadGroup)) {
1224           return loadgroup;
1225         }
1226         throw Components.Exception("", Cr.NS_ERROR_NO_INTERFACE);
1227       },
1228     };
1230     var channel = NetUtil.newChannel({
1231       uri,
1232       loadUsingSystemPrincipal: true,
1233     });
1235     if (channel) {
1236       channel.channelIsForDownload = true;
1237     }
1239     var uriLoader = Cc["@mozilla.org/uriloader;1"].getService(Ci.nsIURILoader);
1240     uriLoader.openURI(
1241       channel,
1242       Ci.nsIURILoader.IS_CONTENT_PREFERRED,
1243       uriListener
1244     );
1245   }