How to launch a normal download from an addon

2019-05-07 09:44发布

Developing an addon for Firefox I find that I need to be able to launch a download as if the user requested it, that is, either showing the normal file save dialog or saving the file to wherever the user prefers, as it could be configured under preferences > content.

Every single post or documentation regarding downloads seem to only take in consideration the scenario where I know where to download the file, but that is not what I need in this case, in this case it needs to be as if the user started the download.

How can this be accomplished preferably via the methods of the SDK?

2条回答
放我归山
2楼-- · 2019-05-07 09:54

Like @canuckistani said, use the Downloads.jsm

let { Downloads } = require("resource://gre/modules/Downloads.jsm");
let { OS } = require("resource://gre/modules/osfile.jsm")
let { Task } = require("resource://gre/modules/Task.jsm");

Task.spawn(function () {

  yield Downloads.fetch("http://www.mozilla.org/",
                        OS.Path.join(OS.Constants.Path.tmpDir,
                                     "example-download.html"));

  console.log("example-download.html has been downloaded.");

}).then(null, Components.utils.reportError);
查看更多
再贱就再见
3楼-- · 2019-05-07 10:09

Well, you could just initiate an actual save.

Initiating a save link from your code:
In the context menu the oncommand value is gContextMenu.saveLink();. saveLink() is defined in: chrome://browser/content/nsContextMenu.js. It does some housekeeping and then calls saveHelper() which is defined in the same file. You could just call saveHelper() with appropriate arguments. It is included in panels from chrome://browser/content/web-panels.xul with:

<script type="application/javascript" 
            src="chrome://browser/content/nsContextMenu.js"/>

Then the gContextMenu variable declared in chrome://browser/content/browser.js as null is assigned:
gContextMenu = new nsContextMenu(this, event.shiftKey);
in the onpopupshowing event handler for context menus. It is returned to:
'gContextMenu = null;'
in the onpopuphiding event handler.

If you want to use it in your own code you can do:

let urlToSave = "http://stackoverflow.com/questions/26694442";
let linkText = "Some Link text";

//  Add a "/" to un-comment the code appropriate for your add-on type.
/* Overlay and bootstrap:
const Cc = Components.classes;
const Ci = Components.interfaces;
const Cr = Components.results;
//*/
/* Add-on SDK:
var {Cc, Ci, Cr} = require("chrome");
//*/

if (window === null || typeof window !== "object") {
    //If you do not already have a window reference, you need to obtain one:
    //  Add a "/" to un-comment the code appropriate for your add-on type.
    /* Add-on SDK:
    var window = require('sdk/window/utils').getMostRecentBrowserWindow();
    //*/
    /* Overlay and bootstrap (from almost any context/scope):
    var window=Components.classes["@mozilla.org/appshell/window-mediator;1"]
                         .getService(Components.interfaces.nsIWindowMediator)
                         .getMostRecentWindow("navigator:browser");        
    //*/
}

//Create an object in which we attach nsContextMenu.js.
//  It needs some support properties/functions which
//  nsContextMenu.js assumes are part of its context.
let contextMenuObj = {
    makeURI: function (aURL, aOriginCharset, aBaseURI) {
      var ioService = Cc["@mozilla.org/network/io-service;1"]
                                .getService(Ci.nsIIOService);
      return ioService.newURI(aURL, aOriginCharset, aBaseURI);
    },
    gPrefService: Cc["@mozilla.org/preferences-service;1"]
                            .getService(Ci.nsIPrefService)
                            .QueryInterface(Ci.nsIPrefBranch),
    Cc: Cc,
    Ci: Ci,
    Cr: Cr
};

Cc["@mozilla.org/moz/jssubscript-loader;1"]
        .getService(Ci.mozIJSSubScriptLoader)
        .loadSubScript("chrome://browser/content/nsContextMenu.js"
                       ,contextMenuObj);

//Re-define the initMenu function, as there is not a desire to actually
//  initialize a menu.        
contextMenuObj.nsContextMenu.prototype.initMenu = function() { };

let myContextMenu = new contextMenuObj.nsContextMenu();
//Save the specified URL
myContextMenu.saveHelper(urlToSave,linkText,null,true,window.content.document);

Alternative to using loadSubScript to load nsContextMenu.js:
My preference is to use loadSubScript to load the saveHelper code from nsContextMenu.js. This keeps the code up to date with any changes which are made in future Firefox releases. However, it introduces the dependency that you are using a function from a non-official API. Thus, it might change in some way in future Firefox release and require changes in your add-on. The alternative is to duplicate the saveHelper() code in your extension. It is defined as the following:

// Helper function to wait for appropriate MIME-type headers and
// then prompt the user with a file picker
saveHelper: function(linkURL, linkText, dialogTitle, bypassCache, doc) {
  // canonical def in nsURILoader.h
  const NS_ERROR_SAVE_LINK_AS_TIMEOUT = 0x805d0020;

  // an object to proxy the data through to
  // nsIExternalHelperAppService.doContent, which will wait for the
  // appropriate MIME-type headers and then prompt the user with a
  // file picker
  function saveAsListener() {}
  saveAsListener.prototype = {
    extListener: null, 

    onStartRequest: function saveLinkAs_onStartRequest(aRequest, aContext) {

      // if the timer fired, the error status will have been caused by that,
      // and we'll be restarting in onStopRequest, so no reason to notify
      // the user
      if (aRequest.status == NS_ERROR_SAVE_LINK_AS_TIMEOUT)
        return;

      timer.cancel();

      // some other error occured; notify the user...
      if (!Components.isSuccessCode(aRequest.status)) {
        try {
          const sbs = Cc["@mozilla.org/intl/stringbundle;1"].
                      getService(Ci.nsIStringBundleService);
          const bundle = sbs.createBundle(
                  "chrome://mozapps/locale/downloads/downloads.properties");

          const title = bundle.GetStringFromName("downloadErrorAlertTitle");
          const msg = bundle.GetStringFromName("downloadErrorGeneric");

          const promptSvc = Cc["@mozilla.org/embedcomp/prompt-service;1"].
                            getService(Ci.nsIPromptService);
          promptSvc.alert(doc.defaultView, title, msg);
        } catch (ex) {}
        return;
      }

      var extHelperAppSvc = 
        Cc["@mozilla.org/uriloader/external-helper-app-service;1"].
        getService(Ci.nsIExternalHelperAppService);
      var channel = aRequest.QueryInterface(Ci.nsIChannel);
      this.extListener = 
        extHelperAppSvc.doContent(channel.contentType, aRequest, 
                                  doc.defaultView, true);
      this.extListener.onStartRequest(aRequest, aContext);
    }, 

    onStopRequest: function saveLinkAs_onStopRequest(aRequest, aContext, 
                                                     aStatusCode) {
      if (aStatusCode == NS_ERROR_SAVE_LINK_AS_TIMEOUT) {
        // do it the old fashioned way, which will pick the best filename
        // it can without waiting.
        saveURL(linkURL, linkText, dialogTitle, bypassCache, false,
                doc.documentURIObject, doc);
      }
      if (this.extListener)
        this.extListener.onStopRequest(aRequest, aContext, aStatusCode);
    },

    onDataAvailable: function saveLinkAs_onDataAvailable(aRequest, aContext,
                                                         aInputStream,
                                                         aOffset, aCount) {
      this.extListener.onDataAvailable(aRequest, aContext, aInputStream,
                                       aOffset, aCount);
    }
  }

  function callbacks() {}
  callbacks.prototype = {
    getInterface: function sLA_callbacks_getInterface(aIID) {
      if (aIID.equals(Ci.nsIAuthPrompt) || aIID.equals(Ci.nsIAuthPrompt2)) {
        // If the channel demands authentication prompt, we must cancel it
        // because the save-as-timer would expire and cancel the channel
        // before we get credentials from user.  Both authentication dialog
        // and save as dialog would appear on the screen as we fall back to
        // the old fashioned way after the timeout.
        timer.cancel();
        channel.cancel(NS_ERROR_SAVE_LINK_AS_TIMEOUT);
      }
      throw Cr.NS_ERROR_NO_INTERFACE;
    } 
  }

  // if it we don't have the headers after a short time, the user 
  // won't have received any feedback from their click.  that's bad.  so
  // we give up waiting for the filename. 
  function timerCallback() {}
  timerCallback.prototype = {
    notify: function sLA_timer_notify(aTimer) {
      channel.cancel(NS_ERROR_SAVE_LINK_AS_TIMEOUT);
      return;
    }
  }

  // set up a channel to do the saving
  var ioService = Cc["@mozilla.org/network/io-service;1"].
                  getService(Ci.nsIIOService);
  var channel = ioService.newChannelFromURI(makeURI(linkURL));
  if (channel instanceof Ci.nsIPrivateBrowsingChannel) {
    let docIsPrivate = PrivateBrowsingUtils.isWindowPrivate(doc.defaultView);
    channel.setPrivate(docIsPrivate);
  }
  channel.notificationCallbacks = new callbacks();

  let flags = Ci.nsIChannel.LOAD_CALL_CONTENT_SNIFFERS;

  if (bypassCache)
    flags |= Ci.nsIRequest.LOAD_BYPASS_CACHE;

  if (channel instanceof Ci.nsICachingChannel)
    flags |= Ci.nsICachingChannel.LOAD_BYPASS_LOCAL_CACHE_IF_BUSY;

  channel.loadFlags |= flags;

  if (channel instanceof Ci.nsIHttpChannel) {
    channel.referrer = doc.documentURIObject;
    if (channel instanceof Ci.nsIHttpChannelInternal)
      channel.forceAllowThirdPartyCookie = true;
  }

  // fallback to the old way if we don't see the headers quickly 
  var timeToWait = 
    gPrefService.getIntPref("browser.download.saveLinkAsFilenameTimeout");
  var timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
  timer.initWithCallback(new timerCallback(), timeToWait,
                         timer.TYPE_ONE_SHOT);

  // kick off the channel with our proxy object as the listener
  channel.asyncOpen(new saveAsListener(), null);
}
查看更多
登录 后发表回答