no bug - Bumping Firefox l10n changesets r=release a=l10n-bump DONTBUILD CLOSED TREE
[gecko.git] / toolkit / content / contentAreaUtils.js
blob983fd9890d84cf85e0e3048a941e4f8556165e43
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   DownloadLastDir: "resource://gre/modules/DownloadLastDir.sys.mjs",
15   DownloadPaths: "resource://gre/modules/DownloadPaths.sys.mjs",
16   Downloads: "resource://gre/modules/Downloads.sys.mjs",
17   FileUtils: "resource://gre/modules/FileUtils.sys.mjs",
18   NetUtil: "resource://gre/modules/NetUtil.sys.mjs",
19   PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs",
20 });
22 var ContentAreaUtils = {
23   get stringBundle() {
24     delete this.stringBundle;
25     return (this.stringBundle = Services.strings.createBundle(
26       "chrome://global/locale/contentAreaCommands.properties"
27     ));
28   },
31 function urlSecurityCheck(
32   aURL,
33   aPrincipal,
34   aFlags = Services.scriptSecurityManager
35 ) {
36   if (aURL instanceof Ci.nsIURI) {
37     Services.scriptSecurityManager.checkLoadURIWithPrincipal(
38       aPrincipal,
39       aURL,
40       aFlags
41     );
42   } else {
43     Services.scriptSecurityManager.checkLoadURIStrWithPrincipal(
44       aPrincipal,
45       aURL,
46       aFlags
47     );
48   }
51 // Clientele: (Make sure you don't break any of these)
52 //  - File    ->  Save Page/Frame As...
53 //  - Context ->  Save Page/Frame As...
54 //  - Context ->  Save Link As...
55 //  - Alt-Click links in web pages
56 //  - Alt-Click links in the UI
58 // Try saving each of these types:
59 // - A complete webpage using File->Save Page As, and Context->Save Page As
60 // - A webpage as HTML only using the above methods
61 // - A webpage as Text only using the above methods
62 // - An image with an extension (e.g. .jpg) in its file name, using
63 //   Context->Save Image As...
64 // - An image without an extension (e.g. a banner ad on cnn.com) using
65 //   the above method.
66 // - A linked document using Save Link As...
67 // - A linked document using Alt-click Save Link As...
69 function saveURL(
70   aURL,
71   aOriginalURL,
72   aFileName,
73   aFilePickerTitleKey,
74   aShouldBypassCache,
75   aSkipPrompt,
76   aReferrerInfo,
77   aCookieJarSettings,
78   aSourceDocument,
79   aIsContentWindowPrivate,
80   aPrincipal
81 ) {
82   internalSave(
83     aURL,
84     aOriginalURL,
85     null,
86     aFileName,
87     null,
88     null,
89     aShouldBypassCache,
90     aFilePickerTitleKey,
91     null,
92     aReferrerInfo,
93     aCookieJarSettings,
94     aSourceDocument,
95     aSkipPrompt,
96     null,
97     aIsContentWindowPrivate,
98     aPrincipal
99   );
102 // Save the current document inside any browser/frame-like element,
103 // whether in-process or out-of-process.
104 function saveBrowser(aBrowser, aSkipPrompt, aBrowsingContext = null) {
105   if (!aBrowser) {
106     throw new Error("Must have a browser when calling saveBrowser");
107   }
108   let persistable = aBrowser.frameLoader;
109   // PDF.js has its own way to handle saving PDFs since it may need to
110   // generate a new PDF to save modified form data.
111   if (aBrowser.contentPrincipal.spec == "resource://pdf.js/web/viewer.html") {
112     aBrowser.sendMessageToActor("PDFJS:Save", {}, "Pdfjs");
113     return;
114   }
115   let stack = Components.stack.caller;
116   persistable.startPersistence(aBrowsingContext, {
117     onDocumentReady(document) {
118       if (!document || !(document instanceof Ci.nsIWebBrowserPersistDocument)) {
119         throw new Error("Must have an nsIWebBrowserPersistDocument!");
120       }
122       internalSave(
123         document.documentURI,
124         null, // originalURL
125         document,
126         null, // file name
127         document.contentDisposition,
128         document.contentType,
129         false, // bypass cache
130         null, // file picker title key
131         null, // chosen file data
132         document.referrerInfo,
133         document.cookieJarSettings,
134         document,
135         aSkipPrompt,
136         document.cacheKey
137       );
138     },
139     onError(status) {
140       throw new Components.Exception(
141         "saveBrowser failed asynchronously in startPersistence",
142         status,
143         stack
144       );
145     },
146   });
149 function DownloadListener(win, transfer) {
150   function makeClosure(name) {
151     return function () {
152       transfer[name].apply(transfer, arguments);
153     };
154   }
156   this.window = win;
158   // Now... we need to forward all calls to our transfer
159   for (var i in transfer) {
160     if (i != "QueryInterface") {
161       this[i] = makeClosure(i);
162     }
163   }
166 DownloadListener.prototype = {
167   QueryInterface: ChromeUtils.generateQI([
168     "nsIInterfaceRequestor",
169     "nsIWebProgressListener",
170     "nsIWebProgressListener2",
171   ]),
173   getInterface: function dl_gi(aIID) {
174     if (aIID.equals(Ci.nsIAuthPrompt) || aIID.equals(Ci.nsIAuthPrompt2)) {
175       var ww = Cc["@mozilla.org/embedcomp/window-watcher;1"].getService(
176         Ci.nsIPromptFactory
177       );
178       return ww.getPrompt(this.window, aIID);
179     }
181     throw Components.Exception("", Cr.NS_ERROR_NO_INTERFACE);
182   },
185 const kSaveAsType_Complete = 0; // Save document with attached objects.
186 XPCOMUtils.defineConstant(this, "kSaveAsType_Complete", 0);
187 // const kSaveAsType_URL      = 1; // Save document or URL by itself.
188 const kSaveAsType_Text = 2; // Save document, converting to plain text.
189 XPCOMUtils.defineConstant(this, "kSaveAsType_Text", kSaveAsType_Text);
192  * internalSave: Used when saving a document or URL.
194  * If aChosenData is null, this method:
195  *  - Determines a local target filename to use
196  *  - Prompts the user to confirm the destination filename and save mode
197  *    (aContentType affects this)
198  *  - [Note] This process involves the parameters aURL, aReferrerInfo,
199  *    aDocument, aDefaultFileName, aFilePickerTitleKey, and aSkipPrompt.
201  * If aChosenData is non-null, this method:
202  *  - Uses the provided source URI and save file name
203  *  - Saves the document as complete DOM if possible (aDocument present and
204  *    right aContentType)
205  *  - [Note] The parameters aURL, aDefaultFileName, aFilePickerTitleKey, and
206  *    aSkipPrompt are ignored.
208  * In any case, this method:
209  *  - Creates a 'Persist' object (which will perform the saving in the
210  *    background) and then starts it.
211  *  - [Note] This part of the process only involves the parameters aDocument,
212  *    aShouldBypassCache and aReferrerInfo. The source, the save name and the
213  *    save mode are the ones determined previously.
215  * @param aURL
216  *        The String representation of the URL of the document being saved
217  * @param aOriginalURL
218  *        The String representation of the original URL of the document being
219  *        saved. It can useful in case aURL is a blob.
220  * @param aDocument
221  *        The document to be saved
222  * @param aDefaultFileName
223  *        The caller-provided suggested filename if we don't
224  *        find a better one
225  * @param aContentDisposition
226  *        The caller-provided content-disposition header to use.
227  * @param aContentType
228  *        The caller-provided content-type to use
229  * @param aShouldBypassCache
230  *        If true, the document will always be refetched from the server
231  * @param aFilePickerTitleKey
232  *        Alternate title for the file picker
233  * @param aChosenData
234  *        If non-null this contains an instance of object AutoChosen (see below)
235  *        which holds pre-determined data so that the user does not need to be
236  *        prompted for a target filename.
237  * @param aReferrerInfo
238  *        the referrerInfo object to use, or null if no referrer should be sent.
239  * @param aCookieJarSettings
240  *        the cookieJarSettings object to use. This will be used for the channel
241  *        used to save.
242  * @param aInitiatingDocument [optional]
243  *        The document from which the save was initiated.
244  *        If this is omitted then aIsContentWindowPrivate has to be provided.
245  * @param aSkipPrompt [optional]
246  *        If set to true, we will attempt to save the file to the
247  *        default downloads folder without prompting.
248  * @param aCacheKey [optional]
249  *        If set will be passed to saveURI.  See nsIWebBrowserPersist for
250  *        allowed values.
251  * @param aIsContentWindowPrivate [optional]
252  *        This parameter is provided when the aInitiatingDocument is not a
253  *        real document object. Stores whether aInitiatingDocument.defaultView
254  *        was private or not.
255  * @param aPrincipal [optional]
256  *        This parameter is provided when neither aDocument nor
257  *        aInitiatingDocument is provided. Used to determine what level of
258  *        privilege to load the URI with.
259  */
260 function internalSave(
261   aURL,
262   aOriginalURL,
263   aDocument,
264   aDefaultFileName,
265   aContentDisposition,
266   aContentType,
267   aShouldBypassCache,
268   aFilePickerTitleKey,
269   aChosenData,
270   aReferrerInfo,
271   aCookieJarSettings,
272   aInitiatingDocument,
273   aSkipPrompt,
274   aCacheKey,
275   aIsContentWindowPrivate,
276   aPrincipal
277 ) {
278   if (aSkipPrompt == undefined) {
279     aSkipPrompt = false;
280   }
282   if (aCacheKey == undefined) {
283     aCacheKey = 0;
284   }
286   // Note: aDocument == null when this code is used by save-link-as...
287   var saveMode = GetSaveModeForContentType(aContentType, aDocument);
289   var file, sourceURI, saveAsType;
290   let contentPolicyType = Ci.nsIContentPolicy.TYPE_SAVEAS_DOWNLOAD;
291   // Find the URI object for aURL and the FileName/Extension to use when saving.
292   // FileName/Extension will be ignored if aChosenData supplied.
293   if (aChosenData) {
294     file = aChosenData.file;
295     sourceURI = aChosenData.uri;
296     saveAsType = kSaveAsType_Complete;
298     continueSave();
299   } else {
300     var charset = null;
301     if (aDocument) {
302       charset = aDocument.characterSet;
303     }
304     var fileInfo = new FileInfo(aDefaultFileName);
305     initFileInfo(
306       fileInfo,
307       aURL,
308       charset,
309       aDocument,
310       aContentType,
311       aContentDisposition
312     );
313     sourceURI = fileInfo.uri;
315     if (aContentType && aContentType.startsWith("image/")) {
316       contentPolicyType = Ci.nsIContentPolicy.TYPE_IMAGE;
317     }
318     var fpParams = {
319       fpTitleKey: aFilePickerTitleKey,
320       fileInfo,
321       contentType: aContentType,
322       saveMode,
323       saveAsType: kSaveAsType_Complete,
324       file,
325     };
327     // Find a URI to use for determining last-downloaded-to directory
328     let relatedURI =
329       aOriginalURL || aReferrerInfo?.originalReferrer || sourceURI;
331     promiseTargetFile(fpParams, aSkipPrompt, relatedURI)
332       .then(aDialogAccepted => {
333         if (!aDialogAccepted) {
334           return;
335         }
337         saveAsType = fpParams.saveAsType;
338         file = fpParams.file;
340         continueSave();
341       })
342       .catch(console.error);
343   }
345   function continueSave() {
346     // XXX We depend on the following holding true in appendFiltersForContentType():
347     // If we should save as a complete page, the saveAsType is kSaveAsType_Complete.
348     // If we should save as text, the saveAsType is kSaveAsType_Text.
349     var useSaveDocument =
350       aDocument &&
351       ((saveMode & SAVEMODE_COMPLETE_DOM &&
352         saveAsType == kSaveAsType_Complete) ||
353         (saveMode & SAVEMODE_COMPLETE_TEXT && saveAsType == kSaveAsType_Text));
354     // If we're saving a document, and are saving either in complete mode or
355     // as converted text, pass the document to the web browser persist component.
356     // If we're just saving the HTML (second option in the list), send only the URI.
358     let isPrivate = aIsContentWindowPrivate;
359     if (isPrivate === undefined) {
360       isPrivate =
361         aInitiatingDocument.nodeType == 9 /* DOCUMENT_NODE */
362           ? PrivateBrowsingUtils.isContentWindowPrivate(
363               aInitiatingDocument.defaultView
364             )
365           : aInitiatingDocument.isPrivate;
366     }
368     // We have to cover the cases here where we were either passed an explicit
369     // principal, or a 'real' document (with a nodePrincipal property), or an
370     // nsIWebBrowserPersistDocument which has a principal property.
371     let sourcePrincipal =
372       aPrincipal ||
373       (aDocument && (aDocument.nodePrincipal || aDocument.principal)) ||
374       (aInitiatingDocument && aInitiatingDocument.nodePrincipal);
376     let sourceOriginalURI = aOriginalURL ? makeURI(aOriginalURL) : null;
378     var persistArgs = {
379       sourceURI,
380       sourceOriginalURI,
381       sourcePrincipal,
382       sourceReferrerInfo: aReferrerInfo,
383       sourceDocument: useSaveDocument ? aDocument : null,
384       targetContentType: saveAsType == kSaveAsType_Text ? "text/plain" : null,
385       targetFile: file,
386       sourceCacheKey: aCacheKey,
387       sourcePostData: aDocument ? getPostData(aDocument) : null,
388       bypassCache: aShouldBypassCache,
389       contentPolicyType,
390       cookieJarSettings: aCookieJarSettings,
391       isPrivate,
392     };
394     // Start the actual save process
395     internalPersist(persistArgs);
396   }
400  * internalPersist: Creates a 'Persist' object (which will perform the saving
401  *  in the background) and then starts it.
403  * @param persistArgs.sourceURI
404  *        The nsIURI of the document being saved
405  * @param persistArgs.sourceCacheKey [optional]
406  *        If set will be passed to saveURI
407  * @param persistArgs.sourceDocument [optional]
408  *        The document to be saved, or null if not saving a complete document
409  * @param persistArgs.sourceReferrerInfo
410  *        Required and used only when persistArgs.sourceDocument is NOT present,
411  *        the nsIReferrerInfo of the referrer info to use, or null if no
412  *        referrer should be sent.
413  * @param persistArgs.sourcePostData
414  *        Required and used only when persistArgs.sourceDocument is NOT present,
415  *        represents the POST data to be sent along with the HTTP request, and
416  *        must be null if no POST data should be sent.
417  * @param persistArgs.targetFile
418  *        The nsIFile of the file to create
419  * @param persistArgs.contentPolicyType
420  *        The type of content we're saving. Will be used to determine what
421  *        content is accepted, enforce sniffing restrictions, etc.
422  * @param persistArgs.cookieJarSettings [optional]
423  *        The nsICookieJarSettings that will be used for the saving channel, or
424  *        null that saveURI will create one based on the current
425  *        state of the prefs/permissions
426  * @param persistArgs.targetContentType
427  *        Required and used only when persistArgs.sourceDocument is present,
428  *        determines the final content type of the saved file, or null to use
429  *        the same content type as the source document. Currently only
430  *        "text/plain" is meaningful.
431  * @param persistArgs.bypassCache
432  *        If true, the document will always be refetched from the server
433  * @param persistArgs.isPrivate
434  *        Indicates whether this is taking place in a private browsing context.
435  */
436 function internalPersist(persistArgs) {
437   var persist = makeWebBrowserPersist();
439   // Calculate persist flags.
440   const nsIWBP = Ci.nsIWebBrowserPersist;
441   const flags = nsIWBP.PERSIST_FLAGS_REPLACE_EXISTING_FILES;
442   if (persistArgs.bypassCache) {
443     persist.persistFlags = flags | nsIWBP.PERSIST_FLAGS_BYPASS_CACHE;
444   } else {
445     persist.persistFlags = flags | nsIWBP.PERSIST_FLAGS_FROM_CACHE;
446   }
448   // Leave it to WebBrowserPersist to discover the encoding type (or lack thereof):
449   persist.persistFlags |= nsIWBP.PERSIST_FLAGS_AUTODETECT_APPLY_CONVERSION;
451   // Find the URI associated with the target file
452   var targetFileURL = makeFileURI(persistArgs.targetFile);
454   // Create download and initiate it (below)
455   var tr = Cc["@mozilla.org/transfer;1"].createInstance(Ci.nsITransfer);
456   tr.init(
457     persistArgs.sourceURI,
458     persistArgs.sourceOriginalURI,
459     targetFileURL,
460     "",
461     null,
462     null,
463     null,
464     persist,
465     persistArgs.isPrivate,
466     Ci.nsITransfer.DOWNLOAD_ACCEPTABLE,
467     persistArgs.sourceReferrerInfo
468   );
469   persist.progressListener = new DownloadListener(window, tr);
471   if (persistArgs.sourceDocument) {
472     // Saving a Document, not a URI:
473     var filesFolder = null;
474     if (persistArgs.targetContentType != "text/plain") {
475       // Create the local directory into which to save associated files.
476       filesFolder = persistArgs.targetFile.clone();
478       var nameWithoutExtension = getFileBaseName(filesFolder.leafName);
479       var filesFolderLeafName =
480         ContentAreaUtils.stringBundle.formatStringFromName("filesFolder", [
481           nameWithoutExtension,
482         ]);
484       filesFolder.leafName = filesFolderLeafName;
485     }
487     var encodingFlags = 0;
488     if (persistArgs.targetContentType == "text/plain") {
489       encodingFlags |= nsIWBP.ENCODE_FLAGS_FORMATTED;
490       encodingFlags |= nsIWBP.ENCODE_FLAGS_ABSOLUTE_LINKS;
491       encodingFlags |= nsIWBP.ENCODE_FLAGS_NOFRAMES_CONTENT;
492     } else {
493       encodingFlags |= nsIWBP.ENCODE_FLAGS_ENCODE_BASIC_ENTITIES;
494     }
496     const kWrapColumn = 80;
497     persist.saveDocument(
498       persistArgs.sourceDocument,
499       targetFileURL,
500       filesFolder,
501       persistArgs.targetContentType,
502       encodingFlags,
503       kWrapColumn
504     );
505   } else {
506     persist.saveURI(
507       persistArgs.sourceURI,
508       persistArgs.sourcePrincipal,
509       persistArgs.sourceCacheKey,
510       persistArgs.sourceReferrerInfo,
511       persistArgs.cookieJarSettings,
512       persistArgs.sourcePostData,
513       null,
514       targetFileURL,
515       persistArgs.contentPolicyType || Ci.nsIContentPolicy.TYPE_SAVEAS_DOWNLOAD,
516       persistArgs.isPrivate
517     );
518   }
522  * Structure for holding info about automatically supplied parameters for
523  * internalSave(...). This allows parameters to be supplied so the user does not
524  * need to be prompted for file info.
525  * @param aFileAutoChosen This is an nsIFile object that has been
526  *        pre-determined as the filename for the target to save to
527  * @param aUriAutoChosen  This is the nsIURI object for the target
528  */
529 function AutoChosen(aFileAutoChosen, aUriAutoChosen) {
530   this.file = aFileAutoChosen;
531   this.uri = aUriAutoChosen;
535  * Structure for holding info about a URL and the target filename it should be
536  * saved to. This structure is populated by initFileInfo(...).
537  * @param aSuggestedFileName This is used by initFileInfo(...) when it
538  *        cannot 'discover' the filename from the url
539  * @param aFileName The target filename
540  * @param aFileBaseName The filename without the file extension
541  * @param aFileExt The extension of the filename
542  * @param aUri An nsIURI object for the url that is being saved
543  */
544 function FileInfo(
545   aSuggestedFileName,
546   aFileName,
547   aFileBaseName,
548   aFileExt,
549   aUri
550 ) {
551   this.suggestedFileName = aSuggestedFileName;
552   this.fileName = aFileName;
553   this.fileBaseName = aFileBaseName;
554   this.fileExt = aFileExt;
555   this.uri = aUri;
559  * Determine what the 'default' filename string is, its file extension and the
560  * filename without the extension. This filename is used when prompting the user
561  * for confirmation in the file picker dialog.
562  * @param aFI A FileInfo structure into which we'll put the results of this method.
563  * @param aURL The String representation of the URL of the document being saved
564  * @param aURLCharset The charset of aURL.
565  * @param aDocument The document to be saved
566  * @param aContentType The content type we're saving, if it could be
567  *        determined by the caller.
568  * @param aContentDisposition The content-disposition header for the object
569  *        we're saving, if it could be determined by the caller.
570  */
571 function initFileInfo(
572   aFI,
573   aURL,
574   aURLCharset,
575   aDocument,
576   aContentType,
577   aContentDisposition
578 ) {
579   try {
580     let uriExt = null;
581     // Get an nsIURI object from aURL if possible:
582     try {
583       aFI.uri = makeURI(aURL, aURLCharset);
584       // Assuming nsiUri is valid, calling QueryInterface(...) on it will
585       // populate extra object fields (eg filename and file extension).
586       uriExt = aFI.uri.QueryInterface(Ci.nsIURL).fileExtension;
587     } catch (e) {}
589     // Get the default filename:
590     let fileName = getDefaultFileName(
591       aFI.suggestedFileName || aFI.fileName,
592       aFI.uri,
593       aDocument,
594       aContentDisposition
595     );
597     let mimeService = this.getMIMEService();
598     aFI.fileName = mimeService.validateFileNameForSaving(
599       fileName,
600       aContentType,
601       mimeService.VALIDATE_FORCE_APPEND_EXTENSION
602     );
604     // If uriExt is blank, consider: aFI.suggestedFileName is supplied if
605     // saveURL(...) was the original caller (hence both aContentType and
606     // aDocument are blank). If they were saving a link to a website then make
607     // the extension .htm .
608     if (
609       !uriExt &&
610       !aDocument &&
611       !aContentType &&
612       /^http(s?):\/\//i.test(aURL)
613     ) {
614       aFI.fileExt = "htm";
615       aFI.fileBaseName = aFI.fileName;
616     } else {
617       let idx = aFI.fileName.lastIndexOf(".");
618       aFI.fileBaseName =
619         idx >= 0 ? aFI.fileName.substring(0, idx) : aFI.fileName;
620       aFI.fileExt = idx >= 0 ? aFI.fileName.substring(idx + 1) : null;
621     }
622   } catch (e) {}
626  * Given the Filepicker Parameters (aFpP), show the file picker dialog,
627  * prompting the user to confirm (or change) the fileName.
628  * @param aFpP
629  *        A structure (see definition in internalSave(...) method)
630  *        containing all the data used within this method.
631  * @param aSkipPrompt
632  *        If true, attempt to save the file automatically to the user's default
633  *        download directory, thus skipping the explicit prompt for a file name,
634  *        but only if the associated preference is set.
635  *        If false, don't save the file automatically to the user's
636  *        default download directory, even if the associated preference
637  *        is set, but ask for the target explicitly.
638  * @param aRelatedURI
639  *        An nsIURI associated with the download. The last used
640  *        directory of the picker is retrieved from/stored in the
641  *        Content Pref Service using this URI.
642  * @return Promise
643  * @resolve a boolean. When true, it indicates that the file picker dialog
644  *          is accepted.
645  */
646 function promiseTargetFile(
647   aFpP,
648   /* optional */ aSkipPrompt,
649   /* optional */ aRelatedURI
650 ) {
651   return (async function () {
652     let downloadLastDir = new DownloadLastDir(window);
653     let prefBranch = Services.prefs.getBranch("browser.download.");
654     let useDownloadDir = prefBranch.getBoolPref("useDownloadDir");
656     if (!aSkipPrompt) {
657       useDownloadDir = false;
658     }
660     // Default to the user's default downloads directory configured
661     // through download prefs.
662     let dirPath = await Downloads.getPreferredDownloadsDirectory();
663     let dirExists = await IOUtils.exists(dirPath);
664     let dir = new FileUtils.File(dirPath);
666     if (useDownloadDir && dirExists) {
667       dir.append(aFpP.fileInfo.fileName);
668       aFpP.file = uniqueFile(dir);
669       return true;
670     }
672     // We must prompt for the file name explicitly.
673     // If we must prompt because we were asked to...
674     let file = null;
675     if (!useDownloadDir) {
676       file = await downloadLastDir.getFileAsync(aRelatedURI);
677     }
678     if (file && (await IOUtils.exists(file.path))) {
679       dir = file;
680       dirExists = true;
681     }
683     if (!dirExists) {
684       // Default to desktop.
685       dir = Services.dirsvc.get("Desk", Ci.nsIFile);
686     }
688     let fp = makeFilePicker();
689     let titleKey = aFpP.fpTitleKey || "SaveLinkTitle";
690     fp.init(
691       window.browsingContext,
692       ContentAreaUtils.stringBundle.GetStringFromName(titleKey),
693       Ci.nsIFilePicker.modeSave
694     );
696     fp.displayDirectory = dir;
697     fp.defaultExtension = aFpP.fileInfo.fileExt;
698     fp.defaultString = aFpP.fileInfo.fileName;
699     appendFiltersForContentType(
700       fp,
701       aFpP.contentType,
702       aFpP.fileInfo.fileExt,
703       aFpP.saveMode
704     );
706     // The index of the selected filter is only preserved and restored if there's
707     // more than one filter in addition to "All Files".
708     if (aFpP.saveMode != SAVEMODE_FILEONLY) {
709       // eslint-disable-next-line mozilla/use-default-preference-values
710       try {
711         fp.filterIndex = prefBranch.getIntPref("save_converter_index");
712       } catch (e) {}
713     }
715     let result = await new Promise(resolve => {
716       fp.open(function (aResult) {
717         resolve(aResult);
718       });
719     });
720     if (result == Ci.nsIFilePicker.returnCancel || !fp.file) {
721       return false;
722     }
724     if (aFpP.saveMode != SAVEMODE_FILEONLY) {
725       prefBranch.setIntPref("save_converter_index", fp.filterIndex);
726     }
728     // Do not store the last save directory as a pref inside the private browsing mode
729     downloadLastDir.setFile(aRelatedURI, fp.file.parent);
731     aFpP.saveAsType = fp.filterIndex;
732     aFpP.file = fp.file;
733     aFpP.file.leafName = validateFileName(aFpP.file.leafName);
735     return true;
736   })();
739 // Since we're automatically downloading, we don't get the file picker's
740 // logic to check for existing files, so we need to do that here.
742 // Note - this code is identical to that in
743 //   mozilla/toolkit/mozapps/downloads/src/nsHelperAppDlg.js.in
744 // If you are updating this code, update that code too! We can't share code
745 // here since that code is called in a js component.
746 function uniqueFile(aLocalFile) {
747   var collisionCount = 0;
748   while (aLocalFile.exists()) {
749     collisionCount++;
750     if (collisionCount == 1) {
751       // Append "(2)" before the last dot in (or at the end of) the filename
752       // special case .ext.gz etc files so we don't wind up with .tar(2).gz
753       if (aLocalFile.leafName.match(/\.[^\.]{1,3}\.(gz|bz2|Z)$/i)) {
754         aLocalFile.leafName = aLocalFile.leafName.replace(
755           /\.[^\.]{1,3}\.(gz|bz2|Z)$/i,
756           "(2)$&"
757         );
758       } else {
759         aLocalFile.leafName = aLocalFile.leafName.replace(
760           /(\.[^\.]*)?$/,
761           "(2)$&"
762         );
763       }
764     } else {
765       // replace the last (n) in the filename with (n+1)
766       aLocalFile.leafName = aLocalFile.leafName.replace(
767         /^(.*\()\d+\)/,
768         "$1" + (collisionCount + 1) + ")"
769       );
770     }
771   }
772   return aLocalFile;
776  * Download a URL using the Downloads API.
778  * @param aURL
779  *        the url to download
780  * @param [optional] aFileName
781  *        the destination file name, if omitted will be obtained from the url.
782  * @param aInitiatingDocument
783  *        The document from which the download was initiated.
784  */
785 function DownloadURL(aURL, aFileName, aInitiatingDocument) {
786   // For private browsing, try to get document out of the most recent browser
787   // window, or provide our own if there's no browser window.
788   let isPrivate = aInitiatingDocument.defaultView.docShell.QueryInterface(
789     Ci.nsILoadContext
790   ).usePrivateBrowsing;
792   let fileInfo = new FileInfo(aFileName);
793   initFileInfo(fileInfo, aURL, null, null, null, null);
795   let filepickerParams = {
796     fileInfo,
797     saveMode: SAVEMODE_FILEONLY,
798   };
800   (async function () {
801     let accepted = await promiseTargetFile(
802       filepickerParams,
803       true,
804       fileInfo.uri
805     );
806     if (!accepted) {
807       return;
808     }
810     let file = filepickerParams.file;
811     let download = await Downloads.createDownload({
812       source: { url: aURL, isPrivate },
813       target: { path: file.path, partFilePath: file.path + ".part" },
814     });
815     download.tryToKeepPartialData = true;
817     // Ignore errors because failures are reported through the download list.
818     download.start().catch(() => {});
820     // Add the download to the list, allowing it to be managed.
821     let list = await Downloads.getList(Downloads.ALL);
822     list.add(download);
823   })().catch(console.error);
826 // We have no DOM, and can only save the URL as is.
827 const SAVEMODE_FILEONLY = 0x00;
828 XPCOMUtils.defineConstant(this, "SAVEMODE_FILEONLY", SAVEMODE_FILEONLY);
829 // We have a DOM and can save as complete.
830 const SAVEMODE_COMPLETE_DOM = 0x01;
831 XPCOMUtils.defineConstant(this, "SAVEMODE_COMPLETE_DOM", SAVEMODE_COMPLETE_DOM);
832 // We have a DOM which we can serialize as text.
833 const SAVEMODE_COMPLETE_TEXT = 0x02;
834 XPCOMUtils.defineConstant(
835   this,
836   "SAVEMODE_COMPLETE_TEXT",
837   SAVEMODE_COMPLETE_TEXT
840 // If we are able to save a complete DOM, the 'save as complete' filter
841 // must be the first filter appended.  The 'save page only' counterpart
842 // must be the second filter appended.  And the 'save as complete text'
843 // filter must be the third filter appended.
844 function appendFiltersForContentType(
845   aFilePicker,
846   aContentType,
847   aFileExtension,
848   aSaveMode
849 ) {
850   // The bundle name for saving only a specific content type.
851   var bundleName;
852   // The corresponding filter string for a specific content type.
853   var filterString;
855   // Every case where GetSaveModeForContentType can return non-FILEONLY
856   // modes must be handled here.
857   if (aSaveMode != SAVEMODE_FILEONLY) {
858     switch (aContentType) {
859       case "text/html":
860         bundleName = "WebPageHTMLOnlyFilter";
861         filterString = "*.htm; *.html";
862         break;
864       case "application/xhtml+xml":
865         bundleName = "WebPageXHTMLOnlyFilter";
866         filterString = "*.xht; *.xhtml";
867         break;
869       case "image/svg+xml":
870         bundleName = "WebPageSVGOnlyFilter";
871         filterString = "*.svg; *.svgz";
872         break;
874       case "text/xml":
875       case "application/xml":
876         bundleName = "WebPageXMLOnlyFilter";
877         filterString = "*.xml";
878         break;
879     }
880   }
882   if (!bundleName) {
883     if (aSaveMode != SAVEMODE_FILEONLY) {
884       throw new Error(`Invalid save mode for type '${aContentType}'`);
885     }
887     var mimeInfo = getMIMEInfoForType(aContentType, aFileExtension);
888     if (mimeInfo) {
889       var extString = "";
890       for (var extension of mimeInfo.getFileExtensions()) {
891         if (extString) {
892           extString += "; ";
893         } // If adding more than one extension,
894         // separate by semi-colon
895         extString += "*." + extension;
896       }
898       if (extString) {
899         aFilePicker.appendFilter(mimeInfo.description, extString);
900       }
901     }
902   }
904   if (aSaveMode & SAVEMODE_COMPLETE_DOM) {
905     aFilePicker.appendFilter(
906       ContentAreaUtils.stringBundle.GetStringFromName("WebPageCompleteFilter"),
907       filterString
908     );
909     // We should always offer a choice to save document only if
910     // we allow saving as complete.
911     aFilePicker.appendFilter(
912       ContentAreaUtils.stringBundle.GetStringFromName(bundleName),
913       filterString
914     );
915   }
917   if (aSaveMode & SAVEMODE_COMPLETE_TEXT) {
918     aFilePicker.appendFilters(Ci.nsIFilePicker.filterText);
919   }
921   // Always append the all files (*) filter
922   aFilePicker.appendFilters(Ci.nsIFilePicker.filterAll);
925 function getPostData(aDocument) {
926   if (aDocument instanceof Ci.nsIWebBrowserPersistDocument) {
927     return aDocument.postData;
928   }
929   try {
930     // Find the session history entry corresponding to the given document. In
931     // the current implementation, nsIWebPageDescriptor.currentDescriptor always
932     // returns a session history entry.
933     let sessionHistoryEntry = aDocument.defaultView.docShell
934       .QueryInterface(Ci.nsIWebPageDescriptor)
935       .currentDescriptor.QueryInterface(Ci.nsISHEntry);
936     return sessionHistoryEntry.postData;
937   } catch (e) {}
938   return null;
941 function makeWebBrowserPersist() {
942   const persistContractID =
943     "@mozilla.org/embedding/browser/nsWebBrowserPersist;1";
944   const persistIID = Ci.nsIWebBrowserPersist;
945   return Cc[persistContractID].createInstance(persistIID);
948 function makeURI(aURL, aOriginCharset, aBaseURI) {
949   return Services.io.newURI(aURL, aOriginCharset, aBaseURI);
952 function makeFileURI(aFile) {
953   return Services.io.newFileURI(aFile);
956 function makeFilePicker() {
957   const fpContractID = "@mozilla.org/filepicker;1";
958   const fpIID = Ci.nsIFilePicker;
959   return Cc[fpContractID].createInstance(fpIID);
962 function getMIMEService() {
963   const mimeSvcContractID = "@mozilla.org/mime;1";
964   const mimeSvcIID = Ci.nsIMIMEService;
965   const mimeSvc = Cc[mimeSvcContractID].getService(mimeSvcIID);
966   return mimeSvc;
969 // Given aFileName, find the fileName without the extension on the end.
970 function getFileBaseName(aFileName) {
971   // Remove the file extension from aFileName:
972   return aFileName.replace(/\.[^.]*$/, "");
975 function getMIMETypeForURI(aURI) {
976   try {
977     return getMIMEService().getTypeFromURI(aURI);
978   } catch (e) {}
979   return null;
982 function getMIMEInfoForType(aMIMEType, aExtension) {
983   if (aMIMEType || aExtension) {
984     try {
985       return getMIMEService().getFromTypeAndExtension(aMIMEType, aExtension);
986     } catch (e) {}
987   }
988   return null;
991 function getDefaultFileName(
992   aDefaultFileName,
993   aURI,
994   aDocument,
995   aContentDisposition
996 ) {
997   // 1) look for a filename in the content-disposition header, if any
998   if (aContentDisposition) {
999     const mhpContractID = "@mozilla.org/network/mime-hdrparam;1";
1000     const mhpIID = Ci.nsIMIMEHeaderParam;
1001     const mhp = Cc[mhpContractID].getService(mhpIID);
1002     var dummy = { value: null }; // Need an out param...
1003     var charset = getCharsetforSave(aDocument);
1005     var fileName = null;
1006     try {
1007       fileName = mhp.getParameter(
1008         aContentDisposition,
1009         "filename",
1010         charset,
1011         true,
1012         dummy
1013       );
1014     } catch (e) {
1015       try {
1016         fileName = mhp.getParameter(
1017           aContentDisposition,
1018           "name",
1019           charset,
1020           true,
1021           dummy
1022         );
1023       } catch (e) {}
1024     }
1025     if (fileName) {
1026       return Services.textToSubURI.unEscapeURIForUI(
1027         fileName,
1028         /* dontEscape = */ true
1029       );
1030     }
1031   }
1033   let docTitle;
1034   if (aDocument && aDocument.title && aDocument.title.trim()) {
1035     // If the document looks like HTML or XML, try to use its original title.
1036     let contentType = aDocument.contentType;
1037     if (
1038       contentType == "application/xhtml+xml" ||
1039       contentType == "application/xml" ||
1040       contentType == "image/svg+xml" ||
1041       contentType == "text/html" ||
1042       contentType == "text/xml"
1043     ) {
1044       // 2) Use the document title
1045       return aDocument.title;
1046     }
1047   }
1049   try {
1050     var url = aURI.QueryInterface(Ci.nsIURL);
1051     if (url.fileName != "") {
1052       // 3) Use the actual file name, if present
1053       return Services.textToSubURI.unEscapeURIForUI(
1054         url.fileName,
1055         /* dontEscape = */ true
1056       );
1057     }
1058   } catch (e) {
1059     // This is something like a data: and so forth URI... no filename here.
1060   }
1062   // Don't use the title if it's from a data URI
1063   if (docTitle && aURI?.scheme != "data") {
1064     // 4) Use the document title
1065     return docTitle;
1066   }
1068   if (aDefaultFileName) {
1069     // 5) Use the caller-provided name, if any
1070     return aDefaultFileName;
1071   }
1073   try {
1074     if (aURI.host) {
1075       // 6) Use the host.
1076       return aURI.host;
1077     }
1078   } catch (e) {
1079     // Some files have no information at all, like Javascript generated pages
1080   }
1082   return "";
1085 // This is only used after the user has entered a filename.
1086 function validateFileName(aFileName) {
1087   let processed =
1088     DownloadPaths.sanitize(aFileName, {
1089       compressWhitespaces: false,
1090       allowInvalidFilenames: true,
1091     }) || "_";
1092   if (AppConstants.platform == "android") {
1093     // If a large part of the filename has been sanitized, then we
1094     // will use a default filename instead
1095     if (processed.replace(/_/g, "").length <= processed.length / 2) {
1096       // We purposefully do not use a localized default filename,
1097       // which we could have done using
1098       // ContentAreaUtils.stringBundle.GetStringFromName("UntitledSaveFileName")
1099       // since it may contain invalid characters.
1100       var original = processed;
1101       processed = "download";
1103       // Preserve a suffix, if there is one
1104       if (original.includes(".")) {
1105         var suffix = original.split(".").slice(-1)[0];
1106         if (suffix && !suffix.includes("_")) {
1107           processed += "." + suffix;
1108         }
1109       }
1110     }
1111   }
1112   return processed;
1115 function GetSaveModeForContentType(aContentType, aDocument) {
1116   // We can only save a complete page if we have a loaded document,
1117   if (!aDocument) {
1118     return SAVEMODE_FILEONLY;
1119   }
1121   // Find the possible save modes using the provided content type
1122   var saveMode = SAVEMODE_FILEONLY;
1123   switch (aContentType) {
1124     case "text/html":
1125     case "application/xhtml+xml":
1126     case "image/svg+xml":
1127       saveMode |= SAVEMODE_COMPLETE_TEXT;
1128     // Fall through
1129     case "text/xml":
1130     case "application/xml":
1131       saveMode |= SAVEMODE_COMPLETE_DOM;
1132       break;
1133   }
1135   return saveMode;
1138 function getCharsetforSave(aDocument) {
1139   if (aDocument) {
1140     return aDocument.characterSet;
1141   }
1143   if (document.commandDispatcher.focusedWindow) {
1144     return document.commandDispatcher.focusedWindow.document.characterSet;
1145   }
1147   return window.content.document.characterSet;
1151  * Open a URL from chrome, determining if we can handle it internally or need to
1152  *  launch an external application to handle it.
1153  * @param aURL The URL to be opened
1155  * WARNING: Please note that openURL() does not perform any content security checks!!!
1156  */
1157 function openURL(aURL) {
1158   var uri = aURL instanceof Ci.nsIURI ? aURL : makeURI(aURL);
1160   var protocolSvc = Cc[
1161     "@mozilla.org/uriloader/external-protocol-service;1"
1162   ].getService(Ci.nsIExternalProtocolService);
1164   let recentWindow = Services.wm.getMostRecentWindow("navigator:browser");
1166   if (!protocolSvc.isExposedProtocol(uri.scheme)) {
1167     // If we're not a browser, use the external protocol service to load the URI.
1168     protocolSvc.loadURI(uri, recentWindow?.document.contentPrincipal);
1169   } else {
1170     if (recentWindow) {
1171       recentWindow.openWebLinkIn(uri.spec, "tab", {
1172         triggeringPrincipal: recentWindow.document.contentPrincipal,
1173       });
1174       return;
1175     }
1177     var loadgroup = Cc["@mozilla.org/network/load-group;1"].createInstance(
1178       Ci.nsILoadGroup
1179     );
1180     var appstartup = Services.startup;
1182     var loadListener = {
1183       onStartRequest: function ll_start(aRequest) {
1184         appstartup.enterLastWindowClosingSurvivalArea();
1185       },
1186       onStopRequest: function ll_stop(aRequest, aStatusCode) {
1187         appstartup.exitLastWindowClosingSurvivalArea();
1188       },
1189       QueryInterface: ChromeUtils.generateQI([
1190         "nsIRequestObserver",
1191         "nsISupportsWeakReference",
1192       ]),
1193     };
1194     loadgroup.groupObserver = loadListener;
1196     var uriListener = {
1197       doContent(ctype, preferred, request, handler) {
1198         return false;
1199       },
1200       isPreferred(ctype, desired) {
1201         return false;
1202       },
1203       canHandleContent(ctype, preferred, desired) {
1204         return false;
1205       },
1206       loadCookie: null,
1207       parentContentListener: null,
1208       getInterface(iid) {
1209         if (iid.equals(Ci.nsIURIContentListener)) {
1210           return this;
1211         }
1212         if (iid.equals(Ci.nsILoadGroup)) {
1213           return loadgroup;
1214         }
1215         throw Components.Exception("", Cr.NS_ERROR_NO_INTERFACE);
1216       },
1217     };
1219     var channel = NetUtil.newChannel({
1220       uri,
1221       loadUsingSystemPrincipal: true,
1222     });
1224     if (channel) {
1225       channel.channelIsForDownload = true;
1226     }
1228     var uriLoader = Cc["@mozilla.org/uriloader;1"].getService(Ci.nsIURILoader);
1229     uriLoader.openURI(
1230       channel,
1231       Ci.nsIURILoader.IS_CONTENT_PREFERRED,
1232       uriListener
1233     );
1234   }