toolkit/content/contentAreaUtils.js

Sat, 03 Jan 2015 20:18:00 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Sat, 03 Jan 2015 20:18:00 +0100
branch
TOR_BUG_3246
changeset 7
129ffea94266
permissions
-rw-r--r--

Conditionally enable double key logic according to:
private browsing mode or privacy.thirdparty.isolate preference and
implement in GetCookieStringCommon and FindCookie where it counts...
With some reservations of how to convince FindCookie users to test
condition and pass a nullptr when disabling double key logic.

     1 # -*- Mode: javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- 
     2 # This Source Code Form is subject to the terms of the Mozilla Public
     3 # License, v. 2.0. If a copy of the MPL was not distributed with this
     4 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
     6 Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
     8 XPCOMUtils.defineLazyModuleGetter(this, "BrowserUtils",
     9                                   "resource://gre/modules/BrowserUtils.jsm");
    10 XPCOMUtils.defineLazyModuleGetter(this, "Downloads",
    11                                   "resource://gre/modules/Downloads.jsm");
    12 XPCOMUtils.defineLazyModuleGetter(this, "DownloadLastDir",
    13                                   "resource://gre/modules/DownloadLastDir.jsm");
    14 XPCOMUtils.defineLazyModuleGetter(this, "FileUtils",
    15                                   "resource://gre/modules/FileUtils.jsm");
    16 XPCOMUtils.defineLazyModuleGetter(this, "OS",
    17                                   "resource://gre/modules/osfile.jsm");
    18 XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils",
    19                                   "resource://gre/modules/PrivateBrowsingUtils.jsm");
    20 XPCOMUtils.defineLazyModuleGetter(this, "Promise",
    21                                   "resource://gre/modules/Promise.jsm");
    22 XPCOMUtils.defineLazyModuleGetter(this, "Services",
    23                                   "resource://gre/modules/Services.jsm");
    24 XPCOMUtils.defineLazyModuleGetter(this, "Task",
    25                                   "resource://gre/modules/Task.jsm");
    26 var ContentAreaUtils = {
    28   // this is for backwards compatibility.
    29   get ioService() {
    30     return Services.io;
    31   },
    33   get stringBundle() {
    34     delete this.stringBundle;
    35     return this.stringBundle =
    36       Services.strings.createBundle("chrome://global/locale/contentAreaCommands.properties");
    37   }
    38 }
    40 function urlSecurityCheck(aURL, aPrincipal, aFlags)
    41 {
    42   return BrowserUtils.urlSecurityCheck(aURL, aPrincipal, aFlags);
    43 }
    45 /**
    46  * Determine whether or not a given focused DOMWindow is in the content area.
    47  **/
    48 function isContentFrame(aFocusedWindow)
    49 {
    50   if (!aFocusedWindow)
    51     return false;
    53   return (aFocusedWindow.top == window.content);
    54 }
    56 // Clientele: (Make sure you don't break any of these)
    57 //  - File    ->  Save Page/Frame As...
    58 //  - Context ->  Save Page/Frame As...
    59 //  - Context ->  Save Link As...
    60 //  - Alt-Click links in web pages
    61 //  - Alt-Click links in the UI
    62 //
    63 // Try saving each of these types:
    64 // - A complete webpage using File->Save Page As, and Context->Save Page As
    65 // - A webpage as HTML only using the above methods
    66 // - A webpage as Text only using the above methods
    67 // - An image with an extension (e.g. .jpg) in its file name, using
    68 //   Context->Save Image As...
    69 // - An image without an extension (e.g. a banner ad on cnn.com) using
    70 //   the above method.
    71 // - A linked document using Save Link As...
    72 // - A linked document using Alt-click Save Link As...
    73 //
    74 function saveURL(aURL, aFileName, aFilePickerTitleKey, aShouldBypassCache,
    75                  aSkipPrompt, aReferrer, aSourceDocument)
    76 {
    77   internalSave(aURL, null, aFileName, null, null, aShouldBypassCache,
    78                aFilePickerTitleKey, null, aReferrer, aSourceDocument,
    79                aSkipPrompt, null);
    80 }
    82 // Just like saveURL, but will get some info off the image before
    83 // calling internalSave
    84 // Clientele: (Make sure you don't break any of these)
    85 //  - Context ->  Save Image As...
    86 const imgICache = Components.interfaces.imgICache;
    87 const nsISupportsCString = Components.interfaces.nsISupportsCString;
    89 function saveImageURL(aURL, aFileName, aFilePickerTitleKey, aShouldBypassCache,
    90                       aSkipPrompt, aReferrer, aDoc)
    91 {
    92   var contentType = null;
    93   var contentDisposition = null;
    94   if (!aShouldBypassCache) {
    95     try {
    96       var imageCache = Components.classes["@mozilla.org/image/tools;1"]
    97                                  .getService(Components.interfaces.imgITools)
    98                                  .getImgCacheForDocument(aDoc);
    99       var props =
   100         imageCache.findEntryProperties(makeURI(aURL, getCharsetforSave(null)));
   101       if (props) {
   102         contentType = props.get("type", nsISupportsCString);
   103         contentDisposition = props.get("content-disposition",
   104                                        nsISupportsCString);
   105       }
   106     } catch (e) {
   107       // Failure to get type and content-disposition off the image is non-fatal
   108     }
   109   }
   110   internalSave(aURL, null, aFileName, contentDisposition, contentType,
   111                aShouldBypassCache, aFilePickerTitleKey, null, aReferrer,
   112                aDoc, aSkipPrompt, null);
   113 }
   115 function saveDocument(aDocument, aSkipPrompt)
   116 {
   117   if (!aDocument)
   118     throw "Must have a document when calling saveDocument";
   120   // We want to use cached data because the document is currently visible.
   121   var ifreq =
   122     aDocument.defaultView
   123              .QueryInterface(Components.interfaces.nsIInterfaceRequestor);
   125   var contentDisposition = null;
   126   try {
   127     contentDisposition =
   128       ifreq.getInterface(Components.interfaces.nsIDOMWindowUtils)
   129            .getDocumentMetadata("content-disposition");
   130   } catch (ex) {
   131     // Failure to get a content-disposition is ok
   132   }
   134   var cacheKey = null;
   135   try {
   136     cacheKey =
   137       ifreq.getInterface(Components.interfaces.nsIWebNavigation)
   138            .QueryInterface(Components.interfaces.nsIWebPageDescriptor);
   139   } catch (ex) {
   140     // We might not find it in the cache.  Oh, well.
   141   }
   143   internalSave(aDocument.location.href, aDocument, null, contentDisposition,
   144                aDocument.contentType, false, null, null,
   145                aDocument.referrer ? makeURI(aDocument.referrer) : null,
   146                aDocument, aSkipPrompt, cacheKey);
   147 }
   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 }
   165 DownloadListener.prototype = {
   166   QueryInterface: function dl_qi(aIID)
   167   {
   168     if (aIID.equals(Components.interfaces.nsIInterfaceRequestor) ||
   169         aIID.equals(Components.interfaces.nsIWebProgressListener) ||
   170         aIID.equals(Components.interfaces.nsIWebProgressListener2) ||
   171         aIID.equals(Components.interfaces.nsISupports)) {
   172       return this;
   173     }
   174     throw Components.results.NS_ERROR_NO_INTERFACE;
   175   },
   177   getInterface: function dl_gi(aIID)
   178   {
   179     if (aIID.equals(Components.interfaces.nsIAuthPrompt) ||
   180         aIID.equals(Components.interfaces.nsIAuthPrompt2)) {
   181       var ww =
   182         Components.classes["@mozilla.org/embedcomp/window-watcher;1"]
   183                   .getService(Components.interfaces.nsIPromptFactory);
   184       return ww.getPrompt(this.window, aIID);
   185     }
   187     throw Components.results.NS_ERROR_NO_INTERFACE;
   188   }
   189 }
   191 const kSaveAsType_Complete = 0; // Save document with attached objects.
   192 // const kSaveAsType_URL      = 1; // Save document or URL by itself.
   193 const kSaveAsType_Text     = 2; // Save document, converting to plain text.
   195 /**
   196  * internalSave: Used when saving a document or URL.
   197  *
   198  * If aChosenData is null, this method:
   199  *  - Determines a local target filename to use
   200  *  - Prompts the user to confirm the destination filename and save mode
   201  *    (aContentType affects this)
   202  *  - [Note] This process involves the parameters aURL, aReferrer (to determine
   203  *    how aURL was encoded), aDocument, aDefaultFileName, aFilePickerTitleKey,
   204  *    and aSkipPrompt.
   205  *
   206  * If aChosenData is non-null, this method:
   207  *  - Uses the provided source URI and save file name
   208  *  - Saves the document as complete DOM if possible (aDocument present and
   209  *    right aContentType)
   210  *  - [Note] The parameters aURL, aDefaultFileName, aFilePickerTitleKey, and
   211  *    aSkipPrompt are ignored.
   212  *
   213  * In any case, this method:
   214  *  - Creates a 'Persist' object (which will perform the saving in the
   215  *    background) and then starts it.
   216  *  - [Note] This part of the process only involves the parameters aDocument,
   217  *    aShouldBypassCache and aReferrer. The source, the save name and the save
   218  *    mode are the ones determined previously.
   219  *
   220  * @param aURL
   221  *        The String representation of the URL of the document being saved
   222  * @param aDocument
   223  *        The document to be saved
   224  * @param aDefaultFileName
   225  *        The caller-provided suggested filename if we don't 
   226  *        find a better one
   227  * @param aContentDisposition
   228  *        The caller-provided content-disposition header to use.
   229  * @param aContentType
   230  *        The caller-provided content-type to use
   231  * @param aShouldBypassCache
   232  *        If true, the document will always be refetched from the server
   233  * @param aFilePickerTitleKey
   234  *        Alternate title for the file picker
   235  * @param aChosenData
   236  *        If non-null this contains an instance of object AutoChosen (see below)
   237  *        which holds pre-determined data so that the user does not need to be
   238  *        prompted for a target filename.
   239  * @param aReferrer
   240  *        the referrer URI object (not URL string) to use, or null
   241  *        if no referrer should be sent.
   242  * @param aInitiatingDocument
   243  *        The document from which the save was initiated.
   244  * @param aSkipPrompt [optional]
   245  *        If set to true, we will attempt to save the file to the
   246  *        default downloads folder without prompting.
   247  * @param aCacheKey [optional]
   248  *        If set will be passed to saveURI.  See nsIWebBrowserPersist for
   249  *        allowed values.
   250  */
   251 function internalSave(aURL, aDocument, aDefaultFileName, aContentDisposition,
   252                       aContentType, aShouldBypassCache, aFilePickerTitleKey,
   253                       aChosenData, aReferrer, aInitiatingDocument, aSkipPrompt,
   254                       aCacheKey)
   255 {
   256   if (aSkipPrompt == undefined)
   257     aSkipPrompt = false;
   259   if (aCacheKey == undefined)
   260     aCacheKey = null;
   262   // Note: aDocument == null when this code is used by save-link-as...
   263   var saveMode = GetSaveModeForContentType(aContentType, aDocument);
   265   var file, sourceURI, saveAsType;
   266   // Find the URI object for aURL and the FileName/Extension to use when saving.
   267   // FileName/Extension will be ignored if aChosenData supplied.
   268   if (aChosenData) {
   269     file = aChosenData.file;
   270     sourceURI = aChosenData.uri;
   271     saveAsType = kSaveAsType_Complete;
   273     continueSave();
   274   } else {
   275     var charset = null;
   276     if (aDocument)
   277       charset = aDocument.characterSet;
   278     else if (aReferrer)
   279       charset = aReferrer.originCharset;
   280     var fileInfo = new FileInfo(aDefaultFileName);
   281     initFileInfo(fileInfo, aURL, charset, aDocument,
   282                  aContentType, aContentDisposition);
   283     sourceURI = fileInfo.uri;
   285     var fpParams = {
   286       fpTitleKey: aFilePickerTitleKey,
   287       fileInfo: fileInfo,
   288       contentType: aContentType,
   289       saveMode: saveMode,
   290       saveAsType: kSaveAsType_Complete,
   291       file: file
   292     };
   294     // Find a URI to use for determining last-downloaded-to directory
   295     let relatedURI = aReferrer || sourceURI;
   297     promiseTargetFile(fpParams, aSkipPrompt, relatedURI).then(aDialogAccepted => {
   298       if (!aDialogAccepted)
   299         return;
   301       saveAsType = fpParams.saveAsType;
   302       file = fpParams.file;
   304       continueSave();
   305     }).then(null, Components.utils.reportError);
   306   }
   308   function continueSave() {
   309     // XXX We depend on the following holding true in appendFiltersForContentType():
   310     // If we should save as a complete page, the saveAsType is kSaveAsType_Complete.
   311     // If we should save as text, the saveAsType is kSaveAsType_Text.
   312     var useSaveDocument = aDocument &&
   313                           (((saveMode & SAVEMODE_COMPLETE_DOM) && (saveAsType == kSaveAsType_Complete)) ||
   314                            ((saveMode & SAVEMODE_COMPLETE_TEXT) && (saveAsType == kSaveAsType_Text)));
   315     // If we're saving a document, and are saving either in complete mode or
   316     // as converted text, pass the document to the web browser persist component.
   317     // If we're just saving the HTML (second option in the list), send only the URI.
   318     var persistArgs = {
   319       sourceURI         : sourceURI,
   320       sourceReferrer    : aReferrer,
   321       sourceDocument    : useSaveDocument ? aDocument : null,
   322       targetContentType : (saveAsType == kSaveAsType_Text) ? "text/plain" : null,
   323       targetFile        : file,
   324       sourceCacheKey    : aCacheKey,
   325       sourcePostData    : aDocument ? getPostData(aDocument) : null,
   326       bypassCache       : aShouldBypassCache,
   327       initiatingWindow  : aInitiatingDocument.defaultView
   328     };
   330     // Start the actual save process
   331     internalPersist(persistArgs);
   332   }
   333 }
   335 /**
   336  * internalPersist: Creates a 'Persist' object (which will perform the saving
   337  *  in the background) and then starts it.
   338  *
   339  * @param persistArgs.sourceURI
   340  *        The nsIURI of the document being saved
   341  * @param persistArgs.sourceCacheKey [optional]
   342  *        If set will be passed to saveURI
   343  * @param persistArgs.sourceDocument [optional]
   344  *        The document to be saved, or null if not saving a complete document
   345  * @param persistArgs.sourceReferrer
   346  *        Required and used only when persistArgs.sourceDocument is NOT present,
   347  *        the nsIURI of the referrer to use, or null if no referrer should be
   348  *        sent.
   349  * @param persistArgs.sourcePostData
   350  *        Required and used only when persistArgs.sourceDocument is NOT present,
   351  *        represents the POST data to be sent along with the HTTP request, and
   352  *        must be null if no POST data should be sent.
   353  * @param persistArgs.targetFile
   354  *        The nsIFile of the file to create
   355  * @param persistArgs.targetContentType
   356  *        Required and used only when persistArgs.sourceDocument is present,
   357  *        determines the final content type of the saved file, or null to use
   358  *        the same content type as the source document. Currently only
   359  *        "text/plain" is meaningful.
   360  * @param persistArgs.bypassCache
   361  *        If true, the document will always be refetched from the server
   362  * @param persistArgs.initiatingWindow
   363  *        The window from which the save operation was initiated.
   364  */
   365 function internalPersist(persistArgs)
   366 {
   367   var persist = makeWebBrowserPersist();
   369   // Calculate persist flags.
   370   const nsIWBP = Components.interfaces.nsIWebBrowserPersist;
   371   const flags = nsIWBP.PERSIST_FLAGS_REPLACE_EXISTING_FILES |
   372                 nsIWBP.PERSIST_FLAGS_FORCE_ALLOW_COOKIES;
   373   if (persistArgs.bypassCache)
   374     persist.persistFlags = flags | nsIWBP.PERSIST_FLAGS_BYPASS_CACHE;
   375   else
   376     persist.persistFlags = flags | nsIWBP.PERSIST_FLAGS_FROM_CACHE;
   378   // Leave it to WebBrowserPersist to discover the encoding type (or lack thereof):
   379   persist.persistFlags |= nsIWBP.PERSIST_FLAGS_AUTODETECT_APPLY_CONVERSION;
   381   // Find the URI associated with the target file
   382   var targetFileURL = makeFileURI(persistArgs.targetFile);
   384   var isPrivate = PrivateBrowsingUtils.isWindowPrivate(persistArgs.initiatingWindow);
   386   // Create download and initiate it (below)
   387   var tr = Components.classes["@mozilla.org/transfer;1"].createInstance(Components.interfaces.nsITransfer);
   388   tr.init(persistArgs.sourceURI,
   389           targetFileURL, "", null, null, null, persist, isPrivate);
   390   persist.progressListener = new DownloadListener(window, tr);
   392   if (persistArgs.sourceDocument) {
   393     // Saving a Document, not a URI:
   394     var filesFolder = null;
   395     if (persistArgs.targetContentType != "text/plain") {
   396       // Create the local directory into which to save associated files.
   397       filesFolder = persistArgs.targetFile.clone();
   399       var nameWithoutExtension = getFileBaseName(filesFolder.leafName);
   400       var filesFolderLeafName =
   401         ContentAreaUtils.stringBundle
   402                         .formatStringFromName("filesFolder", [nameWithoutExtension], 1);
   404       filesFolder.leafName = filesFolderLeafName;
   405     }
   407     var encodingFlags = 0;
   408     if (persistArgs.targetContentType == "text/plain") {
   409       encodingFlags |= nsIWBP.ENCODE_FLAGS_FORMATTED;
   410       encodingFlags |= nsIWBP.ENCODE_FLAGS_ABSOLUTE_LINKS;
   411       encodingFlags |= nsIWBP.ENCODE_FLAGS_NOFRAMES_CONTENT;
   412     }
   413     else {
   414       encodingFlags |= nsIWBP.ENCODE_FLAGS_ENCODE_BASIC_ENTITIES;
   415     }
   417     const kWrapColumn = 80;
   418     persist.saveDocument(persistArgs.sourceDocument, targetFileURL, filesFolder,
   419                          persistArgs.targetContentType, encodingFlags, kWrapColumn);
   420   } else {
   421     let privacyContext = persistArgs.initiatingWindow
   422                                     .QueryInterface(Components.interfaces.nsIInterfaceRequestor)
   423                                     .getInterface(Components.interfaces.nsIWebNavigation)
   424                                     .QueryInterface(Components.interfaces.nsILoadContext);
   425     persist.saveURI(persistArgs.sourceURI,
   426                     persistArgs.sourceCacheKey, persistArgs.sourceReferrer, persistArgs.sourcePostData, null,
   427                     targetFileURL, privacyContext);
   428   }
   429 }
   431 /**
   432  * Structure for holding info about automatically supplied parameters for
   433  * internalSave(...). This allows parameters to be supplied so the user does not
   434  * need to be prompted for file info.
   435  * @param aFileAutoChosen This is an nsIFile object that has been
   436  *        pre-determined as the filename for the target to save to
   437  * @param aUriAutoChosen  This is the nsIURI object for the target
   438  */
   439 function AutoChosen(aFileAutoChosen, aUriAutoChosen) {
   440   this.file = aFileAutoChosen;
   441   this.uri  = aUriAutoChosen;
   442 }
   444 /**
   445  * Structure for holding info about a URL and the target filename it should be
   446  * saved to. This structure is populated by initFileInfo(...).
   447  * @param aSuggestedFileName This is used by initFileInfo(...) when it
   448  *        cannot 'discover' the filename from the url 
   449  * @param aFileName The target filename
   450  * @param aFileBaseName The filename without the file extension
   451  * @param aFileExt The extension of the filename
   452  * @param aUri An nsIURI object for the url that is being saved
   453  */
   454 function FileInfo(aSuggestedFileName, aFileName, aFileBaseName, aFileExt, aUri) {
   455   this.suggestedFileName = aSuggestedFileName;
   456   this.fileName = aFileName;
   457   this.fileBaseName = aFileBaseName;
   458   this.fileExt = aFileExt;
   459   this.uri = aUri;
   460 }
   462 /**
   463  * Determine what the 'default' filename string is, its file extension and the
   464  * filename without the extension. This filename is used when prompting the user
   465  * for confirmation in the file picker dialog.
   466  * @param aFI A FileInfo structure into which we'll put the results of this method.
   467  * @param aURL The String representation of the URL of the document being saved
   468  * @param aURLCharset The charset of aURL.
   469  * @param aDocument The document to be saved
   470  * @param aContentType The content type we're saving, if it could be
   471  *        determined by the caller.
   472  * @param aContentDisposition The content-disposition header for the object
   473  *        we're saving, if it could be determined by the caller.
   474  */
   475 function initFileInfo(aFI, aURL, aURLCharset, aDocument,
   476                       aContentType, aContentDisposition)
   477 {
   478   try {
   479     // Get an nsIURI object from aURL if possible:
   480     try {
   481       aFI.uri = makeURI(aURL, aURLCharset);
   482       // Assuming nsiUri is valid, calling QueryInterface(...) on it will
   483       // populate extra object fields (eg filename and file extension).
   484       var url = aFI.uri.QueryInterface(Components.interfaces.nsIURL);
   485       aFI.fileExt = url.fileExtension;
   486     } catch (e) {
   487     }
   489     // Get the default filename:
   490     aFI.fileName = getDefaultFileName((aFI.suggestedFileName || aFI.fileName),
   491                                       aFI.uri, aDocument, aContentDisposition);
   492     // If aFI.fileExt is still blank, consider: aFI.suggestedFileName is supplied
   493     // if saveURL(...) was the original caller (hence both aContentType and
   494     // aDocument are blank). If they were saving a link to a website then make
   495     // the extension .htm .
   496     if (!aFI.fileExt && !aDocument && !aContentType && (/^http(s?):\/\//i.test(aURL))) {
   497       aFI.fileExt = "htm";
   498       aFI.fileBaseName = aFI.fileName;
   499     } else {
   500       aFI.fileExt = getDefaultExtension(aFI.fileName, aFI.uri, aContentType);
   501       aFI.fileBaseName = getFileBaseName(aFI.fileName);
   502     }
   503   } catch (e) {
   504   }
   505 }
   507 /** 
   508  * Given the Filepicker Parameters (aFpP), show the file picker dialog,
   509  * prompting the user to confirm (or change) the fileName.
   510  * @param aFpP
   511  *        A structure (see definition in internalSave(...) method)
   512  *        containing all the data used within this method.
   513  * @param aSkipPrompt
   514  *        If true, attempt to save the file automatically to the user's default
   515  *        download directory, thus skipping the explicit prompt for a file name,
   516  *        but only if the associated preference is set.
   517  *        If false, don't save the file automatically to the user's
   518  *        default download directory, even if the associated preference
   519  *        is set, but ask for the target explicitly.
   520  * @param aRelatedURI
   521  *        An nsIURI associated with the download. The last used
   522  *        directory of the picker is retrieved from/stored in the 
   523  *        Content Pref Service using this URI.
   524  * @return Promise
   525  * @resolve a boolean. When true, it indicates that the file picker dialog
   526  *          is accepted.
   527  */
   528 function promiseTargetFile(aFpP, /* optional */ aSkipPrompt, /* optional */ aRelatedURI)
   529 {
   530   return Task.spawn(function() {
   531     let downloadLastDir = new DownloadLastDir(window);
   532     let prefBranch = Services.prefs.getBranch("browser.download.");
   533     let useDownloadDir = prefBranch.getBoolPref("useDownloadDir");
   535     if (!aSkipPrompt)
   536       useDownloadDir = false;
   538     // Default to the user's default downloads directory configured
   539     // through download prefs.
   540     let dirPath = yield Downloads.getPreferredDownloadsDirectory();
   541     let dirExists = yield OS.File.exists(dirPath);
   542     let dir = new FileUtils.File(dirPath);
   544     if (useDownloadDir && dirExists) {
   545       dir.append(getNormalizedLeafName(aFpP.fileInfo.fileName,
   546                                        aFpP.fileInfo.fileExt));
   547       aFpP.file = uniqueFile(dir);
   548       throw new Task.Result(true);
   549     }
   551     // We must prompt for the file name explicitly.
   552     // If we must prompt because we were asked to...
   553     let deferred = Promise.defer();
   554     if (useDownloadDir) {
   555       // Keep async behavior in both branches
   556       Services.tm.mainThread.dispatch(function() {
   557         deferred.resolve(null);
   558       }, Components.interfaces.nsIThread.DISPATCH_NORMAL);
   559     } else {
   560       downloadLastDir.getFileAsync(aRelatedURI, function getFileAsyncCB(aFile) {
   561         deferred.resolve(aFile);
   562       });
   563     }
   564     let file = yield deferred.promise;
   565     if (file && (yield OS.File.exists(file.path))) {
   566       dir = file;
   567       dirExists = true;
   568     }
   570     if (!dirExists) {
   571       // Default to desktop.
   572       dir = Services.dirsvc.get("Desk", Components.interfaces.nsIFile);
   573     }
   575     let fp = makeFilePicker();
   576     let titleKey = aFpP.fpTitleKey || "SaveLinkTitle";
   577     fp.init(window, ContentAreaUtils.stringBundle.GetStringFromName(titleKey),
   578             Components.interfaces.nsIFilePicker.modeSave);
   580     fp.displayDirectory = dir;
   581     fp.defaultExtension = aFpP.fileInfo.fileExt;
   582     fp.defaultString = getNormalizedLeafName(aFpP.fileInfo.fileName,
   583                                              aFpP.fileInfo.fileExt);
   584     appendFiltersForContentType(fp, aFpP.contentType, aFpP.fileInfo.fileExt,
   585                                 aFpP.saveMode);
   587     // The index of the selected filter is only preserved and restored if there's
   588     // more than one filter in addition to "All Files".
   589     if (aFpP.saveMode != SAVEMODE_FILEONLY) {
   590       try {
   591         fp.filterIndex = prefBranch.getIntPref("save_converter_index");
   592       }
   593       catch (e) {
   594       }
   595     }
   597     let deferComplete = Promise.defer();
   598     fp.open(function(aResult) {
   599       deferComplete.resolve(aResult);
   600     });
   601     let result = yield deferComplete.promise;
   602     if (result == Components.interfaces.nsIFilePicker.returnCancel || !fp.file) {
   603       throw new Task.Result(false);
   604     }
   606     if (aFpP.saveMode != SAVEMODE_FILEONLY)
   607       prefBranch.setIntPref("save_converter_index", fp.filterIndex);
   609     // Do not store the last save directory as a pref inside the private browsing mode
   610     downloadLastDir.setFile(aRelatedURI, fp.file.parent);
   612     fp.file.leafName = validateFileName(fp.file.leafName);
   614     aFpP.saveAsType = fp.filterIndex;
   615     aFpP.file = fp.file;
   616     aFpP.fileURL = fp.fileURL;
   618     throw new Task.Result(true);
   619   });
   620 }
   622 // Since we're automatically downloading, we don't get the file picker's
   623 // logic to check for existing files, so we need to do that here.
   624 //
   625 // Note - this code is identical to that in
   626 //   mozilla/toolkit/mozapps/downloads/src/nsHelperAppDlg.js.in
   627 // If you are updating this code, update that code too! We can't share code
   628 // here since that code is called in a js component.
   629 function uniqueFile(aLocalFile)
   630 {
   631   var collisionCount = 0;
   632   while (aLocalFile.exists()) {
   633     collisionCount++;
   634     if (collisionCount == 1) {
   635       // Append "(2)" before the last dot in (or at the end of) the filename
   636       // special case .ext.gz etc files so we don't wind up with .tar(2).gz
   637       if (aLocalFile.leafName.match(/\.[^\.]{1,3}\.(gz|bz2|Z)$/i))
   638         aLocalFile.leafName = aLocalFile.leafName.replace(/\.[^\.]{1,3}\.(gz|bz2|Z)$/i, "(2)$&");
   639       else
   640         aLocalFile.leafName = aLocalFile.leafName.replace(/(\.[^\.]*)?$/, "(2)$&");
   641     }
   642     else {
   643       // replace the last (n) in the filename with (n+1)
   644       aLocalFile.leafName = aLocalFile.leafName.replace(/^(.*\()\d+\)/, "$1" + (collisionCount + 1) + ")");
   645     }
   646   }
   647   return aLocalFile;
   648 }
   650 #ifdef MOZ_JSDOWNLOADS
   651 /**
   652  * Download a URL using the new jsdownloads API.
   653  *
   654  * @param aURL
   655  *        the url to download
   656  * @param [optional] aFileName
   657  *        the destination file name, if omitted will be obtained from the url.
   658  * @param aInitiatingDocument
   659  *        The document from which the download was initiated.
   660  */
   661 function DownloadURL(aURL, aFileName, aInitiatingDocument) {
   662   // For private browsing, try to get document out of the most recent browser
   663   // window, or provide our own if there's no browser window.
   664   let isPrivate = aInitiatingDocument.defaultView
   665                                      .QueryInterface(Ci.nsIInterfaceRequestor)
   666                                      .getInterface(Ci.nsIWebNavigation)
   667                                      .QueryInterface(Ci.nsILoadContext)
   668                                      .usePrivateBrowsing;
   670   let fileInfo = new FileInfo(aFileName);
   671   initFileInfo(fileInfo, aURL, null, null, null, null);
   673   let filepickerParams = {
   674     fileInfo: fileInfo,
   675     saveMode: SAVEMODE_FILEONLY
   676   };
   678   Task.spawn(function* () {
   679     let accepted = yield promiseTargetFile(filepickerParams, true, fileInfo.uri);
   680     if (!accepted)
   681       return;
   683     let file = filepickerParams.file;
   684     let download = yield Downloads.createDownload({
   685       source: { url: aURL, isPrivate: isPrivate },
   686       target: { path: file.path, partFilePath: file.path + ".part" }
   687     });
   688     download.tryToKeepPartialData = true;
   689     download.start();
   691     // Add the download to the list, allowing it to be managed.
   692     let list = yield Downloads.getList(Downloads.ALL);
   693     list.add(download);
   694   }).then(null, Components.utils.reportError);
   695 }
   696 #endif
   698 // We have no DOM, and can only save the URL as is.
   699 const SAVEMODE_FILEONLY      = 0x00;
   700 // We have a DOM and can save as complete.
   701 const SAVEMODE_COMPLETE_DOM  = 0x01;
   702 // We have a DOM which we can serialize as text.
   703 const SAVEMODE_COMPLETE_TEXT = 0x02;
   705 // If we are able to save a complete DOM, the 'save as complete' filter
   706 // must be the first filter appended.  The 'save page only' counterpart
   707 // must be the second filter appended.  And the 'save as complete text'
   708 // filter must be the third filter appended.
   709 function appendFiltersForContentType(aFilePicker, aContentType, aFileExtension, aSaveMode)
   710 {
   711   // The bundle name for saving only a specific content type.
   712   var bundleName;
   713   // The corresponding filter string for a specific content type.
   714   var filterString;
   716   // XXX all the cases that are handled explicitly here MUST be handled
   717   // in GetSaveModeForContentType to return a non-fileonly filter.
   718   switch (aContentType) {
   719   case "text/html":
   720     bundleName   = "WebPageHTMLOnlyFilter";
   721     filterString = "*.htm; *.html";
   722     break;
   724   case "application/xhtml+xml":
   725     bundleName   = "WebPageXHTMLOnlyFilter";
   726     filterString = "*.xht; *.xhtml";
   727     break;
   729   case "image/svg+xml":
   730     bundleName   = "WebPageSVGOnlyFilter";
   731     filterString = "*.svg; *.svgz";
   732     break;
   734   case "text/xml":
   735   case "application/xml":
   736     bundleName   = "WebPageXMLOnlyFilter";
   737     filterString = "*.xml";
   738     break;
   740   default:
   741     if (aSaveMode != SAVEMODE_FILEONLY)
   742       throw "Invalid save mode for type '" + aContentType + "'";
   744     var mimeInfo = getMIMEInfoForType(aContentType, aFileExtension);
   745     if (mimeInfo) {
   747       var extEnumerator = mimeInfo.getFileExtensions();
   749       var extString = "";
   750       while (extEnumerator.hasMore()) {
   751         var extension = extEnumerator.getNext();
   752         if (extString)
   753           extString += "; ";    // If adding more than one extension,
   754                                 // separate by semi-colon
   755         extString += "*." + extension;
   756       }
   758       if (extString)
   759         aFilePicker.appendFilter(mimeInfo.description, extString);
   760     }
   762     break;
   763   }
   765   if (aSaveMode & SAVEMODE_COMPLETE_DOM) {
   766     aFilePicker.appendFilter(ContentAreaUtils.stringBundle.GetStringFromName("WebPageCompleteFilter"),
   767                              filterString);
   768     // We should always offer a choice to save document only if
   769     // we allow saving as complete.
   770     aFilePicker.appendFilter(ContentAreaUtils.stringBundle.GetStringFromName(bundleName),
   771                              filterString);
   772   }
   774   if (aSaveMode & SAVEMODE_COMPLETE_TEXT)
   775     aFilePicker.appendFilters(Components.interfaces.nsIFilePicker.filterText);
   777   // Always append the all files (*) filter
   778   aFilePicker.appendFilters(Components.interfaces.nsIFilePicker.filterAll);
   779 }
   781 function getPostData(aDocument)
   782 {
   783   try {
   784     // Find the session history entry corresponding to the given document. In
   785     // the current implementation, nsIWebPageDescriptor.currentDescriptor always
   786     // returns a session history entry.
   787     var sessionHistoryEntry =
   788         aDocument.defaultView
   789                  .QueryInterface(Components.interfaces.nsIInterfaceRequestor)
   790                  .getInterface(Components.interfaces.nsIWebNavigation)
   791                  .QueryInterface(Components.interfaces.nsIWebPageDescriptor)
   792                  .currentDescriptor
   793                  .QueryInterface(Components.interfaces.nsISHEntry);
   794     return sessionHistoryEntry.postData;
   795   }
   796   catch (e) {
   797   }
   798   return null;
   799 }
   801 function makeWebBrowserPersist()
   802 {
   803   const persistContractID = "@mozilla.org/embedding/browser/nsWebBrowserPersist;1";
   804   const persistIID = Components.interfaces.nsIWebBrowserPersist;
   805   return Components.classes[persistContractID].createInstance(persistIID);
   806 }
   808 function makeURI(aURL, aOriginCharset, aBaseURI)
   809 {
   810   return BrowserUtils.makeURI(aURL, aOriginCharset, aBaseURI);
   811 }
   813 function makeFileURI(aFile)
   814 {
   815   return BrowserUtils.makeFileURI(aFile);
   816 }
   818 function makeFilePicker()
   819 {
   820   const fpContractID = "@mozilla.org/filepicker;1";
   821   const fpIID = Components.interfaces.nsIFilePicker;
   822   return Components.classes[fpContractID].createInstance(fpIID);
   823 }
   825 function getMIMEService()
   826 {
   827   const mimeSvcContractID = "@mozilla.org/mime;1";
   828   const mimeSvcIID = Components.interfaces.nsIMIMEService;
   829   const mimeSvc = Components.classes[mimeSvcContractID].getService(mimeSvcIID);
   830   return mimeSvc;
   831 }
   833 // Given aFileName, find the fileName without the extension on the end.
   834 function getFileBaseName(aFileName)
   835 {
   836   // Remove the file extension from aFileName:
   837   return aFileName.replace(/\.[^.]*$/, "");
   838 }
   840 function getMIMETypeForURI(aURI)
   841 {
   842   try {
   843     return getMIMEService().getTypeFromURI(aURI);
   844   }
   845   catch (e) {
   846   }
   847   return null;
   848 }
   850 function getMIMEInfoForType(aMIMEType, aExtension)
   851 {
   852   if (aMIMEType || aExtension) {
   853     try {
   854       return getMIMEService().getFromTypeAndExtension(aMIMEType, aExtension);
   855     }
   856     catch (e) {
   857     }
   858   }
   859   return null;
   860 }
   862 function getDefaultFileName(aDefaultFileName, aURI, aDocument,
   863                             aContentDisposition)
   864 {
   865   // 1) look for a filename in the content-disposition header, if any
   866   if (aContentDisposition) {
   867     const mhpContractID = "@mozilla.org/network/mime-hdrparam;1";
   868     const mhpIID = Components.interfaces.nsIMIMEHeaderParam;
   869     const mhp = Components.classes[mhpContractID].getService(mhpIID);
   870     var dummy = { value: null };  // Need an out param...
   871     var charset = getCharsetforSave(aDocument);
   873     var fileName = null;
   874     try {
   875       fileName = mhp.getParameter(aContentDisposition, "filename", charset,
   876                                   true, dummy);
   877     }
   878     catch (e) {
   879       try {
   880         fileName = mhp.getParameter(aContentDisposition, "name", charset, true,
   881                                     dummy);
   882       }
   883       catch (e) {
   884       }
   885     }
   886     if (fileName)
   887       return fileName;
   888   }
   890   let docTitle;
   891   if (aDocument) {
   892     // If the document looks like HTML or XML, try to use its original title.
   893     docTitle = validateFileName(aDocument.title).trim();
   894     if (docTitle) {
   895       let contentType = aDocument.contentType;
   896       if (contentType == "application/xhtml+xml" ||
   897           contentType == "application/xml" ||
   898           contentType == "image/svg+xml" ||
   899           contentType == "text/html" ||
   900           contentType == "text/xml") {
   901         // 2) Use the document title
   902         return docTitle;
   903       }
   904     }
   905   }
   907   try {
   908     var url = aURI.QueryInterface(Components.interfaces.nsIURL);
   909     if (url.fileName != "") {
   910       // 3) Use the actual file name, if present
   911       var textToSubURI = Components.classes["@mozilla.org/intl/texttosuburi;1"]
   912                                    .getService(Components.interfaces.nsITextToSubURI);
   913       return validateFileName(textToSubURI.unEscapeURIForUI(url.originCharset || "UTF-8", url.fileName));
   914     }
   915   } catch (e) {
   916     // This is something like a data: and so forth URI... no filename here.
   917   }
   919   if (docTitle)
   920     // 4) Use the document title
   921     return docTitle;
   923   if (aDefaultFileName)
   924     // 5) Use the caller-provided name, if any
   925     return validateFileName(aDefaultFileName);
   927   // 6) If this is a directory, use the last directory name
   928   var path = aURI.path.match(/\/([^\/]+)\/$/);
   929   if (path && path.length > 1)
   930     return validateFileName(path[1]);
   932   try {
   933     if (aURI.host)
   934       // 7) Use the host.
   935       return aURI.host;
   936   } catch (e) {
   937     // Some files have no information at all, like Javascript generated pages
   938   }
   939   try {
   940     // 8) Use the default file name
   941     return ContentAreaUtils.stringBundle.GetStringFromName("DefaultSaveFileName");
   942   } catch (e) {
   943     //in case localized string cannot be found
   944   }
   945   // 9) If all else fails, use "index"
   946   return "index";
   947 }
   949 function validateFileName(aFileName)
   950 {
   951   var re = /[\/]+/g;
   952   if (navigator.appVersion.indexOf("Windows") != -1) {
   953     re = /[\\\/\|]+/g;
   954     aFileName = aFileName.replace(/[\"]+/g, "'");
   955     aFileName = aFileName.replace(/[\*\:\?]+/g, " ");
   956     aFileName = aFileName.replace(/[\<]+/g, "(");
   957     aFileName = aFileName.replace(/[\>]+/g, ")");
   958   }
   959   else if (navigator.appVersion.indexOf("Macintosh") != -1)
   960     re = /[\:\/]+/g;
   961   else if (navigator.appVersion.indexOf("Android") != -1) {
   962     // On mobile devices, the filesystem may be very limited in what
   963     // it considers valid characters. To avoid errors, we sanitize
   964     // conservatively.
   965     const dangerousChars = "*?<>|\":/\\[];,+=";
   966     var processed = "";
   967     for (var i = 0; i < aFileName.length; i++)
   968       processed += aFileName.charCodeAt(i) >= 32 &&
   969                    !(dangerousChars.indexOf(aFileName[i]) >= 0) ? aFileName[i]
   970                                                                 : "_";
   972     // Last character should not be a space
   973     processed = processed.trim();
   975     // If a large part of the filename has been sanitized, then we
   976     // will use a default filename instead
   977     if (processed.replace(/_/g, "").length <= processed.length/2) {
   978       // We purposefully do not use a localized default filename,
   979       // which we could have done using
   980       // ContentAreaUtils.stringBundle.GetStringFromName("DefaultSaveFileName")
   981       // since it may contain invalid characters.
   982       var original = processed;
   983       processed = "download";
   985       // Preserve a suffix, if there is one
   986       if (original.indexOf(".") >= 0) {
   987         var suffix = original.split(".").slice(-1)[0];
   988         if (suffix && suffix.indexOf("_") < 0)
   989           processed += "." + suffix;
   990       }
   991     }
   992     return processed;
   993   }
   995   return aFileName.replace(re, "_");
   996 }
   998 function getNormalizedLeafName(aFile, aDefaultExtension)
   999 {
  1000   if (!aDefaultExtension)
  1001     return aFile;
  1003 #ifdef XP_WIN
  1004   // Remove trailing dots and spaces on windows
  1005   aFile = aFile.replace(/[\s.]+$/, "");
  1006 #endif
  1008   // Remove leading dots
  1009   aFile = aFile.replace(/^\.+/, "");
  1011   // Fix up the file name we're saving to to include the default extension
  1012   var i = aFile.lastIndexOf(".");
  1013   if (aFile.substr(i + 1) != aDefaultExtension)
  1014     return aFile + "." + aDefaultExtension;
  1016   return aFile;
  1019 function getDefaultExtension(aFilename, aURI, aContentType)
  1021   if (aContentType == "text/plain" || aContentType == "application/octet-stream" || aURI.scheme == "ftp")
  1022     return "";   // temporary fix for bug 120327
  1024   // First try the extension from the filename
  1025   const stdURLContractID = "@mozilla.org/network/standard-url;1";
  1026   const stdURLIID = Components.interfaces.nsIURL;
  1027   var url = Components.classes[stdURLContractID].createInstance(stdURLIID);
  1028   url.filePath = aFilename;
  1030   var ext = url.fileExtension;
  1032   // This mirrors some code in nsExternalHelperAppService::DoContent
  1033   // Use the filename first and then the URI if that fails
  1035   var mimeInfo = getMIMEInfoForType(aContentType, ext);
  1037   if (ext && mimeInfo && mimeInfo.extensionExists(ext))
  1038     return ext;
  1040   // Well, that failed.  Now try the extension from the URI
  1041   var urlext;
  1042   try {
  1043     url = aURI.QueryInterface(Components.interfaces.nsIURL);
  1044     urlext = url.fileExtension;
  1045   } catch (e) {
  1048   if (urlext && mimeInfo && mimeInfo.extensionExists(urlext)) {
  1049     return urlext;
  1051   else {
  1052     try {
  1053       if (mimeInfo)
  1054         return mimeInfo.primaryExtension;
  1056     catch (e) {
  1058     // Fall back on the extensions in the filename and URI for lack
  1059     // of anything better.
  1060     return ext || urlext;
  1064 function GetSaveModeForContentType(aContentType, aDocument)
  1066   // We can only save a complete page if we have a loaded document
  1067   if (!aDocument)
  1068     return SAVEMODE_FILEONLY;
  1070   // Find the possible save modes using the provided content type
  1071   var saveMode = SAVEMODE_FILEONLY;
  1072   switch (aContentType) {
  1073   case "text/html":
  1074   case "application/xhtml+xml":
  1075   case "image/svg+xml":
  1076     saveMode |= SAVEMODE_COMPLETE_TEXT;
  1077     // Fall through
  1078   case "text/xml":
  1079   case "application/xml":
  1080     saveMode |= SAVEMODE_COMPLETE_DOM;
  1081     break;
  1084   return saveMode;
  1087 function getCharsetforSave(aDocument)
  1089   if (aDocument)
  1090     return aDocument.characterSet;
  1092   if (document.commandDispatcher.focusedWindow)
  1093     return document.commandDispatcher.focusedWindow.document.characterSet;
  1095   return window.content.document.characterSet;
  1098 /**
  1099  * Open a URL from chrome, determining if we can handle it internally or need to
  1100  *  launch an external application to handle it.
  1101  * @param aURL The URL to be opened
  1102  */
  1103 function openURL(aURL)
  1105   var uri = makeURI(aURL);
  1107   var protocolSvc = Components.classes["@mozilla.org/uriloader/external-protocol-service;1"]
  1108                               .getService(Components.interfaces.nsIExternalProtocolService);
  1110   if (!protocolSvc.isExposedProtocol(uri.scheme)) {
  1111     // If we're not a browser, use the external protocol service to load the URI.
  1112     protocolSvc.loadUrl(uri);
  1114   else {
  1115     var recentWindow = Services.wm.getMostRecentWindow("navigator:browser");
  1116     if (recentWindow) {
  1117       recentWindow.openUILinkIn(uri.spec, "tab");
  1118       return;
  1121     var loadgroup = Components.classes["@mozilla.org/network/load-group;1"]
  1122                               .createInstance(Components.interfaces.nsILoadGroup);
  1123     var appstartup = Services.startup;
  1125     var loadListener = {
  1126       onStartRequest: function ll_start(aRequest, aContext) {
  1127         appstartup.enterLastWindowClosingSurvivalArea();
  1128       },
  1129       onStopRequest: function ll_stop(aRequest, aContext, aStatusCode) {
  1130         appstartup.exitLastWindowClosingSurvivalArea();
  1131       },
  1132       QueryInterface: function ll_QI(iid) {
  1133         if (iid.equals(Components.interfaces.nsISupports) ||
  1134             iid.equals(Components.interfaces.nsIRequestObserver) ||
  1135             iid.equals(Components.interfaces.nsISupportsWeakReference))
  1136           return this;
  1137         throw Components.results.NS_ERROR_NO_INTERFACE;
  1140     loadgroup.groupObserver = loadListener;
  1142     var uriListener = {
  1143       onStartURIOpen: function(uri) { return false; },
  1144       doContent: function(ctype, preferred, request, handler) { return false; },
  1145       isPreferred: function(ctype, desired) { return false; },
  1146       canHandleContent: function(ctype, preferred, desired) { return false; },
  1147       loadCookie: null,
  1148       parentContentListener: null,
  1149       getInterface: function(iid) {
  1150         if (iid.equals(Components.interfaces.nsIURIContentListener))
  1151           return this;
  1152         if (iid.equals(Components.interfaces.nsILoadGroup))
  1153           return loadgroup;
  1154         throw Components.results.NS_ERROR_NO_INTERFACE;
  1158     var channel = Services.io.newChannelFromURI(uri);
  1159     var uriLoader = Components.classes["@mozilla.org/uriloader;1"]
  1160                               .getService(Components.interfaces.nsIURILoader);
  1161     uriLoader.openURI(channel,
  1162                       Components.interfaces.nsIURILoader.IS_CONTENT_PREFERRED,
  1163                       uriListener);

mercurial