Bug 1608150 [wpt PR 21112] - Add missing space in `./wpt lint` command line docs...
[gecko.git] / toolkit / content / contentAreaUtils.js
blob073608f14901516d45cb5222c63902bb3248555e
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.import(
6   "resource://gre/modules/AppConstants.jsm"
7 );
8 var { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
9 var { XPCOMUtils } = ChromeUtils.import(
10   "resource://gre/modules/XPCOMUtils.jsm"
13 XPCOMUtils.defineLazyModuleGetters(this, {
14   BrowserUtils: "resource://gre/modules/BrowserUtils.jsm",
15   Downloads: "resource://gre/modules/Downloads.jsm",
16   DownloadPaths: "resource://gre/modules/DownloadPaths.jsm",
17   DownloadLastDir: "resource://gre/modules/DownloadLastDir.jsm",
18   FileUtils: "resource://gre/modules/FileUtils.jsm",
19   OS: "resource://gre/modules/osfile.jsm",
20   PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.jsm",
21   Deprecated: "resource://gre/modules/Deprecated.jsm",
22   NetUtil: "resource://gre/modules/NetUtil.jsm",
23 });
25 var ContentAreaUtils = {
26   get stringBundle() {
27     delete this.stringBundle;
28     return (this.stringBundle = Services.strings.createBundle(
29       "chrome://global/locale/contentAreaCommands.properties"
30     ));
31   },
34 function urlSecurityCheck(aURL, aPrincipal, aFlags) {
35   return BrowserUtils.urlSecurityCheck(aURL, aPrincipal, aFlags);
38 function forbidCPOW(arg, func, argname) {
39   if (
40     arg &&
41     (typeof arg == "object" || typeof arg == "function") &&
42     Cu.isCrossProcessWrapper(arg)
43   ) {
44     throw new Error(`no CPOWs allowed for argument ${argname} to ${func}`);
45   }
48 // Clientele: (Make sure you don't break any of these)
49 //  - File    ->  Save Page/Frame As...
50 //  - Context ->  Save Page/Frame As...
51 //  - Context ->  Save Link As...
52 //  - Alt-Click links in web pages
53 //  - Alt-Click links in the UI
55 // Try saving each of these types:
56 // - A complete webpage using File->Save Page As, and Context->Save Page As
57 // - A webpage as HTML only using the above methods
58 // - A webpage as Text only using the above methods
59 // - An image with an extension (e.g. .jpg) in its file name, using
60 //   Context->Save Image As...
61 // - An image without an extension (e.g. a banner ad on cnn.com) using
62 //   the above method.
63 // - A linked document using Save Link As...
64 // - A linked document using Alt-click Save Link As...
66 function saveURL(
67   aURL,
68   aFileName,
69   aFilePickerTitleKey,
70   aShouldBypassCache,
71   aSkipPrompt,
72   aReferrerInfo,
73   aSourceDocument,
74   aIsContentWindowPrivate,
75   aPrincipal
76 ) {
77   forbidCPOW(aURL, "saveURL", "aURL");
78   forbidCPOW(aReferrerInfo, "saveURL", "aReferrerInfo");
79   // Allow aSourceDocument to be a CPOW.
81   internalSave(
82     aURL,
83     null,
84     aFileName,
85     null,
86     null,
87     aShouldBypassCache,
88     aFilePickerTitleKey,
89     null,
90     aReferrerInfo,
91     aSourceDocument,
92     aSkipPrompt,
93     null,
94     aIsContentWindowPrivate,
95     aPrincipal
96   );
99 // Just like saveURL, but will get some info off the image before
100 // calling internalSave
101 // Clientele: (Make sure you don't break any of these)
102 //  - Context ->  Save Image As...
103 const imgICache = Ci.imgICache;
104 const nsISupportsCString = Ci.nsISupportsCString;
107  * Offers to save an image URL to the file system.
109  * @param aURL (string)
110  *        The URL of the image to be saved.
111  * @param aFileName (string)
112  *        The suggested filename for the saved file.
113  * @param aFilePickerTitleKey (string, optional)
114  *        Localized string key for an alternate title for the file
115  *        picker. If set to null, this will default to something sensible.
116  * @param aShouldBypassCache (bool)
117  *        If true, the image will always be retrieved from the server instead
118  *        of the network or image caches.
119  * @param aSkipPrompt (bool)
120  *        If true, we will attempt to save the file with the suggested
121  *        filename to the default downloads folder without showing the
122  *        file picker.
123  * @param aReferrerInfo (nsIReferrerInfo, optional)
124  *        the referrerInfo object to use, or null if no referrer should be sent.
125  * @param aDoc (Document, deprecated, optional)
126  *        The content document that the save is being initiated from. If this
127  *        is omitted, then aIsContentWindowPrivate must be provided.
128  * @param aContentType (string, optional)
129  *        The content type of the image.
130  * @param aContentDisp (string, optional)
131  *        The content disposition of the image.
132  * @param aIsContentWindowPrivate (bool)
133  *        Whether or not the containing window is in private browsing mode.
134  *        Does not need to be provided is aDoc is passed.
135  */
136 function saveImageURL(
137   aURL,
138   aFileName,
139   aFilePickerTitleKey,
140   aShouldBypassCache,
141   aSkipPrompt,
142   aReferrerInfo,
143   aDoc,
144   aContentType,
145   aContentDisp,
146   aIsContentWindowPrivate,
147   aPrincipal
148 ) {
149   forbidCPOW(aURL, "saveImageURL", "aURL");
150   forbidCPOW(aReferrerInfo, "saveImageURL", "aReferrerInfo");
152   if (aDoc && aIsContentWindowPrivate == undefined) {
153     if (Cu.isCrossProcessWrapper(aDoc)) {
154       Deprecated.warning(
155         "saveImageURL should not be passed document CPOWs. " +
156           "The caller should pass in the content type and " +
157           "disposition themselves",
158         "https://bugzilla.mozilla.org/show_bug.cgi?id=1243643"
159       );
160     }
161     // This will definitely not work for in-browser code or multi-process compatible
162     // add-ons due to bug 1233497, which makes unsafe CPOW usage throw by default.
163     Deprecated.warning(
164       "saveImageURL should be passed the private state of " +
165         "the containing window.",
166       "https://bugzilla.mozilla.org/show_bug.cgi?id=1243643"
167     );
168     aIsContentWindowPrivate = PrivateBrowsingUtils.isContentWindowPrivate(
169       aDoc.defaultView
170     );
171   }
173   // We'd better have the private state by now.
174   if (aIsContentWindowPrivate == undefined) {
175     throw new Error(
176       "saveImageURL couldn't compute private state of content window"
177     );
178   }
180   if (
181     !aShouldBypassCache &&
182     (aDoc && !Cu.isCrossProcessWrapper(aDoc)) &&
183     (!aContentType && !aContentDisp)
184   ) {
185     try {
186       var imageCache = Cc["@mozilla.org/image/tools;1"]
187         .getService(Ci.imgITools)
188         .getImgCacheForDocument(aDoc);
189       var props = imageCache.findEntryProperties(
190         makeURI(aURL, getCharsetforSave(null)),
191         aDoc
192       );
193       if (props) {
194         aContentType = props.get("type", nsISupportsCString);
195         aContentDisp = props.get("content-disposition", nsISupportsCString);
196       }
197     } catch (e) {
198       // Failure to get type and content-disposition off the image is non-fatal
199     }
200   }
202   internalSave(
203     aURL,
204     null,
205     aFileName,
206     aContentDisp,
207     aContentType,
208     aShouldBypassCache,
209     aFilePickerTitleKey,
210     null,
211     aReferrerInfo,
212     aDoc,
213     aSkipPrompt,
214     null,
215     aIsContentWindowPrivate,
216     aPrincipal
217   );
220 // This is like saveDocument, but takes any browser/frame-like element
221 // and saves the current document inside it,
222 // whether in-process or out-of-process.
223 function saveBrowser(aBrowser, aSkipPrompt, aOuterWindowID = 0) {
224   if (!aBrowser) {
225     throw new Error("Must have a browser when calling saveBrowser");
226   }
227   let persistable = aBrowser.frameLoader;
228   // Because of how pdf.js deals with principals, saving the document the "normal"
229   // way won't work. Work around this by saving the pdf's URL directly:
230   if (
231     aBrowser.contentPrincipal.URI &&
232     aBrowser.contentPrincipal.URI.spec == "resource://pdf.js/web/viewer.html" &&
233     aBrowser.currentURI.schemeIs("file")
234   ) {
235     let correctPrincipal = Services.scriptSecurityManager.createContentPrincipal(
236       aBrowser.currentURI,
237       aBrowser.contentPrincipal.originAttributes
238     );
239     internalSave(
240       aBrowser.currentURI.spec,
241       null /* no document */,
242       null /* automatically determine filename */,
243       null /* no content disposition */,
244       "application/pdf",
245       false /* don't bypass cache */,
246       null /* no alternative title */,
247       null /* no auto-chosen file info */,
248       null /* null referrer will be OK for file: */,
249       null /* no document */,
250       aSkipPrompt /* caller decides about prompting */,
251       null /* no cache key because the one for the document will be for pdfjs */,
252       PrivateBrowsingUtils.isWindowPrivate(aBrowser.ownerGlobal),
253       correctPrincipal
254     );
255     return;
256   }
257   let stack = Components.stack.caller;
258   persistable.startPersistence(aOuterWindowID, {
259     onDocumentReady(document) {
260       saveDocument(document, aSkipPrompt);
261     },
262     onError(status) {
263       throw new Components.Exception(
264         "saveBrowser failed asynchronously in startPersistence",
265         status,
266         stack
267       );
268     },
269   });
272 // Saves a document; aDocument can be an nsIWebBrowserPersistDocument
273 // (see saveBrowser, above) or a Document.
275 // aDocument can also be a CPOW for a remote Document, in which
276 // case "save as" modes that serialize the document's DOM are
277 // unavailable.  This is a temporary measure for the "Save Frame As"
278 // command (bug 1141337) and pre-e10s add-ons.
279 function saveDocument(aDocument, aSkipPrompt) {
280   if (!aDocument) {
281     throw new Error("Must have a document when calling saveDocument");
282   }
284   let contentDisposition = null;
285   let cacheKey = 0;
287   if (aDocument instanceof Ci.nsIWebBrowserPersistDocument) {
288     // nsIWebBrowserPersistDocument exposes these directly.
289     contentDisposition = aDocument.contentDisposition;
290     cacheKey = aDocument.cacheKey;
291   } else if (aDocument.nodeType == 9 /* DOCUMENT_NODE */) {
292     // Otherwise it's an actual document (and possibly a CPOW).
293     // We want to use cached data because the document is currently visible.
294     let win = aDocument.defaultView;
296     try {
297       contentDisposition = win.windowUtils.getDocumentMetadata(
298         "content-disposition"
299       );
300     } catch (ex) {
301       // Failure to get a content-disposition is ok
302     }
304     try {
305       let shEntry = win.docShell
306         .QueryInterface(Ci.nsIWebPageDescriptor)
307         .currentDescriptor.QueryInterface(Ci.nsISHEntry);
309       cacheKey = shEntry.cacheKey;
310     } catch (ex) {
311       // We might not find it in the cache.  Oh, well.
312     }
313   }
315   internalSave(
316     aDocument.documentURI,
317     aDocument,
318     null,
319     contentDisposition,
320     aDocument.contentType,
321     false,
322     null,
323     null,
324     aDocument.referrerInfo,
325     aDocument,
326     aSkipPrompt,
327     cacheKey
328   );
331 function DownloadListener(win, transfer) {
332   function makeClosure(name) {
333     return function() {
334       transfer[name].apply(transfer, arguments);
335     };
336   }
338   this.window = win;
340   // Now... we need to forward all calls to our transfer
341   for (var i in transfer) {
342     if (i != "QueryInterface") {
343       this[i] = makeClosure(i);
344     }
345   }
348 DownloadListener.prototype = {
349   QueryInterface: ChromeUtils.generateQI([
350     "nsIInterfaceRequestor",
351     "nsIWebProgressListener",
352     "nsIWebProgressListener2",
353   ]),
355   getInterface: function dl_gi(aIID) {
356     if (aIID.equals(Ci.nsIAuthPrompt) || aIID.equals(Ci.nsIAuthPrompt2)) {
357       var ww = Cc["@mozilla.org/embedcomp/window-watcher;1"].getService(
358         Ci.nsIPromptFactory
359       );
360       return ww.getPrompt(this.window, aIID);
361     }
363     throw Cr.NS_ERROR_NO_INTERFACE;
364   },
367 const kSaveAsType_Complete = 0; // Save document with attached objects.
368 XPCOMUtils.defineConstant(this, "kSaveAsType_Complete", 0);
369 // const kSaveAsType_URL      = 1; // Save document or URL by itself.
370 const kSaveAsType_Text = 2; // Save document, converting to plain text.
371 XPCOMUtils.defineConstant(this, "kSaveAsType_Text", kSaveAsType_Text);
374  * internalSave: Used when saving a document or URL.
376  * If aChosenData is null, this method:
377  *  - Determines a local target filename to use
378  *  - Prompts the user to confirm the destination filename and save mode
379  *    (aContentType affects this)
380  *  - [Note] This process involves the parameters aURL, aReferrerInfo,
381  *    aDocument, aDefaultFileName, aFilePickerTitleKey, and aSkipPrompt.
383  * If aChosenData is non-null, this method:
384  *  - Uses the provided source URI and save file name
385  *  - Saves the document as complete DOM if possible (aDocument present and
386  *    right aContentType)
387  *  - [Note] The parameters aURL, aDefaultFileName, aFilePickerTitleKey, and
388  *    aSkipPrompt are ignored.
390  * In any case, this method:
391  *  - Creates a 'Persist' object (which will perform the saving in the
392  *    background) and then starts it.
393  *  - [Note] This part of the process only involves the parameters aDocument,
394  *    aShouldBypassCache and aReferrerInfo. The source, the save name and the
395  *    save mode are the ones determined previously.
397  * @param aURL
398  *        The String representation of the URL of the document being saved
399  * @param aDocument
400  *        The document to be saved
401  * @param aDefaultFileName
402  *        The caller-provided suggested filename if we don't
403  *        find a better one
404  * @param aContentDisposition
405  *        The caller-provided content-disposition header to use.
406  * @param aContentType
407  *        The caller-provided content-type to use
408  * @param aShouldBypassCache
409  *        If true, the document will always be refetched from the server
410  * @param aFilePickerTitleKey
411  *        Alternate title for the file picker
412  * @param aChosenData
413  *        If non-null this contains an instance of object AutoChosen (see below)
414  *        which holds pre-determined data so that the user does not need to be
415  *        prompted for a target filename.
416  * @param aReferrerInfo
417  *        the referrerInfo object to use, or null if no referrer should be sent.
418  * @param aInitiatingDocument [optional]
419  *        The document from which the save was initiated.
420  *        If this is omitted then aIsContentWindowPrivate has to be provided.
421  * @param aSkipPrompt [optional]
422  *        If set to true, we will attempt to save the file to the
423  *        default downloads folder without prompting.
424  * @param aCacheKey [optional]
425  *        If set will be passed to saveURI.  See nsIWebBrowserPersist for
426  *        allowed values.
427  * @param aIsContentWindowPrivate [optional]
428  *        This parameter is provided when the aInitiatingDocument is not a
429  *        real document object. Stores whether aInitiatingDocument.defaultView
430  *        was private or not.
431  * @param aPrincipal [optional]
432  *        This parameter is provided when neither aDocument nor
433  *        aInitiatingDocument is provided. Used to determine what level of
434  *        privilege to load the URI with.
435  */
436 function internalSave(
437   aURL,
438   aDocument,
439   aDefaultFileName,
440   aContentDisposition,
441   aContentType,
442   aShouldBypassCache,
443   aFilePickerTitleKey,
444   aChosenData,
445   aReferrerInfo,
446   aInitiatingDocument,
447   aSkipPrompt,
448   aCacheKey,
449   aIsContentWindowPrivate,
450   aPrincipal
451 ) {
452   forbidCPOW(aURL, "internalSave", "aURL");
453   forbidCPOW(aReferrerInfo, "internalSave", "aReferrerInfo");
454   forbidCPOW(aCacheKey, "internalSave", "aCacheKey");
455   // Allow aInitiatingDocument to be a CPOW.
457   if (aSkipPrompt == undefined) {
458     aSkipPrompt = false;
459   }
461   if (aCacheKey == undefined) {
462     aCacheKey = 0;
463   }
465   // Note: aDocument == null when this code is used by save-link-as...
466   var saveMode = GetSaveModeForContentType(aContentType, aDocument);
468   var file, sourceURI, saveAsType;
469   // Find the URI object for aURL and the FileName/Extension to use when saving.
470   // FileName/Extension will be ignored if aChosenData supplied.
471   if (aChosenData) {
472     file = aChosenData.file;
473     sourceURI = aChosenData.uri;
474     saveAsType = kSaveAsType_Complete;
476     continueSave();
477   } else {
478     var charset = null;
479     if (aDocument) {
480       charset = aDocument.characterSet;
481     }
482     var fileInfo = new FileInfo(aDefaultFileName);
483     initFileInfo(
484       fileInfo,
485       aURL,
486       charset,
487       aDocument,
488       aContentType,
489       aContentDisposition
490     );
491     sourceURI = fileInfo.uri;
493     var fpParams = {
494       fpTitleKey: aFilePickerTitleKey,
495       fileInfo,
496       contentType: aContentType,
497       saveMode,
498       saveAsType: kSaveAsType_Complete,
499       file,
500     };
502     // Find a URI to use for determining last-downloaded-to directory
503     let relatedURI = aReferrerInfo ? aReferrerInfo.originalReferrer : sourceURI;
505     promiseTargetFile(fpParams, aSkipPrompt, relatedURI)
506       .then(aDialogAccepted => {
507         if (!aDialogAccepted) {
508           return;
509         }
511         saveAsType = fpParams.saveAsType;
512         file = fpParams.file;
514         continueSave();
515       })
516       .catch(Cu.reportError);
517   }
519   function continueSave() {
520     // XXX We depend on the following holding true in appendFiltersForContentType():
521     // If we should save as a complete page, the saveAsType is kSaveAsType_Complete.
522     // If we should save as text, the saveAsType is kSaveAsType_Text.
523     var useSaveDocument =
524       aDocument &&
525       ((saveMode & SAVEMODE_COMPLETE_DOM &&
526         saveAsType == kSaveAsType_Complete) ||
527         (saveMode & SAVEMODE_COMPLETE_TEXT && saveAsType == kSaveAsType_Text));
528     // If we're saving a document, and are saving either in complete mode or
529     // as converted text, pass the document to the web browser persist component.
530     // If we're just saving the HTML (second option in the list), send only the URI.
531     let nonCPOWDocument = aDocument && !Cu.isCrossProcessWrapper(aDocument);
533     let isPrivate = aIsContentWindowPrivate;
534     if (isPrivate === undefined) {
535       isPrivate =
536         aInitiatingDocument.nodeType == 9 /* DOCUMENT_NODE */
537           ? PrivateBrowsingUtils.isContentWindowPrivate(
538               aInitiatingDocument.defaultView
539             )
540           : aInitiatingDocument.isPrivate;
541     }
543     // We have to cover the cases here where we were either passed an explicit
544     // principal, or a 'real' document (with a nodePrincipal property), or an
545     // nsIWebBrowserPersistDocument which has a principal property.
546     let sourcePrincipal =
547       aPrincipal ||
548       (aDocument && (aDocument.nodePrincipal || aDocument.principal)) ||
549       (aInitiatingDocument && aInitiatingDocument.nodePrincipal);
551     var persistArgs = {
552       sourceURI,
553       sourcePrincipal,
554       sourceReferrerInfo: aReferrerInfo,
555       sourceDocument: useSaveDocument ? aDocument : null,
556       targetContentType: saveAsType == kSaveAsType_Text ? "text/plain" : null,
557       targetFile: file,
558       sourceCacheKey: aCacheKey,
559       sourcePostData: nonCPOWDocument ? getPostData(aDocument) : null,
560       bypassCache: aShouldBypassCache,
561       isPrivate,
562     };
564     // Start the actual save process
565     internalPersist(persistArgs);
566   }
570  * internalPersist: Creates a 'Persist' object (which will perform the saving
571  *  in the background) and then starts it.
573  * @param persistArgs.sourceURI
574  *        The nsIURI of the document being saved
575  * @param persistArgs.sourceCacheKey [optional]
576  *        If set will be passed to savePrivacyAwareURI
577  * @param persistArgs.sourceDocument [optional]
578  *        The document to be saved, or null if not saving a complete document
579  * @param persistArgs.sourceReferrerInfo
580  *        Required and used only when persistArgs.sourceDocument is NOT present,
581  *        the nsIReferrerInfo of the referrer info to use, or null if no
582  *        referrer should be sent.
583  * @param persistArgs.sourcePostData
584  *        Required and used only when persistArgs.sourceDocument is NOT present,
585  *        represents the POST data to be sent along with the HTTP request, and
586  *        must be null if no POST data should be sent.
587  * @param persistArgs.targetFile
588  *        The nsIFile of the file to create
589  * @param persistArgs.targetContentType
590  *        Required and used only when persistArgs.sourceDocument is present,
591  *        determines the final content type of the saved file, or null to use
592  *        the same content type as the source document. Currently only
593  *        "text/plain" is meaningful.
594  * @param persistArgs.bypassCache
595  *        If true, the document will always be refetched from the server
596  * @param persistArgs.isPrivate
597  *        Indicates whether this is taking place in a private browsing context.
598  */
599 function internalPersist(persistArgs) {
600   var persist = makeWebBrowserPersist();
602   // Calculate persist flags.
603   const nsIWBP = Ci.nsIWebBrowserPersist;
604   const flags = nsIWBP.PERSIST_FLAGS_REPLACE_EXISTING_FILES;
605   if (persistArgs.bypassCache) {
606     persist.persistFlags = flags | nsIWBP.PERSIST_FLAGS_BYPASS_CACHE;
607   } else {
608     persist.persistFlags = flags | nsIWBP.PERSIST_FLAGS_FROM_CACHE;
609   }
611   // Leave it to WebBrowserPersist to discover the encoding type (or lack thereof):
612   persist.persistFlags |= nsIWBP.PERSIST_FLAGS_AUTODETECT_APPLY_CONVERSION;
614   // Find the URI associated with the target file
615   var targetFileURL = makeFileURI(persistArgs.targetFile);
617   // Create download and initiate it (below)
618   var tr = Cc["@mozilla.org/transfer;1"].createInstance(Ci.nsITransfer);
619   tr.init(
620     persistArgs.sourceURI,
621     targetFileURL,
622     "",
623     null,
624     null,
625     null,
626     persist,
627     persistArgs.isPrivate
628   );
629   persist.progressListener = new DownloadListener(window, tr);
631   if (persistArgs.sourceDocument) {
632     // Saving a Document, not a URI:
633     var filesFolder = null;
634     if (persistArgs.targetContentType != "text/plain") {
635       // Create the local directory into which to save associated files.
636       filesFolder = persistArgs.targetFile.clone();
638       var nameWithoutExtension = getFileBaseName(filesFolder.leafName);
639       var filesFolderLeafName = ContentAreaUtils.stringBundle.formatStringFromName(
640         "filesFolder",
641         [nameWithoutExtension]
642       );
644       filesFolder.leafName = filesFolderLeafName;
645     }
647     var encodingFlags = 0;
648     if (persistArgs.targetContentType == "text/plain") {
649       encodingFlags |= nsIWBP.ENCODE_FLAGS_FORMATTED;
650       encodingFlags |= nsIWBP.ENCODE_FLAGS_ABSOLUTE_LINKS;
651       encodingFlags |= nsIWBP.ENCODE_FLAGS_NOFRAMES_CONTENT;
652     } else {
653       encodingFlags |= nsIWBP.ENCODE_FLAGS_ENCODE_BASIC_ENTITIES;
654     }
656     const kWrapColumn = 80;
657     persist.saveDocument(
658       persistArgs.sourceDocument,
659       targetFileURL,
660       filesFolder,
661       persistArgs.targetContentType,
662       encodingFlags,
663       kWrapColumn
664     );
665   } else {
666     persist.savePrivacyAwareURI(
667       persistArgs.sourceURI,
668       persistArgs.sourcePrincipal,
669       persistArgs.sourceCacheKey,
670       persistArgs.sourceReferrerInfo,
671       persistArgs.sourcePostData,
672       null,
673       targetFileURL,
674       persistArgs.isPrivate
675     );
676   }
680  * Structure for holding info about automatically supplied parameters for
681  * internalSave(...). This allows parameters to be supplied so the user does not
682  * need to be prompted for file info.
683  * @param aFileAutoChosen This is an nsIFile object that has been
684  *        pre-determined as the filename for the target to save to
685  * @param aUriAutoChosen  This is the nsIURI object for the target
686  */
687 function AutoChosen(aFileAutoChosen, aUriAutoChosen) {
688   this.file = aFileAutoChosen;
689   this.uri = aUriAutoChosen;
693  * Structure for holding info about a URL and the target filename it should be
694  * saved to. This structure is populated by initFileInfo(...).
695  * @param aSuggestedFileName This is used by initFileInfo(...) when it
696  *        cannot 'discover' the filename from the url
697  * @param aFileName The target filename
698  * @param aFileBaseName The filename without the file extension
699  * @param aFileExt The extension of the filename
700  * @param aUri An nsIURI object for the url that is being saved
701  */
702 function FileInfo(
703   aSuggestedFileName,
704   aFileName,
705   aFileBaseName,
706   aFileExt,
707   aUri
708 ) {
709   this.suggestedFileName = aSuggestedFileName;
710   this.fileName = aFileName;
711   this.fileBaseName = aFileBaseName;
712   this.fileExt = aFileExt;
713   this.uri = aUri;
717  * Determine what the 'default' filename string is, its file extension and the
718  * filename without the extension. This filename is used when prompting the user
719  * for confirmation in the file picker dialog.
720  * @param aFI A FileInfo structure into which we'll put the results of this method.
721  * @param aURL The String representation of the URL of the document being saved
722  * @param aURLCharset The charset of aURL.
723  * @param aDocument The document to be saved
724  * @param aContentType The content type we're saving, if it could be
725  *        determined by the caller.
726  * @param aContentDisposition The content-disposition header for the object
727  *        we're saving, if it could be determined by the caller.
728  */
729 function initFileInfo(
730   aFI,
731   aURL,
732   aURLCharset,
733   aDocument,
734   aContentType,
735   aContentDisposition
736 ) {
737   try {
738     // Get an nsIURI object from aURL if possible:
739     try {
740       aFI.uri = makeURI(aURL, aURLCharset);
741       // Assuming nsiUri is valid, calling QueryInterface(...) on it will
742       // populate extra object fields (eg filename and file extension).
743       var url = aFI.uri.QueryInterface(Ci.nsIURL);
744       aFI.fileExt = url.fileExtension;
745     } catch (e) {}
747     // Get the default filename:
748     aFI.fileName = getDefaultFileName(
749       aFI.suggestedFileName || aFI.fileName,
750       aFI.uri,
751       aDocument,
752       aContentDisposition
753     );
754     // If aFI.fileExt is still blank, consider: aFI.suggestedFileName is supplied
755     // if saveURL(...) was the original caller (hence both aContentType and
756     // aDocument are blank). If they were saving a link to a website then make
757     // the extension .htm .
758     if (
759       !aFI.fileExt &&
760       !aDocument &&
761       !aContentType &&
762       /^http(s?):\/\//i.test(aURL)
763     ) {
764       aFI.fileExt = "htm";
765       aFI.fileBaseName = aFI.fileName;
766     } else {
767       aFI.fileExt = getDefaultExtension(aFI.fileName, aFI.uri, aContentType);
768       aFI.fileBaseName = getFileBaseName(aFI.fileName);
769     }
770   } catch (e) {}
774  * Given the Filepicker Parameters (aFpP), show the file picker dialog,
775  * prompting the user to confirm (or change) the fileName.
776  * @param aFpP
777  *        A structure (see definition in internalSave(...) method)
778  *        containing all the data used within this method.
779  * @param aSkipPrompt
780  *        If true, attempt to save the file automatically to the user's default
781  *        download directory, thus skipping the explicit prompt for a file name,
782  *        but only if the associated preference is set.
783  *        If false, don't save the file automatically to the user's
784  *        default download directory, even if the associated preference
785  *        is set, but ask for the target explicitly.
786  * @param aRelatedURI
787  *        An nsIURI associated with the download. The last used
788  *        directory of the picker is retrieved from/stored in the
789  *        Content Pref Service using this URI.
790  * @return Promise
791  * @resolve a boolean. When true, it indicates that the file picker dialog
792  *          is accepted.
793  */
794 function promiseTargetFile(
795   aFpP,
796   /* optional */ aSkipPrompt,
797   /* optional */ aRelatedURI
798 ) {
799   return (async function() {
800     let downloadLastDir = new DownloadLastDir(window);
801     let prefBranch = Services.prefs.getBranch("browser.download.");
802     let useDownloadDir = prefBranch.getBoolPref("useDownloadDir");
804     if (!aSkipPrompt) {
805       useDownloadDir = false;
806     }
808     // Default to the user's default downloads directory configured
809     // through download prefs.
810     let dirPath = await Downloads.getPreferredDownloadsDirectory();
811     let dirExists = await OS.File.exists(dirPath);
812     let dir = new FileUtils.File(dirPath);
814     if (useDownloadDir && dirExists) {
815       dir.append(
816         getNormalizedLeafName(aFpP.fileInfo.fileName, aFpP.fileInfo.fileExt)
817       );
818       aFpP.file = uniqueFile(dir);
819       return true;
820     }
822     // We must prompt for the file name explicitly.
823     // If we must prompt because we were asked to...
824     let file = await new Promise(resolve => {
825       if (useDownloadDir) {
826         // Keep async behavior in both branches
827         Services.tm.dispatchToMainThread(function() {
828           resolve(null);
829         });
830       } else {
831         downloadLastDir.getFileAsync(aRelatedURI, function getFileAsyncCB(
832           aFile
833         ) {
834           resolve(aFile);
835         });
836       }
837     });
838     if (file && (await OS.File.exists(file.path))) {
839       dir = file;
840       dirExists = true;
841     }
843     if (!dirExists) {
844       // Default to desktop.
845       dir = Services.dirsvc.get("Desk", Ci.nsIFile);
846     }
848     let fp = makeFilePicker();
849     let titleKey = aFpP.fpTitleKey || "SaveLinkTitle";
850     fp.init(
851       window,
852       ContentAreaUtils.stringBundle.GetStringFromName(titleKey),
853       Ci.nsIFilePicker.modeSave
854     );
856     fp.displayDirectory = dir;
857     fp.defaultExtension = aFpP.fileInfo.fileExt;
858     fp.defaultString = getNormalizedLeafName(
859       aFpP.fileInfo.fileName,
860       aFpP.fileInfo.fileExt
861     );
862     appendFiltersForContentType(
863       fp,
864       aFpP.contentType,
865       aFpP.fileInfo.fileExt,
866       aFpP.saveMode
867     );
869     // The index of the selected filter is only preserved and restored if there's
870     // more than one filter in addition to "All Files".
871     if (aFpP.saveMode != SAVEMODE_FILEONLY) {
872       // eslint-disable-next-line mozilla/use-default-preference-values
873       try {
874         fp.filterIndex = prefBranch.getIntPref("save_converter_index");
875       } catch (e) {}
876     }
878     let result = await new Promise(resolve => {
879       fp.open(function(aResult) {
880         resolve(aResult);
881       });
882     });
883     if (result == Ci.nsIFilePicker.returnCancel || !fp.file) {
884       return false;
885     }
887     if (aFpP.saveMode != SAVEMODE_FILEONLY) {
888       prefBranch.setIntPref("save_converter_index", fp.filterIndex);
889     }
891     // Do not store the last save directory as a pref inside the private browsing mode
892     downloadLastDir.setFile(aRelatedURI, fp.file.parent);
894     fp.file.leafName = validateFileName(fp.file.leafName);
896     aFpP.saveAsType = fp.filterIndex;
897     aFpP.file = fp.file;
898     aFpP.fileURL = fp.fileURL;
900     return true;
901   })();
904 // Since we're automatically downloading, we don't get the file picker's
905 // logic to check for existing files, so we need to do that here.
907 // Note - this code is identical to that in
908 //   mozilla/toolkit/mozapps/downloads/src/nsHelperAppDlg.js.in
909 // If you are updating this code, update that code too! We can't share code
910 // here since that code is called in a js component.
911 function uniqueFile(aLocalFile) {
912   var collisionCount = 0;
913   while (aLocalFile.exists()) {
914     collisionCount++;
915     if (collisionCount == 1) {
916       // Append "(2)" before the last dot in (or at the end of) the filename
917       // special case .ext.gz etc files so we don't wind up with .tar(2).gz
918       if (aLocalFile.leafName.match(/\.[^\.]{1,3}\.(gz|bz2|Z)$/i)) {
919         aLocalFile.leafName = aLocalFile.leafName.replace(
920           /\.[^\.]{1,3}\.(gz|bz2|Z)$/i,
921           "(2)$&"
922         );
923       } else {
924         aLocalFile.leafName = aLocalFile.leafName.replace(
925           /(\.[^\.]*)?$/,
926           "(2)$&"
927         );
928       }
929     } else {
930       // replace the last (n) in the filename with (n+1)
931       aLocalFile.leafName = aLocalFile.leafName.replace(
932         /^(.*\()\d+\)/,
933         "$1" + (collisionCount + 1) + ")"
934       );
935     }
936   }
937   return aLocalFile;
941  * Download a URL using the Downloads API.
943  * @param aURL
944  *        the url to download
945  * @param [optional] aFileName
946  *        the destination file name, if omitted will be obtained from the url.
947  * @param aInitiatingDocument
948  *        The document from which the download was initiated.
949  */
950 function DownloadURL(aURL, aFileName, aInitiatingDocument) {
951   // For private browsing, try to get document out of the most recent browser
952   // window, or provide our own if there's no browser window.
953   let isPrivate = aInitiatingDocument.defaultView.docShell.QueryInterface(
954     Ci.nsILoadContext
955   ).usePrivateBrowsing;
957   let fileInfo = new FileInfo(aFileName);
958   initFileInfo(fileInfo, aURL, null, null, null, null);
960   let filepickerParams = {
961     fileInfo,
962     saveMode: SAVEMODE_FILEONLY,
963   };
965   (async function() {
966     let accepted = await promiseTargetFile(
967       filepickerParams,
968       true,
969       fileInfo.uri
970     );
971     if (!accepted) {
972       return;
973     }
975     let file = filepickerParams.file;
976     let download = await Downloads.createDownload({
977       source: { url: aURL, isPrivate },
978       target: { path: file.path, partFilePath: file.path + ".part" },
979     });
980     download.tryToKeepPartialData = true;
982     // Ignore errors because failures are reported through the download list.
983     download.start().catch(() => {});
985     // Add the download to the list, allowing it to be managed.
986     let list = await Downloads.getList(Downloads.ALL);
987     list.add(download);
988   })().catch(Cu.reportError);
991 // We have no DOM, and can only save the URL as is.
992 const SAVEMODE_FILEONLY = 0x00;
993 XPCOMUtils.defineConstant(this, "SAVEMODE_FILEONLY", SAVEMODE_FILEONLY);
994 // We have a DOM and can save as complete.
995 const SAVEMODE_COMPLETE_DOM = 0x01;
996 XPCOMUtils.defineConstant(this, "SAVEMODE_COMPLETE_DOM", SAVEMODE_COMPLETE_DOM);
997 // We have a DOM which we can serialize as text.
998 const SAVEMODE_COMPLETE_TEXT = 0x02;
999 XPCOMUtils.defineConstant(
1000   this,
1001   "SAVEMODE_COMPLETE_TEXT",
1002   SAVEMODE_COMPLETE_TEXT
1005 // If we are able to save a complete DOM, the 'save as complete' filter
1006 // must be the first filter appended.  The 'save page only' counterpart
1007 // must be the second filter appended.  And the 'save as complete text'
1008 // filter must be the third filter appended.
1009 function appendFiltersForContentType(
1010   aFilePicker,
1011   aContentType,
1012   aFileExtension,
1013   aSaveMode
1014 ) {
1015   // The bundle name for saving only a specific content type.
1016   var bundleName;
1017   // The corresponding filter string for a specific content type.
1018   var filterString;
1020   // Every case where GetSaveModeForContentType can return non-FILEONLY
1021   // modes must be handled here.
1022   if (aSaveMode != SAVEMODE_FILEONLY) {
1023     switch (aContentType) {
1024       case "text/html":
1025         bundleName = "WebPageHTMLOnlyFilter";
1026         filterString = "*.htm; *.html";
1027         break;
1029       case "application/xhtml+xml":
1030         bundleName = "WebPageXHTMLOnlyFilter";
1031         filterString = "*.xht; *.xhtml";
1032         break;
1034       case "image/svg+xml":
1035         bundleName = "WebPageSVGOnlyFilter";
1036         filterString = "*.svg; *.svgz";
1037         break;
1039       case "text/xml":
1040       case "application/xml":
1041         bundleName = "WebPageXMLOnlyFilter";
1042         filterString = "*.xml";
1043         break;
1044     }
1045   }
1047   if (!bundleName) {
1048     if (aSaveMode != SAVEMODE_FILEONLY) {
1049       throw new Error(`Invalid save mode for type '${aContentType}'`);
1050     }
1052     var mimeInfo = getMIMEInfoForType(aContentType, aFileExtension);
1053     if (mimeInfo) {
1054       var extString = "";
1055       for (var extension of mimeInfo.getFileExtensions()) {
1056         if (extString) {
1057           extString += "; ";
1058         } // If adding more than one extension,
1059         // separate by semi-colon
1060         extString += "*." + extension;
1061       }
1063       if (extString) {
1064         aFilePicker.appendFilter(mimeInfo.description, extString);
1065       }
1066     }
1067   }
1069   if (aSaveMode & SAVEMODE_COMPLETE_DOM) {
1070     aFilePicker.appendFilter(
1071       ContentAreaUtils.stringBundle.GetStringFromName("WebPageCompleteFilter"),
1072       filterString
1073     );
1074     // We should always offer a choice to save document only if
1075     // we allow saving as complete.
1076     aFilePicker.appendFilter(
1077       ContentAreaUtils.stringBundle.GetStringFromName(bundleName),
1078       filterString
1079     );
1080   }
1082   if (aSaveMode & SAVEMODE_COMPLETE_TEXT) {
1083     aFilePicker.appendFilters(Ci.nsIFilePicker.filterText);
1084   }
1086   // Always append the all files (*) filter
1087   aFilePicker.appendFilters(Ci.nsIFilePicker.filterAll);
1090 function getPostData(aDocument) {
1091   if (aDocument instanceof Ci.nsIWebBrowserPersistDocument) {
1092     return aDocument.postData;
1093   }
1094   try {
1095     // Find the session history entry corresponding to the given document. In
1096     // the current implementation, nsIWebPageDescriptor.currentDescriptor always
1097     // returns a session history entry.
1098     let sessionHistoryEntry = aDocument.defaultView.docShell
1099       .QueryInterface(Ci.nsIWebPageDescriptor)
1100       .currentDescriptor.QueryInterface(Ci.nsISHEntry);
1101     return sessionHistoryEntry.postData;
1102   } catch (e) {}
1103   return null;
1106 function makeWebBrowserPersist() {
1107   const persistContractID =
1108     "@mozilla.org/embedding/browser/nsWebBrowserPersist;1";
1109   const persistIID = Ci.nsIWebBrowserPersist;
1110   return Cc[persistContractID].createInstance(persistIID);
1113 function makeURI(aURL, aOriginCharset, aBaseURI) {
1114   return Services.io.newURI(aURL, aOriginCharset, aBaseURI);
1117 function makeFileURI(aFile) {
1118   return Services.io.newFileURI(aFile);
1121 function makeFilePicker() {
1122   const fpContractID = "@mozilla.org/filepicker;1";
1123   const fpIID = Ci.nsIFilePicker;
1124   return Cc[fpContractID].createInstance(fpIID);
1127 function getMIMEService() {
1128   const mimeSvcContractID = "@mozilla.org/mime;1";
1129   const mimeSvcIID = Ci.nsIMIMEService;
1130   const mimeSvc = Cc[mimeSvcContractID].getService(mimeSvcIID);
1131   return mimeSvc;
1134 // Given aFileName, find the fileName without the extension on the end.
1135 function getFileBaseName(aFileName) {
1136   // Remove the file extension from aFileName:
1137   return aFileName.replace(/\.[^.]*$/, "");
1140 function getMIMETypeForURI(aURI) {
1141   try {
1142     return getMIMEService().getTypeFromURI(aURI);
1143   } catch (e) {}
1144   return null;
1147 function getMIMEInfoForType(aMIMEType, aExtension) {
1148   if (aMIMEType || aExtension) {
1149     try {
1150       return getMIMEService().getFromTypeAndExtension(aMIMEType, aExtension);
1151     } catch (e) {}
1152   }
1153   return null;
1156 function getDefaultFileName(
1157   aDefaultFileName,
1158   aURI,
1159   aDocument,
1160   aContentDisposition
1161 ) {
1162   // 1) look for a filename in the content-disposition header, if any
1163   if (aContentDisposition) {
1164     const mhpContractID = "@mozilla.org/network/mime-hdrparam;1";
1165     const mhpIID = Ci.nsIMIMEHeaderParam;
1166     const mhp = Cc[mhpContractID].getService(mhpIID);
1167     var dummy = { value: null }; // Need an out param...
1168     var charset = getCharsetforSave(aDocument);
1170     var fileName = null;
1171     try {
1172       fileName = mhp.getParameter(
1173         aContentDisposition,
1174         "filename",
1175         charset,
1176         true,
1177         dummy
1178       );
1179     } catch (e) {
1180       try {
1181         fileName = mhp.getParameter(
1182           aContentDisposition,
1183           "name",
1184           charset,
1185           true,
1186           dummy
1187         );
1188       } catch (e) {}
1189     }
1190     if (fileName) {
1191       return validateFileName(fileName);
1192     }
1193   }
1195   let docTitle;
1196   if (aDocument && aDocument.title && aDocument.title.trim()) {
1197     // If the document looks like HTML or XML, try to use its original title.
1198     docTitle = validateFileName(aDocument.title);
1199     let contentType = aDocument.contentType;
1200     if (
1201       contentType == "application/xhtml+xml" ||
1202       contentType == "application/xml" ||
1203       contentType == "image/svg+xml" ||
1204       contentType == "text/html" ||
1205       contentType == "text/xml"
1206     ) {
1207       // 2) Use the document title
1208       return docTitle;
1209     }
1210   }
1212   try {
1213     var url = aURI.QueryInterface(Ci.nsIURL);
1214     if (url.fileName != "") {
1215       // 3) Use the actual file name, if present
1216       return validateFileName(
1217         Services.textToSubURI.unEscapeURIForUI("UTF-8", url.fileName)
1218       );
1219     }
1220   } catch (e) {
1221     // This is something like a data: and so forth URI... no filename here.
1222   }
1224   if (docTitle) {
1225     // 4) Use the document title
1226     return docTitle;
1227   }
1229   if (aDefaultFileName) {
1230     // 5) Use the caller-provided name, if any
1231     return validateFileName(aDefaultFileName);
1232   }
1234   // 6) If this is a directory, use the last directory name
1235   var path = aURI.pathQueryRef.match(/\/([^\/]+)\/$/);
1236   if (path && path.length > 1) {
1237     return validateFileName(path[1]);
1238   }
1240   try {
1241     if (aURI.host) {
1242       // 7) Use the host.
1243       return validateFileName(aURI.host);
1244     }
1245   } catch (e) {
1246     // Some files have no information at all, like Javascript generated pages
1247   }
1248   try {
1249     // 8) Use the default file name
1250     return ContentAreaUtils.stringBundle.GetStringFromName(
1251       "DefaultSaveFileName"
1252     );
1253   } catch (e) {
1254     // in case localized string cannot be found
1255   }
1256   // 9) If all else fails, use "index"
1257   return "index";
1260 function validateFileName(aFileName) {
1261   let processed = DownloadPaths.sanitize(aFileName) || "_";
1262   if (AppConstants.platform == "android") {
1263     // If a large part of the filename has been sanitized, then we
1264     // will use a default filename instead
1265     if (processed.replace(/_/g, "").length <= processed.length / 2) {
1266       // We purposefully do not use a localized default filename,
1267       // which we could have done using
1268       // ContentAreaUtils.stringBundle.GetStringFromName("DefaultSaveFileName")
1269       // since it may contain invalid characters.
1270       var original = processed;
1271       processed = "download";
1273       // Preserve a suffix, if there is one
1274       if (original.includes(".")) {
1275         var suffix = original.split(".").slice(-1)[0];
1276         if (suffix && !suffix.includes("_")) {
1277           processed += "." + suffix;
1278         }
1279       }
1280     }
1281   }
1282   return processed;
1285 function getNormalizedLeafName(aFile, aDefaultExtension) {
1286   if (!aDefaultExtension) {
1287     return aFile;
1288   }
1290   if (AppConstants.platform == "win") {
1291     // Remove trailing dots and spaces on windows
1292     aFile = aFile.replace(/[\s.]+$/, "");
1293   }
1295   // Remove leading dots
1296   aFile = aFile.replace(/^\.+/, "");
1298   // Fix up the file name we're saving to to include the default extension
1299   var i = aFile.lastIndexOf(".");
1300   if (aFile.substr(i + 1) != aDefaultExtension) {
1301     return aFile + "." + aDefaultExtension;
1302   }
1304   return aFile;
1307 function getDefaultExtension(aFilename, aURI, aContentType) {
1308   if (
1309     aContentType == "text/plain" ||
1310     aContentType == "application/octet-stream" ||
1311     aURI.scheme == "ftp"
1312   ) {
1313     return "";
1314   } // temporary fix for bug 120327
1316   // First try the extension from the filename
1317   var url = Cc["@mozilla.org/network/standard-url-mutator;1"]
1318     .createInstance(Ci.nsIURIMutator)
1319     .setSpec("http://example.com") // construct the URL
1320     .setFilePath(aFilename)
1321     .finalize()
1322     .QueryInterface(Ci.nsIURL);
1324   var ext = url.fileExtension;
1326   // This mirrors some code in nsExternalHelperAppService::DoContent
1327   // Use the filename first and then the URI if that fails
1329   var mimeInfo = getMIMEInfoForType(aContentType, ext);
1331   if (ext && mimeInfo && mimeInfo.extensionExists(ext)) {
1332     return ext;
1333   }
1335   // Well, that failed.  Now try the extension from the URI
1336   var urlext;
1337   try {
1338     url = aURI.QueryInterface(Ci.nsIURL);
1339     urlext = url.fileExtension;
1340   } catch (e) {}
1342   if (urlext && mimeInfo && mimeInfo.extensionExists(urlext)) {
1343     return urlext;
1344   }
1345   try {
1346     if (mimeInfo) {
1347       return mimeInfo.primaryExtension;
1348     }
1349   } catch (e) {}
1350   // Fall back on the extensions in the filename and URI for lack
1351   // of anything better.
1352   return ext || urlext;
1355 function GetSaveModeForContentType(aContentType, aDocument) {
1356   // We can only save a complete page if we have a loaded document,
1357   // and it's not a CPOW -- nsWebBrowserPersist needs a real document.
1358   if (!aDocument || Cu.isCrossProcessWrapper(aDocument)) {
1359     return SAVEMODE_FILEONLY;
1360   }
1362   // Find the possible save modes using the provided content type
1363   var saveMode = SAVEMODE_FILEONLY;
1364   switch (aContentType) {
1365     case "text/html":
1366     case "application/xhtml+xml":
1367     case "image/svg+xml":
1368       saveMode |= SAVEMODE_COMPLETE_TEXT;
1369     // Fall through
1370     case "text/xml":
1371     case "application/xml":
1372       saveMode |= SAVEMODE_COMPLETE_DOM;
1373       break;
1374   }
1376   return saveMode;
1379 function getCharsetforSave(aDocument) {
1380   if (aDocument) {
1381     return aDocument.characterSet;
1382   }
1384   if (document.commandDispatcher.focusedWindow) {
1385     return document.commandDispatcher.focusedWindow.document.characterSet;
1386   }
1388   return window.content.document.characterSet;
1392  * Open a URL from chrome, determining if we can handle it internally or need to
1393  *  launch an external application to handle it.
1394  * @param aURL The URL to be opened
1396  * WARNING: Please note that openURL() does not perform any content security checks!!!
1397  */
1398 function openURL(aURL) {
1399   var uri = aURL instanceof Ci.nsIURI ? aURL : makeURI(aURL);
1401   var protocolSvc = Cc[
1402     "@mozilla.org/uriloader/external-protocol-service;1"
1403   ].getService(Ci.nsIExternalProtocolService);
1405   if (!protocolSvc.isExposedProtocol(uri.scheme)) {
1406     // If we're not a browser, use the external protocol service to load the URI.
1407     protocolSvc.loadURI(uri);
1408   } else {
1409     var recentWindow = Services.wm.getMostRecentWindow("navigator:browser");
1410     if (recentWindow) {
1411       recentWindow.openWebLinkIn(uri.spec, "tab", {
1412         triggeringPrincipal: recentWindow.document.contentPrincipal,
1413       });
1414       return;
1415     }
1417     var loadgroup = Cc["@mozilla.org/network/load-group;1"].createInstance(
1418       Ci.nsILoadGroup
1419     );
1420     var appstartup = Services.startup;
1422     var loadListener = {
1423       onStartRequest: function ll_start(aRequest) {
1424         appstartup.enterLastWindowClosingSurvivalArea();
1425       },
1426       onStopRequest: function ll_stop(aRequest, aStatusCode) {
1427         appstartup.exitLastWindowClosingSurvivalArea();
1428       },
1429       QueryInterface: ChromeUtils.generateQI([
1430         "nsIRequestObserver",
1431         "nsISupportsWeakReference",
1432       ]),
1433     };
1434     loadgroup.groupObserver = loadListener;
1436     var uriListener = {
1437       onStartURIOpen(uri) {
1438         return false;
1439       },
1440       doContent(ctype, preferred, request, handler) {
1441         return false;
1442       },
1443       isPreferred(ctype, desired) {
1444         return false;
1445       },
1446       canHandleContent(ctype, preferred, desired) {
1447         return false;
1448       },
1449       loadCookie: null,
1450       parentContentListener: null,
1451       getInterface(iid) {
1452         if (iid.equals(Ci.nsIURIContentListener)) {
1453           return this;
1454         }
1455         if (iid.equals(Ci.nsILoadGroup)) {
1456           return loadgroup;
1457         }
1458         throw Cr.NS_ERROR_NO_INTERFACE;
1459       },
1460     };
1462     var channel = NetUtil.newChannel({
1463       uri,
1464       loadUsingSystemPrincipal: true,
1465     });
1467     if (channel) {
1468       channel.channelIsForDownload = true;
1469     }
1471     var uriLoader = Cc["@mozilla.org/uriloader;1"].getService(Ci.nsIURILoader);
1472     uriLoader.openURI(
1473       channel,
1474       Ci.nsIURILoader.IS_CONTENT_PREFERRED,
1475       uriListener
1476     );
1477   }