Bug 983747 - Add 'download' method to Browser API. r=kanru, r=paolo, a=bajaj
authorGhislain 'Aus' Lacroix <glacroix@mozilla.com>
Mon, 09 Jun 2014 10:51:25 -0700
changeset 208005 c84a1d1b6612a1df7a6215590a22997ab21986f9
parent 208004 3bd347abca17fababbdd7a326a4298f75dcb2fbc
child 208006 c8f48ec1e9dcb05b5f04632882c9bb8c87d2bfd4
push id494
push userraliiev@mozilla.com
push dateMon, 25 Aug 2014 18:42:16 +0000
treeherdermozilla-release@a3cc3e46b571 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerskanru, paolo, bajaj
bugs983747
milestone32.0a2
Bug 983747 - Add 'download' method to Browser API. r=kanru, r=paolo, a=bajaj
dom/browser-element/BrowserElementParent.jsm
dom/browser-element/mochitest/browserElement_Download.js
dom/browser-element/mochitest/file_download_bin.sjs
dom/browser-element/mochitest/mochitest-oop.ini
dom/browser-element/mochitest/mochitest.ini
dom/browser-element/mochitest/test_browserElement_inproc_Download.html
dom/browser-element/mochitest/test_browserElement_oop_Download.html
toolkit/components/jsdownloads/src/DownloadCore.jsm
--- a/dom/browser-element/BrowserElementParent.jsm
+++ b/dom/browser-element/BrowserElementParent.jsm
@@ -130,16 +130,17 @@ function BrowserElementParent(frameLoade
   // 0 = disabled, 1 = enabled, 2 - auto detect
   if (getIntPref(TOUCH_EVENTS_ENABLED_PREF, 0) != 0) {
     defineNoReturnMethod('sendTouchEvent', this._sendTouchEvent);
   }
   defineNoReturnMethod('goBack', this._goBack);
   defineNoReturnMethod('goForward', this._goForward);
   defineNoReturnMethod('reload', this._reload);
   defineNoReturnMethod('stop', this._stop);
+  defineMethod('download', this._download);
   defineDOMRequestMethod('purgeHistory', 'purge-history');
   defineMethod('getScreenshot', this._getScreenshot);
   defineMethod('addNextPaintListener', this._addNextPaintListener);
   defineMethod('removeNextPaintListener', this._removeNextPaintListener);
   defineDOMRequestMethod('getCanGoBack', 'get-can-go-back');
   defineDOMRequestMethod('getCanGoForward', 'get-can-go-forward');
 
   let principal = this._frameElement.ownerDocument.nodePrincipal;
@@ -615,16 +616,116 @@ BrowserElementParent.prototype = {
   _reload: function(hardReload) {
     this._sendAsyncMsg('reload', {hardReload: hardReload});
   },
 
   _stop: function() {
     this._sendAsyncMsg('stop');
   },
 
+  _download: function(_url, _options) {
+    let ioService =
+      Cc['@mozilla.org/network/io-service;1'].getService(Ci.nsIIOService);
+    let uri = ioService.newURI(_url, null, null);
+    let url = uri.QueryInterface(Ci.nsIURL);
+
+    // Ensure we have _options, we always use it to send the filename.
+    _options = _options || {};
+    if (!_options.filename) {
+      _options.filename = url.fileName;
+    }
+
+    debug('_options = ' + uneval(_options));
+
+    // Ensure we have a filename.
+    if (!_options.filename) {
+      throw Components.Exception("Invalid argument", Cr.NS_ERROR_INVALID_ARG);
+    }
+
+    let interfaceRequestor =
+      this._frameLoader.loadContext.QueryInterface(Ci.nsIInterfaceRequestor);
+    let req = Services.DOMRequest.createRequest(this._window);
+
+    function DownloadListener() {
+      debug('DownloadListener Constructor');
+    }
+    DownloadListener.prototype = {
+      extListener: null,
+      onStartRequest: function(aRequest, aContext) {
+        debug('DownloadListener - onStartRequest');
+        let extHelperAppSvc =
+          Cc['@mozilla.org/uriloader/external-helper-app-service;1'].
+          getService(Ci.nsIExternalHelperAppService);
+        let channel = aRequest.QueryInterface(Ci.nsIChannel);
+
+        this.extListener =
+          extHelperAppSvc.doContent(
+              channel.contentType,
+              aRequest,
+              interfaceRequestor,
+              true);
+        this.extListener.onStartRequest(aRequest, aContext);
+      },
+      onStopRequest: function(aRequest, aContext, aStatusCode) {
+        debug('DownloadListener - onStopRequest (aStatusCode = ' +
+               aStatusCode + ')');
+        if (aStatusCode == Cr.NS_OK) {
+          // Everything looks great.
+          debug('DownloadListener - Download Successful.');
+          this.services.DOMRequest.fireSuccess(this.req, aStatusCode);
+        }
+        else {
+          // In case of failure, we'll simply return the failure status code.
+          debug('DownloadListener - Download Failed!');
+          this.services.DOMRequest.fireError(this.req, aStatusCode);
+        }
+
+        if (this.extListener) {
+          this.extListener.onStopRequest(aRequest, aContext, aStatusCode);
+        }
+      },
+      onDataAvailable: function(aRequest, aContext, aInputStream,
+                                aOffset, aCount) {
+        this.extListener.onDataAvailable(aRequest, aContext, aInputStream,
+                                         aOffset, aCount);
+      },
+      QueryInterface: XPCOMUtils.generateQI([Ci.nsIStreamListener, 
+                                             Ci.nsIRequestObserver])
+    };
+
+    let channel = ioService.newChannelFromURI(url);
+
+    // XXX We would set private browsing information prior to calling this.
+    channel.notificationCallbacks = interfaceRequestor;
+
+    // Since we're downloading our own local copy we'll want to bypass the
+    // cache and local cache if the channel let's us specify this.
+    let flags = Ci.nsIChannel.LOAD_CALL_CONTENT_SNIFFERS |
+                Ci.nsIChannel.LOAD_BYPASS_CACHE;
+    if (channel instanceof Ci.nsICachingChannel) {
+      debug('This is a caching channel. Forcing bypass.');
+      flags |= Ci.nsICachingChannel.LOAD_BYPASS_LOCAL_CACHE_IF_BUSY;
+    }
+
+    channel.loadFlags |= flags;
+
+    if (channel instanceof Ci.nsIHttpChannel) {
+      debug('Setting HTTP referrer = ' + this._window.document.documentURIObject);
+      channel.referrer = this._window.document.documentURIObject;
+      if (channel instanceof Ci.nsIHttpChannelInternal) {
+        channel.forceAllowThirdPartyCookie = true;
+      }
+    }
+
+    // Set-up complete, let's get things started.
+    channel.asyncOpen(new DownloadListener(), null);
+
+    return req;
+  },
+
   _getScreenshot: function(_width, _height, _mimeType) {
     let width = parseInt(_width);
     let height = parseInt(_height);
     let mimeType = (typeof _mimeType === 'string') ?
       _mimeType.trim().toLowerCase() : 'image/jpeg';
     if (isNaN(width) || isNaN(height) || width < 0 || height < 0) {
       throw Components.Exception("Invalid argument",
                                  Cr.NS_ERROR_INVALID_ARG);
new file mode 100644
--- /dev/null
+++ b/dom/browser-element/mochitest/browserElement_Download.js
@@ -0,0 +1,37 @@
+/* Any copyright is dedicated to the public domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Bug 983747 - Test 'download' method on iframe.
+
+"use strict";
+
+SimpleTest.waitForExplicitFinish();
+browserElementTestHelpers.setEnabledPref(true);
+browserElementTestHelpers.addPermission();
+
+var iframe;
+var downloadURL = 'http://test/tests/dom/browser-element/mochitest/file_download_bin.sjs';
+
+function runTest() {
+  iframe = document.createElement('iframe');
+  SpecialPowers.wrap(iframe).mozbrowser = true;
+
+  iframe.addEventListener('mozbrowserloadend', loadend);
+  iframe.src = 'data:text/html,<html><body>hello</body></html>';
+  iframe.setAttribute('remote', 'true');
+
+  document.body.appendChild(iframe);
+}
+
+function loadend() {
+  var req = iframe.download(downloadURL, { filename: 'test.bin' });
+  req.onsuccess = function() {
+    ok(true, 'Download finished as expected.');
+    SimpleTest.finish();
+  }
+  req.onerror = function() {
+    ok(false, 'Expected no error, got ' + req.error);
+  }
+}
+
+addEventListener('testready', runTest);
new file mode 100644
--- /dev/null
+++ b/dom/browser-element/mochitest/file_download_bin.sjs
@@ -0,0 +1,4 @@
+function handleRequest(request, response) {
+  response.setHeader("Content-Type", "application/octet-stream", false);
+  response.write("BIN");
+}
\ No newline at end of file
--- a/dom/browser-element/mochitest/mochitest-oop.ini
+++ b/dom/browser-element/mochitest/mochitest-oop.ini
@@ -25,16 +25,18 @@ skip-if = (toolkit == 'gonk' && !debug)
 [test_browserElement_oop_BrowserWindowNamespace.html]
 skip-if = (toolkit == 'gonk' && !debug)
 [test_browserElement_oop_BrowserWindowResize.html]
 [test_browserElement_oop_Close.html]
 [test_browserElement_oop_CookiesNotThirdParty.html]
 [test_browserElement_oop_DOMRequestError.html]
 [test_browserElement_oop_DataURI.html]
 [test_browserElement_oop_DocumentFirstPaint.html]
+[test_browserElement_oop_Download.html]
+disabled = bug 1022281
 [test_browserElement_oop_ErrorSecurity.html]
 skip-if = (toolkit == 'gonk' && !debug)
 [test_browserElement_oop_FirstPaint.html]
 [test_browserElement_oop_ForwardName.html]
 [test_browserElement_oop_FrameWrongURI.html]
 skip-if = (toolkit == 'gonk' && !debug)
 [test_browserElement_oop_GetScreenshot.html]
 [test_browserElement_oop_GetScreenshotDppx.html]
--- a/dom/browser-element/mochitest/mochitest.ini
+++ b/dom/browser-element/mochitest/mochitest.ini
@@ -16,16 +16,17 @@ support-files =
   browserElement_Close.js
   browserElement_CloseApp.js
   browserElement_CloseFromOpener.js
   browserElement_ContextmenuEvents.js
   browserElement_CookiesNotThirdParty.js
   browserElement_DOMRequestError.js
   browserElement_DataURI.js
   browserElement_DocumentFirstPaint.js
+  browserElement_Download.js
   browserElement_ErrorSecurity.js
   browserElement_ExposableURI.js
   browserElement_FirstPaint.js
   browserElement_ForwardName.js
   browserElement_FrameWrongURI.js
   browserElement_GetScreenshot.js
   browserElement_GetScreenshotDppx.js
   browserElement_Iconchange.js
@@ -91,16 +92,17 @@ support-files =
   file_browserElement_TargetTop.html
   file_browserElement_XFrameOptions.sjs
   file_browserElement_XFrameOptionsAllowFrom.html
   file_browserElement_XFrameOptionsAllowFrom.sjs
   file_browserElement_XFrameOptionsDeny.html
   file_browserElement_XFrameOptionsSameOrigin.html
   file_bug709759.sjs
   file_bug741717.sjs
+  file_download_bin.sjs
   file_empty.html
   file_empty_script.js
   file_focus.html
   file_http_401_response.sjs
   file_inputmethod.html
   file_post_request.html
   file_wyciwyg.html
 
@@ -129,16 +131,18 @@ skip-if = buildapp == 'b2g'
 skip-if = toolkit == 'android' || buildapp == 'b2g' # android(FAILS, bug 796982) androidx86(FAILS, bug 796982)
 [test_browserElement_inproc_CloseFromOpener.html]
 skip-if = buildapp == 'b2g'
 [test_browserElement_inproc_ContextmenuEvents.html]
 [test_browserElement_inproc_CookiesNotThirdParty.html]
 [test_browserElement_inproc_DOMRequestError.html]
 [test_browserElement_inproc_DataURI.html]
 [test_browserElement_inproc_DocumentFirstPaint.html]
+[test_browserElement_inproc_Download.html]
+disabled = bug 1022281
 [test_browserElement_inproc_ExposableURI.html]
 [test_browserElement_inproc_FirstPaint.html]
 [test_browserElement_inproc_ForwardName.html]
 [test_browserElement_inproc_FrameWrongURI.html]
 skip-if = (toolkit == 'gonk' && !debug)
 [test_browserElement_inproc_GetScreenshot.html]
 [test_browserElement_inproc_GetScreenshotDppx.html]
 [test_browserElement_inproc_Iconchange.html]
new file mode 100644
--- /dev/null
+++ b/dom/browser-element/mochitest/test_browserElement_inproc_Download.html
@@ -0,0 +1,13 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <title>Test for Bug 983747</title>
+  <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+  <script type="application/javascript" src="browserElementTestHelpers.js"></script>
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<script type="application/javascript;version=1.7" src="browserElement_Download.js">
+</script>
+</body>
+</html>
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/dom/browser-element/mochitest/test_browserElement_oop_Download.html
@@ -0,0 +1,13 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <title>Test for Bug 983747</title>
+  <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+  <script type="application/javascript" src="browserElementTestHelpers.js"></script>
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<script type="application/javascript;version=1.7" src="browserElement_Download.js">
+</script>
+</body>
+</html>
\ No newline at end of file
--- a/toolkit/components/jsdownloads/src/DownloadCore.jsm
+++ b/toolkit/components/jsdownloads/src/DownloadCore.jsm
@@ -1386,18 +1386,28 @@ this.DownloadSaver.prototype = {
     let referrer = this.download.source.referrer;
     let referrerUri = referrer ? NetUtil.newURI(referrer) : null;
     let targetUri = NetUtil.newURI(new FileUtils.File(
                                        this.download.target.path));
 
     // The start time is always available when we reach this point.
     let startPRTime = this.download.startTime.getTime() * 1000;
 
-    gDownloadHistory.addDownload(sourceUri, referrerUri, startPRTime,
-                                 targetUri);
+    try {
+      gDownloadHistory.addDownload(sourceUri, referrerUri, startPRTime,
+                                   targetUri);
+    }
+    catch(ex if ex instanceof Components.Exception &&
+                ex.result == Cr.NS_ERROR_NOT_AVAILABLE) {
+      //
+      // Under normal operation the download history service may not
+      // be available. We don't want all downloads that are public to fail
+      // when this happens so we'll ignore this error and this error only!
+      //
+    }
   },
 
   /**
    * Returns a static representation of the current object state.
    *
    * @return A JavaScript object that can be serialized to JSON.
    */
   toSerializable: function ()