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