【问题标题】:How to launch a normal download from a Firefox Add-on SDK extension如何从 Firefox 附加 SDK 扩展启动正常下载
【发布时间】:2020-01-21 09:21:56
【问题描述】:

我正在为 Firefox 开发附加 SDK 扩展。我发现我需要能够像用户请求一样启动下载,也就是说,要么显示正常的文件保存对话框,要么将文件保存到用户喜欢的任何位置,因为它可以在首选项>内容下进行配置。

每个关于下载的帖子或文档似乎只考虑了我知道在哪里下载文件的情况,但在这种情况下,这不是我需要的。在这种情况下,它需要就像用户开始下载一样。

如何实现,最好通过 SDK 的方法?

【问题讨论】:

  • 不错的q。我也对答案很感兴趣,我不太擅长下载。
  • 我认为您需要深入研究Download.jsm 并查看它是如何在 Firefox UI 中触发的。赞成,因为好问题。 :)

标签: javascript firefox firefox-addon firefox-addon-sdk


【解决方案1】:

好吧,你可以开始一个实际的保存。

从您的代码中启动保存链接:
在上下文菜单中,oncommand 值为gContextMenu.saveLink();。 saveLink() 定义在:chrome://browser/content/nsContextMenu.js。它会做一些家务,然后调用saveHelper() which is defined in the same file。您可以使用适当的参数调用 saveHelper() 。它包含在来自chrome://browser/content/web-panels.xul 的面板中:

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

然后将chrome://browser/content/browser.js中声明为nullgContextMenu变量赋值为:
gContextMenu = new nsContextMenu(this, event.shiftKey);
在上下文菜单的 onpopupshowing 事件处理程序中。返回到:
'gContextMenu = null;'
onpopuphiding 事件处理程序中。

如果你想在自己的代码中使用它,你可以这样做:

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);

使用 loadSubScript 加载 nsContextMenu.js 的替代方法:
我的偏好是使用 loadSubScript 从nsContextMenu.js 加载 saveHelper 代码。这使代码在未来 Firefox 版本中所做的任何更改都是最新的。但是,它引入了您使用来自非官方 API 的函数的依赖性。因此,它可能会在未来的 Firefox 版本中以某种方式发生变化,并且需要在您的附加组件中进行更改。另一种方法是在您的扩展程序中复制saveHelper() 代码。定义如下:

// 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);
}

【讨论】:

  • 如何判断linkText?
  • @РоманКоптев,我不太确定你在问什么。您确定链接文本的确切方式取决于您已有的信息。此问题未指定要下载的链接的来源。因此,我没有太多的背景可以为您提供答案。如果您已经拥有&lt;A&gt; 元素,那么您可以使用Node.textContent 完成您想要的。我建议你提出一个新问题,并提供更多关于你想知道的细节。 How to Ask
【解决方案2】:

就像@canuckistani 所说,使用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);

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多