Merge fx-team to m-c.
authorRyan VanderMeulen <ryanvm@gmail.com>
Tue, 10 Sep 2013 16:39:33 -0400
changeset 146393 ccd82434a1fc51e077910da392a81b4cba2a1cfb
parent 146380 ce127f081ebff9025736174b467d04a277fc8eb0 (current diff)
parent 146392 bfb6901822f6c80b56634e79db50239769b43c08 (diff)
child 146394 5e8290749d6079fd9bff462d9c37fde9704f60af
child 146457 5a2ab286a97723a5000d45a1453243a746c78cad
push id1
push userroot
push dateMon, 20 Oct 2014 17:29:22 +0000
milestone26.0a1
Merge fx-team to m-c.
--- a/browser/devtools/webconsole/network-panel.js
+++ b/browser/devtools/webconsole/network-panel.js
@@ -670,19 +670,17 @@ NetworkPanel.prototype =
 
   /**
    * Updates the content of the NetworkPanel's iframe.
    *
    * @returns void
    */
   update: function NP_update()
   {
-    // After the iframe's contentWindow is ready, the document object is set.
-    // If the document object is not available yet nothing needs to be updated.
-    if (!this.document || !this.document.getElementById("headUrl")) {
+    if (!this.document || this.document.readyState != "complete") {
       return;
     }
 
     let updates = this.httpActivity.updates;
     let timing = this.httpActivity.timings;
     let request = this.httpActivity.request;
     let response = this.httpActivity.response;
 
--- a/browser/devtools/webconsole/test/browser_bug_869003_inspect_cross_domain_object.js
+++ b/browser/devtools/webconsole/test/browser_bug_869003_inspect_cross_domain_object.js
@@ -7,16 +7,21 @@
 // bug 869003.
 
 const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-bug-869003-top-window.html";
 
 let gWebConsole, gJSTerm, gVariablesView;
 
 function test()
 {
+  // This test is slightly more involved: it opens the web console, then the
+  // variables view for a given object, it updates a property in the view and
+  // checks the result. We can get a timeout with debug builds on slower machines.
+  requestLongerTimeout(2);
+
   addTab("data:text/html;charset=utf8,<p>hello");
   browser.addEventListener("load", function onLoad() {
     browser.removeEventListener("load", onLoad, true);
     openConsole(null, consoleOpened);
   }, true);
 }
 
 function consoleOpened(hud)
--- a/browser/devtools/webconsole/test/browser_console_private_browsing.js
+++ b/browser/devtools/webconsole/test/browser_console_private_browsing.js
@@ -10,16 +10,20 @@ function test()
 {
   const TEST_URI = "data:text/html;charset=utf8,<p>hello world! bug 874061" +
                    "<button onclick='console.log(\"foobar bug 874061\");" +
                    "fooBazBaz.yummy()'>click</button>";
   let ConsoleAPIStorage = Cu.import("resource://gre/modules/ConsoleAPIStorage.jsm", {}).ConsoleAPIStorage;
   let privateWindow, privateBrowser, privateTab, privateContent;
   let hud, expectedMessages, nonPrivateMessage;
 
+  // This test is slightly more involved: it opens the web console twice,
+  // a new private window once, and the browser console twice. We can get
+  // a timeout with debug builds on slower machines.
+  requestLongerTimeout(2);
   start();
 
   function start()
   {
     gBrowser.selectedTab = gBrowser.addTab("data:text/html;charset=utf8," +
                                            "<p>hello world! I am not private!");
     gBrowser.selectedBrowser.addEventListener("load", onLoadTab, true);
   }
--- a/content/media/webspeech/recognition/SpeechRecognition.cpp
+++ b/content/media/webspeech/recognition/SpeechRecognition.cpp
@@ -133,27 +133,21 @@ SpeechRecognition::GetParentObject() con
 
 void
 SpeechRecognition::ProcessEvent(SpeechEvent* aEvent)
 {
   SR_LOG("Processing %s, current state is %s",
          GetName(aEvent),
          GetName(mCurrentState));
 
-  // Run priority events first
-  for (uint32_t i = 0; i < mPriorityEvents.Length(); ++i) {
-    nsRefPtr<SpeechEvent> event = mPriorityEvents[i];
-
-    SR_LOG("Processing priority %s", GetName(event));
-    Transition(event);
+  if (mAborted && aEvent->mType != EVENT_ABORT) {
+    // ignore all events while aborting
+    return;
   }
 
-  mPriorityEvents.Clear();
-
-  SR_LOG("Processing %s received as argument", GetName(aEvent));
   Transition(aEvent);
 }
 
 void
 SpeechRecognition::Transition(SpeechEvent* aEvent)
 {
   switch (mCurrentState) {
     case STATE_IDLE:
@@ -303,19 +297,16 @@ SpeechRecognition::Transition(SpeechEven
         case EVENT_START:
         case EVENT_RECOGNITIONSERVICE_INTERMEDIATE_RESULT:
           SR_LOG("STATE_WAITING_FOR_RESULT: Unhandled aEvent %s", GetName(aEvent));
           MOZ_CRASH();
         case EVENT_COUNT:
           MOZ_CRASH("Invalid event EVENT_COUNT");
       }
       break;
-    case STATE_ABORTING:
-      DoNothing(aEvent);
-      break;
     case STATE_COUNT:
       MOZ_CRASH("Invalid state STATE_COUNT");
   }
 
   return;
 }
 
 /*
@@ -383,16 +374,17 @@ SpeechRecognition::GetRecognitionService
 void
 SpeechRecognition::Reset()
 {
   SetState(STATE_IDLE);
   mRecognitionService = nullptr;
   mEstimationSamples = 0;
   mBufferedSamples = 0;
   mSpeechDetectionTimer->Cancel();
+  mAborted = false;
 }
 
 void
 SpeechRecognition::ResetAndEnd()
 {
   Reset();
   DispatchTrustedEvent(NS_LITERAL_STRING("end"));
 }
@@ -496,19 +488,16 @@ SpeechRecognition::DoNothing(SpeechEvent
 {
 }
 
 void
 SpeechRecognition::AbortSilently(SpeechEvent* aEvent)
 {
   bool stopRecording = StateBetween(STATE_ESTIMATING, STATE_RECOGNIZING);
 
-  // prevent reentrancy from DOM events
-  SetState(STATE_ABORTING);
-
   if (mRecognitionService) {
     mRecognitionService->Abort();
   }
 
   if (stopRecording) {
     StopRecording();
   }
 
@@ -740,16 +729,21 @@ SpeechRecognition::Stop()
 {
   nsRefPtr<SpeechEvent> event = new SpeechEvent(this, EVENT_STOP);
   NS_DispatchToMainThread(event);
 }
 
 void
 SpeechRecognition::Abort()
 {
+  if (mAborted) {
+    return;
+  }
+
+  mAborted = true;
   nsRefPtr<SpeechEvent> event = new SpeechEvent(this, EVENT_ABORT);
   NS_DispatchToMainThread(event);
 }
 
 void
 SpeechRecognition::DispatchError(EventType aErrorType,
                                  SpeechRecognitionErrorCode aErrorCode,
                                  const nsAString& aMessage)
@@ -897,17 +891,16 @@ SpeechRecognition::GetName(FSMState aId)
 {
   static const char* names[] = {
     "STATE_IDLE",
     "STATE_STARTING",
     "STATE_ESTIMATING",
     "STATE_WAITING_FOR_SPEECH",
     "STATE_RECOGNIZING",
     "STATE_WAITING_FOR_RESULT",
-    "STATE_ABORTING"
   };
 
   MOZ_ASSERT(aId < STATE_COUNT);
   MOZ_ASSERT(ArrayLength(names) == STATE_COUNT);
   return names[aId];
 }
 
 const char*
--- a/content/media/webspeech/recognition/SpeechRecognition.h
+++ b/content/media/webspeech/recognition/SpeechRecognition.h
@@ -165,17 +165,16 @@ public:
 private:
   enum FSMState {
     STATE_IDLE,
     STATE_STARTING,
     STATE_ESTIMATING,
     STATE_WAITING_FOR_SPEECH,
     STATE_RECOGNIZING,
     STATE_WAITING_FOR_RESULT,
-    STATE_ABORTING,
     STATE_COUNT
   };
 
   void SetState(FSMState state);
   bool StateBetween(FSMState begin, FSMState end);
 
   class GetUserMediaStreamOptions : public nsIMediaStreamOptions
   {
@@ -243,29 +242,29 @@ private:
 
   nsRefPtr<DOMMediaStream> mDOMStream;
   nsRefPtr<SpeechStreamListener> mSpeechListener;
   nsCOMPtr<nsISpeechRecognitionService> mRecognitionService;
 
   void GetRecognitionServiceCID(nsACString& aResultCID);
 
   FSMState mCurrentState;
-  nsTArray<nsRefPtr<SpeechEvent> > mPriorityEvents;
 
   Endpointer mEndpointer;
   uint32_t mEstimationSamples;
 
   uint32_t mAudioSamplesPerChunk;
 
   // buffer holds one chunk of mAudioSamplesPerChunk
   // samples before feeding it to mEndpointer
   nsRefPtr<SharedBuffer> mAudioSamplesBuffer;
   uint32_t mBufferedSamples;
 
   nsCOMPtr<nsITimer> mSpeechDetectionTimer;
+  bool mAborted;
 
   void ProcessTestEventRequest(nsISupports* aSubject, const nsAString& aEventName);
 
   const char* GetName(FSMState aId);
   const char* GetName(SpeechEvent* aId);
 };
 
 class SpeechEvent : public nsRunnable
--- a/mobile/android/base/home/BrowserSearch.java
+++ b/mobile/android/base/home/BrowserSearch.java
@@ -452,16 +452,22 @@ public class BrowserSearch extends HomeF
         } catch (JSONException e) {
             Log.e(LOGTAG, "Error getting search engine JSON", e);
         }
 
         filterSuggestions();
     }
 
     private void showSuggestionsOptIn() {
+        // Return if the ViewStub was already inflated - an inflated ViewStub is removed from the
+        // View hierarchy so a second call to findViewById will return null.
+        if (mSuggestionsOptInPrompt != null) {
+            return;
+        }
+
         mSuggestionsOptInPrompt = ((ViewStub) mView.findViewById(R.id.suggestions_opt_in_prompt)).inflate();
 
         TextView promptText = (TextView) mSuggestionsOptInPrompt.findViewById(R.id.suggestions_prompt_title);
         promptText.setText(getResources().getString(R.string.suggestions_prompt, mSearchEngines.get(0).name));
 
         final View yesButton = mSuggestionsOptInPrompt.findViewById(R.id.suggestions_prompt_yes);
         final View noButton = mSuggestionsOptInPrompt.findViewById(R.id.suggestions_prompt_no);
 
--- a/modules/libpref/src/init/all.js
+++ b/modules/libpref/src/init/all.js
@@ -142,16 +142,23 @@ pref("browser.display.focus_ring_on_anyt
 // focus ring border style.
 // 0 = solid border, 1 = dotted border
 pref("browser.display.focus_ring_style", 1);
 
 pref("browser.helperApps.alwaysAsk.force",  false);
 pref("browser.helperApps.neverAsk.saveToDisk", "");
 pref("browser.helperApps.neverAsk.openFile", "");
 
+#ifdef XP_WIN
+// By default, security zone information is stored in the Alternate Data Stream
+// of downloaded executable files on Windows.  This preference allows disabling
+// this feature, and thus the associated system-level execution prompts.
+pref("browser.download.saveZoneInformation", true);
+#endif
+
 // xxxbsmedberg: where should prefs for the toolkit go?
 pref("browser.chrome.toolbar_tips",         true);
 // 0 = Pictures Only, 1 = Text Only, 2 = Pictures and Text
 pref("browser.chrome.toolbar_style",        2);
 // max image size for which it is placed in the tab icon for tabbrowser.
 // if 0, no images are used for tab icons for image documents.
 pref("browser.chrome.image_icons.max_size", 1024);
 
--- a/toolkit/components/jsdownloads/src/DownloadCore.jsm
+++ b/toolkit/components/jsdownloads/src/DownloadCore.jsm
@@ -62,16 +62,19 @@ XPCOMUtils.defineLazyModuleGetter(this, 
                                   "resource://gre/modules/NetUtil.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "OS",
                                   "resource://gre/modules/osfile.jsm")
 XPCOMUtils.defineLazyModuleGetter(this, "Promise",
                                   "resource://gre/modules/commonjs/sdk/core/promise.js");
 XPCOMUtils.defineLazyModuleGetter(this, "Task",
                                   "resource://gre/modules/Task.jsm");
 
+XPCOMUtils.defineLazyServiceGetter(this, "gDownloadHistory",
+           "@mozilla.org/browser/download-history;1",
+           Ci.nsIDownloadHistory);
 XPCOMUtils.defineLazyServiceGetter(this, "gExternalHelperAppService",
            "@mozilla.org/uriloader/external-helper-app-service;1",
            Ci.nsIExternalHelperAppService);
 
 const BackgroundFileSaverStreamListener = Components.Constructor(
       "@mozilla.org/network/background-file-saver;1?mode=streamlistener",
       "nsIBackgroundFileSaver");
 
@@ -1201,16 +1204,40 @@ DownloadSaver.prototype = {
    * @rejects JavaScript exception.
    */
   removePartialData: function DS_removePartialData()
   {
     return Promise.resolve();
   },
 
   /**
+   * This can be called by the saver implementation when the download is already
+   * started, to add it to the browsing history.  This method has no effect if
+   * the download is private.
+   */
+  addToHistory: function ()
+  {
+    if (this.download.source.isPrivate) {
+      return;
+    }
+
+    let sourceUri = NetUtil.newURI(this.download.source.url);
+    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);
+  },
+
+  /**
    * Returns a static representation of the current object state.
    *
    * @return A JavaScript object that can be serialized to JSON.
    */
   toSerializable: function ()
   {
     throw new Error("Not implemented.");
   },
@@ -1262,16 +1289,21 @@ DownloadCopySaver.prototype = {
   /**
    * Indicates whether the "cancel" method has been called.  This is used to
    * prevent the request from starting in case the operation is canceled before
    * the BackgroundFileSaver instance has been created.
    */
   _canceled: false,
 
   /**
+   * True if the associated download has already been added to browsing history.
+   */
+  alreadyAddedToHistory: false,
+
+  /**
    * String corresponding to the entityID property of the nsIResumableChannel
    * used to execute the download, or null if the channel was not resumable or
    * the saver was instructed not to keep partially downloaded data.
    */
   entityID: null,
 
   /**
    * Implements "DownloadSaver.execute".
@@ -1283,16 +1315,26 @@ DownloadCopySaver.prototype = {
     this._canceled = false;
 
     let download = this.download;
     let targetPath = download.target.path;
     let partFilePath = download.target.partFilePath;
     let keepPartialData = download.tryToKeepPartialData;
 
     return Task.spawn(function task_DCS_execute() {
+      // Add the download to history the first time it is started in this
+      // session.  If the download is restarted in a different session, a new
+      // history visit will be added.  We do this just to avoid the complexity
+      // of serializing this state between sessions, since adding a new visit
+      // does not have any noticeable side effect.
+      if (!this.alreadyAddedToHistory) {
+        this.addToHistory();
+        this.alreadyAddedToHistory = true;
+      }
+
       // To reduce the chance that other downloads reuse the same final target
       // file name, we should create a placeholder as soon as possible, before
       // starting the network request.  The placeholder is also required in case
       // we are using a ".part" file instead of the final target while the
       // download is in progress.
       try {
         // If the file already exists, don't delete its contents yet.
         let file = yield OS.File.open(targetPath, { write: true });
@@ -1628,28 +1670,43 @@ DownloadLegacySaver.prototype = {
    */
   progressWasNotified: false,
 
   /**
    * Called by the nsITransfer implementation when the request has started.
    *
    * @param aRequest
    *        nsIRequest associated to the status update.
+   * @param aAlreadyAddedToHistory
+   *        Indicates that the nsIExternalHelperAppService component already
+   *        added the download to the browsing history, unless it was started
+   *        from a private browsing window.  When this parameter is false, the
+   *        download is added to the browsing history here.  Private downloads
+   *        are never added to history even if this parameter is false.
    */
-  onTransferStarted: function (aRequest)
+  onTransferStarted: function (aRequest, aAlreadyAddedToHistory)
   {
     // Store the entity ID to use for resuming if required.
     if (this.download.tryToKeepPartialData &&
         aRequest instanceof Ci.nsIResumableChannel) {
       try {
         // If reading the ID succeeds, the source is resumable.
         this.entityID = aRequest.entityID;
       } catch (ex if ex instanceof Components.Exception &&
                      ex.result == Cr.NS_ERROR_NOT_RESUMABLE) { }
     }
+
+    // For legacy downloads, we must update the referrer at this time.
+    if (aRequest instanceof Ci.nsIHttpChannel && aRequest.referrer) {
+      this.download.source.referrer = aRequest.referrer.spec;
+    }
+
+    if (!aAlreadyAddedToHistory) {
+      this.addToHistory();
+    }
   },
 
   /**
    * Called by the nsITransfer implementation when the request has finished.
    *
    * @param aRequest
    *        nsIRequest associated to the status update.
    * @param aStatus
@@ -1697,16 +1754,17 @@ DownloadLegacySaver.prototype = {
   {
     // Check if this is not the first execution of the download.  The Download
     // object guarantees that this function is not re-entered during execution.
     if (this.firstExecutionFinished) {
       if (!this.copySaver) {
         this.copySaver = new DownloadCopySaver();
         this.copySaver.download = this.download;
         this.copySaver.entityID = this.entityID;
+        this.copySaver.alreadyAddedToHistory = true;
       }
       return this.copySaver.execute.apply(this.copySaver, arguments);
     }
 
     this.setProgressBytesFn = aSetProgressBytesFn;
 
     return Task.spawn(function task_DLS_execute() {
       try {
--- a/toolkit/components/jsdownloads/src/DownloadIntegration.jsm
+++ b/toolkit/components/jsdownloads/src/DownloadIntegration.jsm
@@ -61,16 +61,24 @@ XPCOMUtils.defineLazyServiceGetter(this,
 XPCOMUtils.defineLazyGetter(this, "gParentalControlsService", function() {
   if ("@mozilla.org/parental-controls-service;1" in Cc) {
     return Cc["@mozilla.org/parental-controls-service;1"]
       .createInstance(Ci.nsIParentalControlsService);
   }
   return null;
 });
 
+/**
+ * ArrayBufferView representing the bytes to be written to the "Zone.Identifier"
+ * Alternate Data Stream to mark a file as coming from the Internet zone.
+ */
+XPCOMUtils.defineLazyGetter(this, "gInternetZoneIdentifier", function() {
+  return new TextEncoder().encode("[ZoneTransfer]\r\nZoneId=3\r\n");
+});
+
 const Timer = Components.Constructor("@mozilla.org/timer;1", "nsITimer",
                                      "initWithCallback");
 
 /**
  * Indicates the delay between a change to the downloads data and the related
  * save operation.  This value is the result of a delicate trade-off, assuming
  * the host application uses the browser history instead of the download store
  * to save completed downloads.
@@ -384,25 +392,56 @@ this.DownloadIntegration = {
    * aParam aDownload
    *        The Download object.
    *
    * @return {Promise}
    * @resolves When all the operations completed successfully.
    * @rejects JavaScript exception if any of the operations failed.
    */
   downloadDone: function(aDownload) {
-    try {
+    return Task.spawn(function () {
+#ifdef XP_WIN
+      // On Windows, we mark any executable file saved to the NTFS file system
+      // as coming from the Internet security zone.  We do this by writing to
+      // the "Zone.Identifier" Alternate Data Stream directly, because the Save
+      // method of the IAttachmentExecute interface would trigger operations
+      // that may cause the application to hang, or other performance issues.
+      // The stream created in this way is forward-compatible with all the
+      // current and future versions of Windows.
+      if (Services.prefs.getBoolPref("browser.download.saveZoneInformation")) {
+        let file = new FileUtils.File(aDownload.target.path);
+        if (file.isExecutable()) {
+          try {
+            let streamPath = aDownload.target.path + ":Zone.Identifier";
+            let stream = yield OS.File.open(streamPath, { create: true });
+            try {
+              yield stream.write(gInternetZoneIdentifier);
+            } finally {
+              yield stream.close();
+            }
+          } catch (ex) {
+            // If writing to the stream fails, we ignore the error and continue.
+            // The Windows API error 123 (ERROR_INVALID_NAME) is expected to
+            // occur when working on a file system that does not support
+            // Alternate Data Streams, like FAT32, thus we don't report this
+            // specific error.
+            if (!(ex instanceof OS.File.Error) || ex.winLastError != 123) {
+              Cu.reportError(ex);
+            }
+          }
+        }
+      }
+#endif
+
       gDownloadPlatform.downloadDone(NetUtil.newURI(aDownload.source.url),
                                      new FileUtils.File(aDownload.target.path),
-                                     aDownload.contentType, aDownload.source.isPrivate);
+                                     aDownload.contentType,
+                                     aDownload.source.isPrivate);
       this.downloadDoneCalled = true;
-      return Promise.resolve();
-    } catch(ex) {
-      return Promise.reject(ex);
-    }
+    }.bind(this));
   },
 
   /**
    * Determines whether it's a Windows Metro app.
    */
   _isImmersiveProcess: function() {
     // TODO: to be implemented
     return false;
@@ -427,33 +466,36 @@ this.DownloadIntegration = {
    *           launched.
    * @rejects  JavaScript exception if there was an error trying to launch
    *           the file.
    */
   launchDownload: function (aDownload) {
     let deferred = Task.spawn(function DI_launchDownload_task() {
       let file = new FileUtils.File(aDownload.target.path);
 
-      // Ask for confirmation if the file is executable.  We do this here,
-      // instead of letting the caller handle the prompt separately in the user
-      // interface layer, for two reasons.  The first is because of its security
-      // nature, so that add-ons cannot forget to do this check.  The second is
-      // that the system-level security prompt, if enabled, would be displayed
-      // at launch time in any case.
+#ifndef XP_WIN
+      // Ask for confirmation if the file is executable, except on Windows where
+      // the operating system will show the prompt based on the security zone.
+      // We do this here, instead of letting the caller handle the prompt
+      // separately in the user interface layer, for two reasons.  The first is
+      // because of its security nature, so that add-ons cannot forget to do
+      // this check.  The second is that the system-level security prompt would
+      // be displayed at launch time in any case.
       if (file.isExecutable() && !this.dontOpenFileAndFolder) {
         // We don't anchor the prompt to a specific window intentionally, not
         // only because this is the same behavior as the system-level prompt,
         // but also because the most recently active window is the right choice
         // in basically all cases.
         let shouldLaunch = yield DownloadUIHelper.getPrompter()
                                    .confirmLaunchExecutable(file.path);
         if (!shouldLaunch) {
           return;
         }
       }
+#endif
 
       // In case of a double extension, like ".tar.gz", we only
       // consider the last one, because the MIME service cannot
       // handle multiple extensions.
       let fileExtension = null, mimeInfo = null;
       let match = file.leafName.match(/\.([^.]+)$/);
       if (match) {
         fileExtension = match[1];
--- a/toolkit/components/jsdownloads/src/DownloadLegacy.js
+++ b/toolkit/components/jsdownloads/src/DownloadLegacy.js
@@ -86,26 +86,31 @@ DownloadLegacyTransfer.prototype = {
     if (!Components.isSuccessCode(aStatus)) {
       this._componentFailed = true;
     }
 
     if ((aStateFlags & Ci.nsIWebProgressListener.STATE_START) &&
         (aStateFlags & Ci.nsIWebProgressListener.STATE_IS_NETWORK)) {
       // The main request has just started.  Wait for the associated Download
       // object to be available before notifying.
-      this._deferDownload.promise.then(function (aDownload) {
-        aDownload.saver.onTransferStarted(aRequest);
+      this._deferDownload.promise.then(download => {
+        download.saver.onTransferStarted(
+                         aRequest,
+                         this._cancelable instanceof Ci.nsIHelperAppLauncher);
       }).then(null, Cu.reportError);
     } else if ((aStateFlags & Ci.nsIWebProgressListener.STATE_STOP) &&
         (aStateFlags & Ci.nsIWebProgressListener.STATE_IS_NETWORK)) {
       // The last file has been received, or the download failed.  Wait for the
       // associated Download object to be available before notifying.
-      this._deferDownload.promise.then(function DLT_OSC_onDownload(aDownload) {
-        aDownload.saver.onTransferFinished(aRequest, aStatus);
+      this._deferDownload.promise.then(download => {
+        download.saver.onTransferFinished(aRequest, aStatus);
       }).then(null, Cu.reportError);
+
+      // Release the reference to the component executing the download.
+      this._cancelable = null;
     }
   },
 
   onProgressChange: function DLT_onProgressChange(aWebProgress, aRequest,
                                                   aCurSelfProgress,
                                                   aMaxSelfProgress,
                                                   aCurTotalProgress,
                                                   aMaxTotalProgress)
@@ -159,16 +164,18 @@ DownloadLegacyTransfer.prototype = {
   },
 
   //////////////////////////////////////////////////////////////////////////////
   //// nsITransfer
 
   init: function DLT_init(aSource, aTarget, aDisplayName, aMIMEInfo, aStartTime,
                           aTempFile, aCancelable, aIsPrivate)
   {
+    this._cancelable = aCancelable;
+
     let launchWhenSucceeded = false, contentType = null, launcherPath = null;
 
     if (aMIMEInfo instanceof Ci.nsIMIMEInfo) {
       launchWhenSucceeded =
                 aMIMEInfo.preferredAction != Ci.nsIMIMEInfo.saveToDisk;
       contentType = aMIMEInfo.type;
 
       let appHandler = aMIMEInfo.preferredApplicationHandler;
@@ -229,16 +236,22 @@ DownloadLegacyTransfer.prototype = {
 
   /**
    * This deferred object contains a promise that is resolved with the Download
    * object associated with this nsITransfer instance, when it is available.
    */
   _deferDownload: null,
 
   /**
+   * Reference to the component that is executing the download.  This component
+   * allows cancellation through its nsICancelable interface.
+   */
+  _cancelable: null,
+
+  /**
    * Indicates that the component that executes the download has notified a
    * failure condition.  In this case, we should never use the component methods
    * that cancel the download.
    */
   _componentFailed: false,
 };
 
 ////////////////////////////////////////////////////////////////////////////////
--- a/toolkit/components/jsdownloads/test/unit/common_test_Download.js
+++ b/toolkit/components/jsdownloads/test/unit/common_test_Download.js
@@ -1671,8 +1671,66 @@ add_task(function test_platform_integrat
     });
 
     // Then, wait for the promise returned by "start" to be resolved.
     yield promiseDownloadStopped(download);
 
     yield promiseVerifyContents(download.target.path, TEST_DATA_SHORT);
   }
 });
+
+/**
+ * Checks that downloads are added to browsing history when they start.
+ */
+add_task(function test_history()
+{
+  mustInterruptResponses();
+
+  // We will wait for the visit to be notified during the download.
+  yield promiseClearHistory();
+  let promiseVisit = promiseWaitForVisit(httpUrl("interruptible.txt"));
+
+  // Start a download that is not allowed to finish yet.
+  let download = yield promiseStartDownload(httpUrl("interruptible.txt"));
+
+  // The history notifications should be received before the download completes.
+  let [time, transitionType] = yield promiseVisit;
+  do_check_eq(time, download.startTime.getTime() * 1000);
+  do_check_eq(transitionType, Ci.nsINavHistoryService.TRANSITION_DOWNLOAD);
+
+  // Restart and complete the download after clearing history.
+  yield promiseClearHistory();
+  download.cancel();
+  continueResponses();
+  yield download.start();
+
+  // The restart should not have added a new history visit.
+  do_check_false(yield promiseIsURIVisited(httpUrl("interruptible.txt")));
+});
+
+/**
+ * Checks that downloads started by nsIHelperAppService are added to the
+ * browsing history when they start.
+ */
+add_task(function test_history_tryToKeepPartialData()
+{
+  // We will wait for the visit to be notified during the download.
+  yield promiseClearHistory();
+  let promiseVisit =
+      promiseWaitForVisit(httpUrl("interruptible_resumable.txt"));
+
+  // Start a download that is not allowed to finish yet.
+  let beforeStartTimeMs = Date.now();
+  let download = yield promiseStartDownload_tryToKeepPartialData();
+
+  // The history notifications should be received before the download completes.
+  let [time, transitionType] = yield promiseVisit;
+  do_check_eq(transitionType, Ci.nsINavHistoryService.TRANSITION_DOWNLOAD);
+
+  // The time set by nsIHelperAppService may be different than the start time in
+  // the download object, thus we only check that it is a meaningful time.  Note
+  // that we subtract one second from the earliest time to account for rounding.
+  do_check_true(time >= beforeStartTimeMs * 1000 - 1000000);
+
+  // Complete the download before finishing the test.
+  continueResponses();
+  yield promiseDownloadStopped(download);
+});
--- a/toolkit/components/jsdownloads/test/unit/head.js
+++ b/toolkit/components/jsdownloads/test/unit/head.js
@@ -165,16 +165,111 @@ function promiseExecuteSoon()
 function promiseTimeout(aTime)
 {
   let deferred = Promise.defer();
   do_timeout(aTime, deferred.resolve);
   return deferred.promise;
 }
 
 /**
+ * Allows waiting for an observer notification once.
+ *
+ * @param aTopic
+ *        Notification topic to observe.
+ *
+ * @return {Promise}
+ * @resolves The array [aSubject, aData] from the observed notification.
+ * @rejects Never.
+ */
+function promiseTopicObserved(aTopic)
+{
+  let deferred = Promise.defer();
+
+  Services.obs.addObserver(
+    function PTO_observe(aSubject, aTopic, aData) {
+      Services.obs.removeObserver(PTO_observe, aTopic);
+      deferred.resolve([aSubject, aData]);
+    }, aTopic, false);
+
+  return deferred.promise;
+}
+
+/**
+ * Clears history asynchronously.
+ *
+ * @return {Promise}
+ * @resolves When history has been cleared.
+ * @rejects Never.
+ */
+function promiseClearHistory()
+{
+  let promise = promiseTopicObserved(PlacesUtils.TOPIC_EXPIRATION_FINISHED);
+  do_execute_soon(function() PlacesUtils.bhistory.removeAllPages());
+  return promise;
+}
+
+/**
+ * Waits for a new history visit to be notified for the specified URI.
+ *
+ * @param aUrl
+ *        String containing the URI that will be visited.
+ *
+ * @return {Promise}
+ * @resolves Array [aTime, aTransitionType] from nsINavHistoryObserver.onVisit.
+ * @rejects Never.
+ */
+function promiseWaitForVisit(aUrl)
+{
+  let deferred = Promise.defer();
+
+  let uri = NetUtil.newURI(aUrl);
+
+  PlacesUtils.history.addObserver({
+    QueryInterface: XPCOMUtils.generateQI([Ci.nsINavHistoryObserver]),
+    onBeginUpdateBatch: function () {},
+    onEndUpdateBatch: function () {},
+    onVisit: function (aURI, aVisitID, aTime, aSessionID, aReferringID,
+                       aTransitionType, aGUID, aHidden) {
+      if (aURI.equals(uri)) {
+        PlacesUtils.history.removeObserver(this);
+        deferred.resolve([aTime, aTransitionType]);
+      }
+    },
+    onTitleChanged: function () {},
+    onDeleteURI: function () {},
+    onClearHistory: function () {},
+    onPageChanged: function () {},
+    onDeleteVisits: function () {},
+  }, false);
+
+  return deferred.promise;
+}
+
+/**
+ * Check browsing history to see whether the given URI has been visited.
+ *
+ * @param aUrl
+ *        String containing the URI that will be visited.
+ *
+ * @return {Promise}
+ * @resolves Boolean indicating whether the URI has been visited.
+ * @rejects JavaScript exception.
+ */
+function promiseIsURIVisited(aUrl) {
+  let deferred = Promise.defer();
+
+  PlacesUtils.asyncHistory.isURIVisited(NetUtil.newURI(aUrl),
+    function (aURI, aIsVisited) {
+      deferred.resolve(aIsVisited);
+    });
+
+  return deferred.promise;
+}
+
+/**
  * Creates a new Download object, setting a temporary file as the target.
  *
  * @param aSourceUrl
  *        String containing the URI for the download source, or null to use
  *        httpUrl("source.txt").
  *
  * @return {Promise}
  * @resolves The newly created Download object.
@@ -437,50 +532,16 @@ function promiseVerifyContents(aPath, aE
       }
       deferred.resolve();
     });
     yield deferred.promise;
   });
 }
 
 /**
- * Adds entry for download.
- *
- * @param aSourceUrl
- *        String containing the URI for the download source, or null to use
- *        httpUrl("source.txt").
- *
- * @return {Promise}
- * @rejects JavaScript exception.
- */
-function promiseAddDownloadToHistory(aSourceUrl) {
-  let deferred = Promise.defer();
-  PlacesUtils.asyncHistory.updatePlaces(
-    {
-      uri: NetUtil.newURI(aSourceUrl || httpUrl("source.txt")),
-      visits: [{
-        transitionType: Ci.nsINavHistoryService.TRANSITION_DOWNLOAD,
-        visitDate:  Date.now()
-      }]
-    },
-    {
-      handleError: function handleError(aResultCode, aPlaceInfo) {
-        let ex = new Components.Exception("Unexpected error in adding visits.",
-                                          aResultCode);
-        deferred.reject(ex);
-      },
-      handleResult: function () {},
-      handleCompletion: function handleCompletion() {
-        deferred.resolve();
-      }
-    });
-  return deferred.promise;
-}
-
-/**
  * Starts a socket listener that closes each incoming connection.
  *
  * @returns nsIServerSocket that listens for connections.  Call its "close"
  *          method to stop listening and free the server port.
  */
 function startFakeServer()
 {
   let serverSocket = new ServerSocket(-1, true, -1);
--- a/toolkit/components/jsdownloads/test/unit/test_DownloadList.js
+++ b/toolkit/components/jsdownloads/test/unit/test_DownloadList.js
@@ -5,16 +5,72 @@
 
 /**
  * Tests the DownloadList object.
  */
 
 "use strict";
 
 ////////////////////////////////////////////////////////////////////////////////
+//// Globals
+
+/**
+ * Returns a PRTime in the past usable to add expirable visits.
+ *
+ * @note Expiration ignores any visit added in the last 7 days, but it's
+ *       better be safe against DST issues, by going back one day more.
+ */
+function getExpirablePRTime()
+{
+  let dateObj = new Date();
+  // Normalize to midnight
+  dateObj.setHours(0);
+  dateObj.setMinutes(0);
+  dateObj.setSeconds(0);
+  dateObj.setMilliseconds(0);
+  dateObj = new Date(dateObj.getTime() - 8 * 86400000);
+  return dateObj.getTime() * 1000;
+}
+
+/**
+ * Adds an expirable history visit for a download.
+ *
+ * @param aSourceUrl
+ *        String containing the URI for the download source, or null to use
+ *        httpUrl("source.txt").
+ *
+ * @return {Promise}
+ * @rejects JavaScript exception.
+ */
+function promiseExpirableDownloadVisit(aSourceUrl)
+{
+  let deferred = Promise.defer();
+  PlacesUtils.asyncHistory.updatePlaces(
+    {
+      uri: NetUtil.newURI(aSourceUrl || httpUrl("source.txt")),
+      visits: [{
+        transitionType: Ci.nsINavHistoryService.TRANSITION_DOWNLOAD,
+        visitDate: getExpirablePRTime(),
+      }]
+    },
+    {
+      handleError: function handleError(aResultCode, aPlaceInfo) {
+        let ex = new Components.Exception("Unexpected error in adding visits.",
+                                          aResultCode);
+        deferred.reject(ex);
+      },
+      handleResult: function () {},
+      handleCompletion: function handleCompletion() {
+        deferred.resolve();
+      }
+    });
+  return deferred.promise;
+}
+
+////////////////////////////////////////////////////////////////////////////////
 //// Tests
 
 /**
  * Checks the testing mechanism used to build different download lists.
  */
 add_task(function test_construction()
 {
   let downloadListOne = yield promiseNewDownloadList();
@@ -200,71 +256,72 @@ add_task(function test_notifications_thi
   do_check_true(receivedOnDownloadRemoved);
 });
 
 /**
  * Checks that download is removed on history expiration.
  */
 add_task(function test_history_expiration()
 {
+  mustInterruptResponses();
+
   function cleanup() {
     Services.prefs.clearUserPref("places.history.expiration.max_pages");
   }
   do_register_cleanup(cleanup);
 
   // Set max pages to 0 to make the download expire.
   Services.prefs.setIntPref("places.history.expiration.max_pages", 0);
 
-  // Add expirable visit for downloads.
-  yield promiseAddDownloadToHistory();
-  yield promiseAddDownloadToHistory(httpUrl("interruptible.txt"));
-
   let list = yield promiseNewDownloadList();
   let downloadOne = yield promiseNewDownload();
   let downloadTwo = yield promiseNewDownload(httpUrl("interruptible.txt"));
-  list.add(downloadOne);
-  list.add(downloadTwo);
 
   let deferred = Promise.defer();
   let removeNotifications = 0;
   let downloadView = {
     onDownloadRemoved: function (aDownload) {
       if (++removeNotifications == 2) {
         deferred.resolve();
       }
     },
   };
   list.addView(downloadView);
 
-  // Start download one.
+  // Work with one finished download and one canceled download.
   yield downloadOne.start();
-
-  // Start download two and then cancel it.
   downloadTwo.start();
   yield downloadTwo.cancel();
 
+  // We must replace the visits added while executing the downloads with visits
+  // that are older than 7 days, otherwise they will not be expired.
+  yield promiseClearHistory();
+  yield promiseExpirableDownloadVisit();
+  yield promiseExpirableDownloadVisit(httpUrl("interruptible.txt"));
+
+  // After clearing history, we can add the downloads to be removed to the list.
+  list.add(downloadOne);
+  list.add(downloadTwo);
+
   // Force a history expiration.
   let expire = Cc["@mozilla.org/places/expiration;1"]
                  .getService(Ci.nsIObserver);
   expire.observe(null, "places-debug-start-expiration", -1);
 
+  // Wait for both downloads to be removed.
   yield deferred.promise;
 
   cleanup();
 });
 
 /**
  * Checks all downloads are removed after clearing history.
  */
 add_task(function test_history_clear()
 {
-  // Add expirable visit for downloads.
-  yield promiseAddDownloadToHistory();
-  yield promiseAddDownloadToHistory();
-
   let list = yield promiseNewDownloadList();
   let downloadOne = yield promiseNewDownload();
   let downloadTwo = yield promiseNewDownload();
   list.add(downloadOne);
   list.add(downloadTwo);
 
   let deferred = Promise.defer();
   let removeNotifications = 0;
@@ -275,18 +332,19 @@ add_task(function test_history_clear()
       }
     },
   };
   list.addView(downloadView);
 
   yield downloadOne.start();
   yield downloadTwo.start();
 
-  PlacesUtils.history.removeAllPages();
+  yield promiseClearHistory();
 
+  // Wait for the removal notifications that may still be pending.
   yield deferred.promise;
 });
 
 /**
  * Tests the removeFinished method to ensure that it only removes
  * finished downloads.
  */
 add_task(function test_removeFinished()
--- a/toolkit/content/widgets/preferences.xml
+++ b/toolkit/content/widgets/preferences.xml
@@ -725,17 +725,17 @@
         <parameter name="aPaneElement"/>
         <body>
         <![CDATA[
           if (!aPaneElement)
             return;
 
           this._selector.selectedItem = document.getAnonymousElementByAttribute(this, "pane", aPaneElement.id);
           if (!aPaneElement.loaded) {
-            function OverlayLoadObserver(aPane)
+            let OverlayLoadObserver = function(aPane)
             {
               this._pane = aPane;
             }
             OverlayLoadObserver.prototype = { 
               _outer: this,
               observe: function (aSubject, aTopic, aData) 
               {
                 this._pane.loaded = true;
--- a/uriloader/exthandler/tests/unit/xpcshell.ini
+++ b/uriloader/exthandler/tests/unit/xpcshell.ini
@@ -1,11 +1,12 @@
 [DEFAULT]
 head = head_handlerService.js
 tail = tail_handlerService.js
+run-sequentially = Bug 912235 - Intermittent failures
 
 [test_getTypeFromExtension_ext_to_type_mapping.js]
 [test_getTypeFromExtension_with_empty_Content_Type.js]
 [test_handlerService.js]
 # Bug 676997: test consistently fails on Android
 fail-if = os == "android"
 [test_punycodeURIs.js]
 # Bug 676997: test consistently fails on Android