Bug 1256652 - [webext] Initial support of webNavigation transition types and qualifiers. r?krizsa draft
authorLuca Greco <lgreco@mozilla.com>
Fri, 15 Apr 2016 14:49:13 +0200
changeset 352270 30b6f4444580b6ca9b09811c1696f6c01f43b0f9
parent 352269 62d9aed3484540777bb3b6285c911c8aa02271af
child 352271 e502766b3b95fcd8ff8a43e4a6a5655fb42eeada
push id15670
push userluca.greco@alcacoop.it
push dateSat, 16 Apr 2016 12:49:27 +0000
reviewerskrizsa
bugs1256652
milestone48.0a1
Bug 1256652 - [webext] Initial support of webNavigation transition types and qualifiers. r?krizsa - transition types: reload, link, auto_subframe - transition qualifiers: forward_back, server_redirect MozReview-Commit-ID: Bx3oG2fuWuv
toolkit/components/extensions/ext-webNavigation.js
toolkit/components/extensions/test/mochitest/test_ext_webnavigation.html
toolkit/modules/addons/WebNavigation.jsm
toolkit/modules/addons/WebNavigationContent.js
--- a/toolkit/components/extensions/ext-webNavigation.js
+++ b/toolkit/components/extensions/ext-webNavigation.js
@@ -13,16 +13,70 @@ XPCOMUtils.defineLazyModuleGetter(this, 
 
 Cu.import("resource://gre/modules/ExtensionUtils.jsm");
 var {
   SingletonEventManager,
   ignoreEvent,
   runSafe,
 } = ExtensionUtils;
 
+const defaultTransitionTypes = {
+  topFrame: "link",
+  subFrame: "auto_subframe",
+};
+
+const frameTransitions = {
+  anyFrame: {
+    qualifiers: ["server_redirect", "client_redirect", "forward_back"],
+  },
+  topFrame: {
+    types: ["reload", "form_submit"],
+  },
+};
+
+function isTopLevelFrame({frameId, parentFrameId}) {
+  return frameId == 0 && parentFrameId == -1;
+}
+
+function fillTransitionProperties(eventName, src, dst) {
+  if (eventName == "onCommitted" || eventName == "onHistoryStateUpdated") {
+    let frameTransitionData = src.frameTransitionData || {};
+
+    let transitionType, transitionQualifiers = [];
+
+    // Fill transition properties for any frame.
+    for (let qualifier of frameTransitions.anyFrame.qualifiers) {
+      if (frameTransitionData[qualifier]) {
+        transitionQualifiers.push(qualifier);
+      }
+    }
+
+    if (isTopLevelFrame(dst)) {
+      for (let type of frameTransitions.topFrame.types) {
+        if (frameTransitionData[type]) {
+          transitionType = type;
+        }
+      }
+
+      // If transitionType is not defined, defaults it to "link".
+      if (!transitionType) {
+        transitionType = defaultTransitionTypes.topFrame;
+      }
+    } else {
+      // If it is sub-frame, transitionType defaults it to "auto_subframe",
+      // "manual_subframe" is set only in case of a recent user interaction.
+      transitionType = defaultTransitionTypes.subFrame;
+    }
+
+    // Fill the transition properties in the webNavigation event object.
+    dst.transitionType = transitionType;
+    dst.transitionQualifiers = transitionQualifiers;
+  }
+}
+
 // Similar to WebRequestEventManager but for WebNavigation.
 function WebNavigationEventManager(context, eventName) {
   let name = `webNavigation.${eventName}`;
   let register = callback => {
     let listener = data => {
       if (!data.browser) {
         return;
       }
@@ -41,16 +95,18 @@ function WebNavigationEventManager(conte
 
       // Fills in tabId typically.
       let result = {};
       extensions.emit("fill-browser-data", data.browser, data2, result);
       if (result.cancel) {
         return;
       }
 
+      fillTransitionProperties(eventName, data, data2);
+
       runSafe(context, callback, data2);
     };
 
     WebNavigation[eventName].addListener(listener);
     return () => {
       WebNavigation[eventName].removeListener(listener);
     };
   };
--- a/toolkit/components/extensions/test/mochitest/test_ext_webnavigation.html
+++ b/toolkit/components/extensions/test/mochitest/test_ext_webnavigation.html
@@ -65,16 +65,18 @@ function backgroundScript() {
   browser.test.sendMessage("ready");
 }
 
 const BASE = "http://mochi.test:8888/tests/toolkit/components/extensions/test/mochitest";
 const URL = BASE + "/file_WebNavigation_page1.html";
 const FRAME = BASE + "/file_WebNavigation_page2.html";
 const FRAME2 = BASE + "/file_WebNavigation_page3.html";
 const FRAME_PUSHSTATE = BASE + "/file_WebNavigation_page3_pushState.html";
+const REDIRECT = BASE + "/redirection.sjs";
+const REDIRECTED = BASE + "/dummy_page.html";
 
 const REQUIRED = [
   "onBeforeNavigate",
   "onCommitted",
   "onDOMContentLoaded",
   "onCompleted",
 ];
 
@@ -86,16 +88,130 @@ function loadAndWait(win, event, url, sc
   received = [];
   waitingEvent = event;
   waitingURL = url;
   dump(`RUN ${script}\n`);
   script();
   return new Promise(resolve => { completedResolve = resolve; });
 }
 
+add_task(function* webnav_transitions_props() {
+  function backgroundScriptTransitions() {
+    const EVENTS = [
+      "onCommitted",
+      "onCompleted",
+    ];
+
+    function gotEvent(event, details) {
+      browser.test.log(`Got ${event} ${details.url} ${details.transitionType}`);
+
+      browser.test.sendMessage("received", {url: details.url, details, event});
+    }
+
+    let listeners = {};
+    for (let event of EVENTS) {
+      listeners[event] = gotEvent.bind(null, event);
+      browser.webNavigation[event].addListener(listeners[event]);
+    }
+
+    browser.test.sendMessage("ready");
+  }
+
+  let extensionData = {
+    manifest: {
+      permissions: [
+        "webNavigation",
+      ],
+    },
+    background: `(${backgroundScriptTransitions})()`,
+  };
+
+  let extension = ExtensionTestUtils.loadExtension(extensionData);
+
+  extension.onMessage("received", ({url, event, details}) => {
+    received.push({url, event, details});
+
+    if (event == waitingEvent && url == waitingURL) {
+      completedResolve();
+    }
+  });
+
+  yield Promise.all([extension.startup(), extension.awaitMessage("ready")]);
+  info("webnavigation extension loaded");
+
+  let win = window.open();
+
+  yield loadAndWait(win, "onCompleted", URL, () => { win.location = URL; });
+
+  // transitionType: reload
+  received = [];
+  yield loadAndWait(win, "onCompleted", URL, () => { win.location.reload(); });
+
+  let found = received.find((data) => (data.event == "onCommitted" && data.url == URL));
+
+  ok(found, "Got the onCommitted event");
+
+  if (found) {
+    is(found.details.transitionType, "reload",
+       "Got the expected 'reload' transitionType in the OnCommitted event");
+    ok(Array.isArray(found.details.transitionQualifiers),
+       "transitionQualifiers found in the OnCommitted events");
+  }
+
+  // transitionType: auto_subframe
+  found = received.find((data) => (data.event == "onCommitted" && data.url == FRAME));
+
+  ok(found, "Got the sub-frame onCommitted event");
+
+  if (found) {
+    is(found.details.transitionType, "auto_subframe",
+       "Got the expected 'auto_subframe' transitionType in the OnCommitted event");
+    ok(Array.isArray(found.details.transitionQualifiers),
+       "transitionQualifiers found in the OnCommitted events");
+  }
+
+  // transitionQualifier: server_redirect
+  received = [];
+  yield loadAndWait(win, "onCompleted", REDIRECTED, () => { win.location = REDIRECT; });
+
+  found = received.find((data) => (data.event == "onCommitted" && data.url == REDIRECTED));
+
+  ok(found, "Got the onCommitted event");
+
+  if (found) {
+    is(found.details.transitionType, "link",
+       "Got the expected 'link' transitionType in the OnCommitted event");
+    ok(Array.isArray(found.details.transitionQualifiers) &&
+       found.details.transitionQualifiers.find((q) => q == "server_redirect"),
+       "Got the expected 'server_redirect' transitionQualifiers in the OnCommitted events");
+  }
+
+  // transitionQualifier: forward_back
+  received = [];
+  yield loadAndWait(win, "onCompleted", URL, () => { win.history.back(); });
+
+  found = received.find((data) => (data.event == "onCommitted" && data.url == URL));
+
+  ok(found, "Got the onCommitted event");
+
+  if (found) {
+    is(found.details.transitionType, "link",
+       "Got the expected 'link' transitionType in the OnCommitted event");
+    ok(Array.isArray(found.details.transitionQualifiers) &&
+       found.details.transitionQualifiers.find((q) => q == "forward_back"),
+       "Got the expected 'forward_back' transitionQualifiers in the OnCommitted events");
+  }
+
+  // cleanup phase
+  win.close();
+
+  yield extension.unload();
+  info("webnavigation extension unloaded");
+});
+
 add_task(function* webnav_ordering() {
   let extensionData = {
     manifest: {
       permissions: [
         "webNavigation",
       ],
     },
     background: "(" + backgroundScript.toString() + ")()",
--- a/toolkit/modules/addons/WebNavigation.jsm
+++ b/toolkit/modules/addons/WebNavigation.jsm
@@ -9,20 +9,17 @@ const EXPORTED_SYMBOLS = ["WebNavigation
 const Ci = Components.interfaces;
 const Cc = Components.classes;
 const Cu = Components.utils;
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
 
 // TODO:
-// Transition types and qualifiers
-// onReferenceFragmentUpdated also triggers for pushState
-// getFrames, getAllFrames
-// onCreatedNavigationTarget, onHistoryStateUpdated
+// onCreatedNavigationTarget
 
 var Manager = {
   listeners: new Map(),
 
   init() {
     Services.mm.addMessageListener("Extension:DOMContentLoaded", this);
     Services.mm.addMessageListener("Extension:StateChange", this);
     Services.mm.addMessageListener("Extension:DocumentChange", this);
@@ -99,28 +96,36 @@ var Manager = {
           let error = `Error code ${data.status}`;
           this.fire("onErrorOccurred", browser, data, {error, url});
         }
       }
     }
   },
 
   onDocumentChange(browser, data) {
-    let url = data.location;
+    let extra = {
+      url: data.location,
+      // Transition data which is coming from the content process.
+      frameTransitionData: data.frameTransitionData,
+    };
 
-    this.fire("onCommitted", browser, data, {url});
+    this.fire("onCommitted", browser, data, extra);
   },
 
   onHistoryChange(browser, data) {
-    let url = data.location;
+    let extra = {
+      url: data.location,
+      // Transition data which is coming from the content process.
+      frameTransitionData: data.frameTransitionData,
+    };
 
     if (data.isReferenceFragmentUpdated) {
-      this.fire("onReferenceFragmentUpdated", browser, data, {url});
+      this.fire("onReferenceFragmentUpdated", browser, data, extra);
     } else if (data.isHistoryStateUpdated) {
-      this.fire("onHistoryStateUpdated", browser, data, {url});
+      this.fire("onHistoryStateUpdated", browser, data, extra);
     }
   },
 
   onLoad(browser, data) {
     this.fire("onDOMContentLoaded", browser, data, {url: data.url});
   },
 
   fire(type, browser, data, extra) {
--- a/toolkit/modules/addons/WebNavigationContent.js
+++ b/toolkit/modules/addons/WebNavigationContent.js
@@ -76,17 +76,17 @@ var WebProgressListener = {
     // initial navigation of a sub-frame.
     // For the above two reasons, when the navigation event is related to
     // a sub-frame we process the document change here and
     // then send an "Extension:DocumentChange" message to the main process,
     // where it will be turned into a webNavigation.onCommitted event.
     // (see Bug 1264936 and Bug 125662 for rationale)
     if ((webProgress.DOMWindow.top != webProgress.DOMWindow) &&
         (stateFlags & Ci.nsIWebProgressListener.STATE_IS_DOCUMENT)) {
-      this.sendDocumentChange({webProgress, locationURI});
+      this.sendDocumentChange({webProgress, locationURI, request});
     }
   },
 
   onLocationChange: function onLocationChange(webProgress, request, locationURI, flags) {
     let {DOMWindow} = webProgress;
 
     // Get the previous URI loaded in the DOMWindow.
     let previousURI = this.previousURIMap.get(DOMWindow);
@@ -98,17 +98,17 @@ var WebProgressListener = {
 
     // When a frame navigation doesn't change the current loaded document
     // (which can be due to history.pushState/replaceState or to a changed hash in the url),
     // it is reported only to the onLocationChange, for this reason
     // we process the history change here and then we are going to send
     // an "Extension:HistoryChange" to the main process, where it will be turned
     // into a webNavigation.onHistoryStateUpdated/onReferenceFragmentUpdated event.
     if (isSameDocument) {
-      this.sendHistoryChange({webProgress, previousURI, locationURI});
+      this.sendHistoryChange({webProgress, previousURI, locationURI, request});
     } else if (webProgress.DOMWindow.top == webProgress.DOMWindow) {
       // We have to catch the document changes from top level frames here,
       // where we can detect the "server redirect" transition.
       // (see Bug 1264936 and Bug 125662 for rationale)
       this.sendDocumentChange({webProgress, locationURI, request});
     }
   },
 
@@ -119,27 +119,31 @@ var WebProgressListener = {
       parentWindowId: WebNavigationFrames.getParentWindowId(webProgress.DOMWindow),
       status,
       stateFlags,
     };
 
     sendAsyncMessage("Extension:StateChange", data);
   },
 
-  sendDocumentChange({webProgress, locationURI}) {
+  sendDocumentChange({webProgress, locationURI, request}) {
+    let {loadType} = webProgress;
+    let frameTransitionData = this.getFrameTransitionData({loadType, request});
+
     let data = {
+      frameTransitionData,
       location: locationURI ? locationURI.spec : "",
       windowId: webProgress.DOMWindowID,
       parentWindowId: WebNavigationFrames.getParentWindowId(webProgress.DOMWindow),
     };
 
     sendAsyncMessage("Extension:DocumentChange", data);
   },
 
-  sendHistoryChange({webProgress, previousURI, locationURI}) {
+  sendHistoryChange({webProgress, previousURI, locationURI, request}) {
     let {loadType} = webProgress;
 
     let isHistoryStateUpdated = false;
     let isReferenceFragmentUpdated = false;
 
     let pathChanged = !(previousURI && locationURI.equalsExceptRef(previousURI));
     let hashChanged = !(previousURI && previousURI.ref == locationURI.ref);
 
@@ -154,27 +158,51 @@ var WebProgressListener = {
       isReferenceFragmentUpdated = true;
     } else if (loadType & Ci.nsIDocShell.LOAD_CMD_PUSHSTATE) {
       isHistoryStateUpdated = true;
     } else if (loadType & Ci.nsIDocShell.LOAD_CMD_HISTORY) {
       isHistoryStateUpdated = true;
     }
 
     if (isHistoryStateUpdated || isReferenceFragmentUpdated) {
+      let frameTransitionData = this.getFrameTransitionData({loadType, request});
+
       let data = {
+        frameTransitionData,
         isHistoryStateUpdated, isReferenceFragmentUpdated,
         location: locationURI ? locationURI.spec : "",
         windowId: webProgress.DOMWindowID,
         parentWindowId: WebNavigationFrames.getParentWindowId(webProgress.DOMWindow),
       };
 
       sendAsyncMessage("Extension:HistoryChange", data);
     }
   },
 
+  getFrameTransitionData({loadType, request}) {
+    let frameTransitionData = {};
+
+    if (loadType & Ci.nsIDocShell.LOAD_CMD_HISTORY) {
+      frameTransitionData.forward_back = true;
+    }
+
+    if (loadType & Ci.nsIDocShell.LOAD_CMD_RELOAD) {
+      frameTransitionData.reload = true;
+    }
+
+    if (request instanceof Ci.nsIChannel) {
+      if (request.loadInfo.redirectChain.length) {
+        frameTransitionData.server_redirect = true;
+      }
+    }
+
+    return frameTransitionData;
+  },
+
+
   QueryInterface: XPCOMUtils.generateQI([Ci.nsIWebProgressListener, Ci.nsISupportsWeakReference]),
 };
 
 var disabled = false;
 WebProgressListener.init();
 addEventListener("unload", () => {
   if (!disabled) {
     disabled = true;