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"
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",
25 var ContentAreaUtils = {
27 delete this.stringBundle;
28 return (this.stringBundle = Services.strings.createBundle(
29 "chrome://global/locale/contentAreaCommands.properties"
34 function urlSecurityCheck(aURL, aPrincipal, aFlags) {
35 return BrowserUtils.urlSecurityCheck(aURL, aPrincipal, aFlags);
38 function forbidCPOW(arg, func, argname) {
41 (typeof arg == "object" || typeof arg == "function") &&
42 Cu.isCrossProcessWrapper(arg)
44 throw new Error(`no CPOWs allowed for argument ${argname} to ${func}`);
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
63 // - A linked document using Save Link As...
64 // - A linked document using Alt-click Save Link As...
74 aIsContentWindowPrivate,
77 forbidCPOW(aURL, "saveURL", "aURL");
78 forbidCPOW(aReferrerInfo, "saveURL", "aReferrerInfo");
79 // Allow aSourceDocument to be a CPOW.
94 aIsContentWindowPrivate,
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
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.
136 function saveImageURL(
146 aIsContentWindowPrivate,
149 forbidCPOW(aURL, "saveImageURL", "aURL");
150 forbidCPOW(aReferrerInfo, "saveImageURL", "aReferrerInfo");
152 if (aDoc && aIsContentWindowPrivate == undefined) {
153 if (Cu.isCrossProcessWrapper(aDoc)) {
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"
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.
164 "saveImageURL should be passed the private state of " +
165 "the containing window.",
166 "https://bugzilla.mozilla.org/show_bug.cgi?id=1243643"
168 aIsContentWindowPrivate = PrivateBrowsingUtils.isContentWindowPrivate(
173 // We'd better have the private state by now.
174 if (aIsContentWindowPrivate == undefined) {
176 "saveImageURL couldn't compute private state of content window"
181 !aShouldBypassCache &&
182 (aDoc && !Cu.isCrossProcessWrapper(aDoc)) &&
183 (!aContentType && !aContentDisp)
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)),
194 aContentType = props.get("type", nsISupportsCString);
195 aContentDisp = props.get("content-disposition", nsISupportsCString);
198 // Failure to get type and content-disposition off the image is non-fatal
215 aIsContentWindowPrivate,
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) {
225 throw new Error("Must have a browser when calling saveBrowser");
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:
231 aBrowser.contentPrincipal.URI &&
232 aBrowser.contentPrincipal.URI.spec == "resource://pdf.js/web/viewer.html" &&
233 aBrowser.currentURI.schemeIs("file")
235 let correctPrincipal = Services.scriptSecurityManager.createContentPrincipal(
237 aBrowser.contentPrincipal.originAttributes
240 aBrowser.currentURI.spec,
241 null /* no document */,
242 null /* automatically determine filename */,
243 null /* no content disposition */,
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),
257 let stack = Components.stack.caller;
258 persistable.startPersistence(aOuterWindowID, {
259 onDocumentReady(document) {
260 saveDocument(document, aSkipPrompt);
263 throw new Components.Exception(
264 "saveBrowser failed asynchronously in startPersistence",
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) {
281 throw new Error("Must have a document when calling saveDocument");
284 let contentDisposition = null;
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;
297 contentDisposition = win.windowUtils.getDocumentMetadata(
298 "content-disposition"
301 // Failure to get a content-disposition is ok
305 let shEntry = win.docShell
306 .QueryInterface(Ci.nsIWebPageDescriptor)
307 .currentDescriptor.QueryInterface(Ci.nsISHEntry);
309 cacheKey = shEntry.cacheKey;
311 // We might not find it in the cache. Oh, well.
316 aDocument.documentURI,
320 aDocument.contentType,
324 aDocument.referrerInfo,
331 function DownloadListener(win, transfer) {
332 function makeClosure(name) {
334 transfer[name].apply(transfer, arguments);
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);
348 DownloadListener.prototype = {
349 QueryInterface: ChromeUtils.generateQI([
350 "nsIInterfaceRequestor",
351 "nsIWebProgressListener",
352 "nsIWebProgressListener2",
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(
360 return ww.getPrompt(this.window, aIID);
363 throw Cr.NS_ERROR_NO_INTERFACE;
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.
398 * The String representation of the URL of the document being saved
400 * The document to be saved
401 * @param aDefaultFileName
402 * The caller-provided suggested filename if we don't
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
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
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.
436 function internalSave(
449 aIsContentWindowPrivate,
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) {
461 if (aCacheKey == undefined) {
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.
472 file = aChosenData.file;
473 sourceURI = aChosenData.uri;
474 saveAsType = kSaveAsType_Complete;
480 charset = aDocument.characterSet;
482 var fileInfo = new FileInfo(aDefaultFileName);
491 sourceURI = fileInfo.uri;
494 fpTitleKey: aFilePickerTitleKey,
496 contentType: aContentType,
498 saveAsType: kSaveAsType_Complete,
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) {
511 saveAsType = fpParams.saveAsType;
512 file = fpParams.file;
516 .catch(Cu.reportError);
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 =
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) {
536 aInitiatingDocument.nodeType == 9 /* DOCUMENT_NODE */
537 ? PrivateBrowsingUtils.isContentWindowPrivate(
538 aInitiatingDocument.defaultView
540 : aInitiatingDocument.isPrivate;
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 =
548 (aDocument && (aDocument.nodePrincipal || aDocument.principal)) ||
549 (aInitiatingDocument && aInitiatingDocument.nodePrincipal);
554 sourceReferrerInfo: aReferrerInfo,
555 sourceDocument: useSaveDocument ? aDocument : null,
556 targetContentType: saveAsType == kSaveAsType_Text ? "text/plain" : null,
558 sourceCacheKey: aCacheKey,
559 sourcePostData: nonCPOWDocument ? getPostData(aDocument) : null,
560 bypassCache: aShouldBypassCache,
564 // Start the actual save process
565 internalPersist(persistArgs);
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.
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;
608 persist.persistFlags = flags | nsIWBP.PERSIST_FLAGS_FROM_CACHE;
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);
620 persistArgs.sourceURI,
627 persistArgs.isPrivate
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(
641 [nameWithoutExtension]
644 filesFolder.leafName = filesFolderLeafName;
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;
653 encodingFlags |= nsIWBP.ENCODE_FLAGS_ENCODE_BASIC_ENTITIES;
656 const kWrapColumn = 80;
657 persist.saveDocument(
658 persistArgs.sourceDocument,
661 persistArgs.targetContentType,
666 persist.savePrivacyAwareURI(
667 persistArgs.sourceURI,
668 persistArgs.sourcePrincipal,
669 persistArgs.sourceCacheKey,
670 persistArgs.sourceReferrerInfo,
671 persistArgs.sourcePostData,
674 persistArgs.isPrivate
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
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
709 this.suggestedFileName = aSuggestedFileName;
710 this.fileName = aFileName;
711 this.fileBaseName = aFileBaseName;
712 this.fileExt = aFileExt;
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.
729 function initFileInfo(
738 // Get an nsIURI object from aURL if possible:
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;
747 // Get the default filename:
748 aFI.fileName = getDefaultFileName(
749 aFI.suggestedFileName || aFI.fileName,
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 .
762 /^http(s?):\/\//i.test(aURL)
765 aFI.fileBaseName = aFI.fileName;
767 aFI.fileExt = getDefaultExtension(aFI.fileName, aFI.uri, aContentType);
768 aFI.fileBaseName = getFileBaseName(aFI.fileName);
774 * Given the Filepicker Parameters (aFpP), show the file picker dialog,
775 * prompting the user to confirm (or change) the fileName.
777 * A structure (see definition in internalSave(...) method)
778 * containing all the data used within this method.
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.
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.
791 * @resolve a boolean. When true, it indicates that the file picker dialog
794 function promiseTargetFile(
796 /* optional */ aSkipPrompt,
797 /* optional */ aRelatedURI
799 return (async function() {
800 let downloadLastDir = new DownloadLastDir(window);
801 let prefBranch = Services.prefs.getBranch("browser.download.");
802 let useDownloadDir = prefBranch.getBoolPref("useDownloadDir");
805 useDownloadDir = false;
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) {
816 getNormalizedLeafName(aFpP.fileInfo.fileName, aFpP.fileInfo.fileExt)
818 aFpP.file = uniqueFile(dir);
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() {
831 downloadLastDir.getFileAsync(aRelatedURI, function getFileAsyncCB(
838 if (file && (await OS.File.exists(file.path))) {
844 // Default to desktop.
845 dir = Services.dirsvc.get("Desk", Ci.nsIFile);
848 let fp = makeFilePicker();
849 let titleKey = aFpP.fpTitleKey || "SaveLinkTitle";
852 ContentAreaUtils.stringBundle.GetStringFromName(titleKey),
853 Ci.nsIFilePicker.modeSave
856 fp.displayDirectory = dir;
857 fp.defaultExtension = aFpP.fileInfo.fileExt;
858 fp.defaultString = getNormalizedLeafName(
859 aFpP.fileInfo.fileName,
860 aFpP.fileInfo.fileExt
862 appendFiltersForContentType(
865 aFpP.fileInfo.fileExt,
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
874 fp.filterIndex = prefBranch.getIntPref("save_converter_index");
878 let result = await new Promise(resolve => {
879 fp.open(function(aResult) {
883 if (result == Ci.nsIFilePicker.returnCancel || !fp.file) {
887 if (aFpP.saveMode != SAVEMODE_FILEONLY) {
888 prefBranch.setIntPref("save_converter_index", fp.filterIndex);
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;
898 aFpP.fileURL = fp.fileURL;
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()) {
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,
924 aLocalFile.leafName = aLocalFile.leafName.replace(
930 // replace the last (n) in the filename with (n+1)
931 aLocalFile.leafName = aLocalFile.leafName.replace(
933 "$1" + (collisionCount + 1) + ")"
941 * Download a URL using the Downloads API.
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.
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(
955 ).usePrivateBrowsing;
957 let fileInfo = new FileInfo(aFileName);
958 initFileInfo(fileInfo, aURL, null, null, null, null);
960 let filepickerParams = {
962 saveMode: SAVEMODE_FILEONLY,
966 let accepted = await promiseTargetFile(
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" },
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);
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(
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(
1015 // The bundle name for saving only a specific content type.
1017 // The corresponding filter string for a specific content type.
1020 // Every case where GetSaveModeForContentType can return non-FILEONLY
1021 // modes must be handled here.
1022 if (aSaveMode != SAVEMODE_FILEONLY) {
1023 switch (aContentType) {
1025 bundleName = "WebPageHTMLOnlyFilter";
1026 filterString = "*.htm; *.html";
1029 case "application/xhtml+xml":
1030 bundleName = "WebPageXHTMLOnlyFilter";
1031 filterString = "*.xht; *.xhtml";
1034 case "image/svg+xml":
1035 bundleName = "WebPageSVGOnlyFilter";
1036 filterString = "*.svg; *.svgz";
1040 case "application/xml":
1041 bundleName = "WebPageXMLOnlyFilter";
1042 filterString = "*.xml";
1048 if (aSaveMode != SAVEMODE_FILEONLY) {
1049 throw new Error(`Invalid save mode for type '${aContentType}'`);
1052 var mimeInfo = getMIMEInfoForType(aContentType, aFileExtension);
1055 for (var extension of mimeInfo.getFileExtensions()) {
1058 } // If adding more than one extension,
1059 // separate by semi-colon
1060 extString += "*." + extension;
1064 aFilePicker.appendFilter(mimeInfo.description, extString);
1069 if (aSaveMode & SAVEMODE_COMPLETE_DOM) {
1070 aFilePicker.appendFilter(
1071 ContentAreaUtils.stringBundle.GetStringFromName("WebPageCompleteFilter"),
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),
1082 if (aSaveMode & SAVEMODE_COMPLETE_TEXT) {
1083 aFilePicker.appendFilters(Ci.nsIFilePicker.filterText);
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;
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;
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);
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) {
1142 return getMIMEService().getTypeFromURI(aURI);
1147 function getMIMEInfoForType(aMIMEType, aExtension) {
1148 if (aMIMEType || aExtension) {
1150 return getMIMEService().getFromTypeAndExtension(aMIMEType, aExtension);
1156 function getDefaultFileName(
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;
1172 fileName = mhp.getParameter(
1173 aContentDisposition,
1181 fileName = mhp.getParameter(
1182 aContentDisposition,
1191 return validateFileName(fileName);
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;
1201 contentType == "application/xhtml+xml" ||
1202 contentType == "application/xml" ||
1203 contentType == "image/svg+xml" ||
1204 contentType == "text/html" ||
1205 contentType == "text/xml"
1207 // 2) Use the document title
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)
1221 // This is something like a data: and so forth URI... no filename here.
1225 // 4) Use the document title
1229 if (aDefaultFileName) {
1230 // 5) Use the caller-provided name, if any
1231 return validateFileName(aDefaultFileName);
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]);
1243 return validateFileName(aURI.host);
1246 // Some files have no information at all, like Javascript generated pages
1249 // 8) Use the default file name
1250 return ContentAreaUtils.stringBundle.GetStringFromName(
1251 "DefaultSaveFileName"
1254 // in case localized string cannot be found
1256 // 9) If all else fails, use "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;
1285 function getNormalizedLeafName(aFile, aDefaultExtension) {
1286 if (!aDefaultExtension) {
1290 if (AppConstants.platform == "win") {
1291 // Remove trailing dots and spaces on windows
1292 aFile = aFile.replace(/[\s.]+$/, "");
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;
1307 function getDefaultExtension(aFilename, aURI, aContentType) {
1309 aContentType == "text/plain" ||
1310 aContentType == "application/octet-stream" ||
1311 aURI.scheme == "ftp"
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)
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)) {
1335 // Well, that failed. Now try the extension from the URI
1338 url = aURI.QueryInterface(Ci.nsIURL);
1339 urlext = url.fileExtension;
1342 if (urlext && mimeInfo && mimeInfo.extensionExists(urlext)) {
1347 return mimeInfo.primaryExtension;
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;
1362 // Find the possible save modes using the provided content type
1363 var saveMode = SAVEMODE_FILEONLY;
1364 switch (aContentType) {
1366 case "application/xhtml+xml":
1367 case "image/svg+xml":
1368 saveMode |= SAVEMODE_COMPLETE_TEXT;
1371 case "application/xml":
1372 saveMode |= SAVEMODE_COMPLETE_DOM;
1379 function getCharsetforSave(aDocument) {
1381 return aDocument.characterSet;
1384 if (document.commandDispatcher.focusedWindow) {
1385 return document.commandDispatcher.focusedWindow.document.characterSet;
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!!!
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);
1409 var recentWindow = Services.wm.getMostRecentWindow("navigator:browser");
1411 recentWindow.openWebLinkIn(uri.spec, "tab", {
1412 triggeringPrincipal: recentWindow.document.contentPrincipal,
1417 var loadgroup = Cc["@mozilla.org/network/load-group;1"].createInstance(
1420 var appstartup = Services.startup;
1422 var loadListener = {
1423 onStartRequest: function ll_start(aRequest) {
1424 appstartup.enterLastWindowClosingSurvivalArea();
1426 onStopRequest: function ll_stop(aRequest, aStatusCode) {
1427 appstartup.exitLastWindowClosingSurvivalArea();
1429 QueryInterface: ChromeUtils.generateQI([
1430 "nsIRequestObserver",
1431 "nsISupportsWeakReference",
1434 loadgroup.groupObserver = loadListener;
1437 onStartURIOpen(uri) {
1440 doContent(ctype, preferred, request, handler) {
1443 isPreferred(ctype, desired) {
1446 canHandleContent(ctype, preferred, desired) {
1450 parentContentListener: null,
1452 if (iid.equals(Ci.nsIURIContentListener)) {
1455 if (iid.equals(Ci.nsILoadGroup)) {
1458 throw Cr.NS_ERROR_NO_INTERFACE;
1462 var channel = NetUtil.newChannel({
1464 loadUsingSystemPrincipal: true,
1468 channel.channelIsForDownload = true;
1471 var uriLoader = Cc["@mozilla.org/uriloader;1"].getService(Ci.nsIURILoader);
1474 Ci.nsIURILoader.IS_CONTENT_PREFERRED,