new file mode 100644
--- /dev/null
+++ b/browser/components/extensions/ext-sessions.js
@@ -0,0 +1,40 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+XPCOMUtils.defineLazyModuleGetter(this, "SessionStore",
+ "resource:///modules/sessionstore/SessionStore.jsm");
+
+extensions.registerSchemaAPI("sessions", "addon_parent", context => {
+ let {extension} = context;
+ return {
+ sessions: {
+ getRecentlyClosed: function(filter) {
+ let recentlyClosed = [];
+
+ // Get closed windows
+ let closedWindowData = JSON.parse(SessionStore.getClosedWindowData());
+ closedWindowData.forEach((window, index) => {
+ recentlyClosed.push({
+ lastModified: window.closedAt,
+ window: WindowManager.convertClosed(window, index, extension)});
+ });
+
+ //Get closed tabs
+ let windows = Array.from(WindowListManager.browserWindows());
+ for (let window of windows) {
+ let closedTabData = JSON.parse(SessionStore.getClosedTabData(window));
+ closedTabData.forEach((tab, index) => {
+ recentlyClosed.push({
+ lastModified: tab.closedAt,
+ tab: TabManager.for(extension).convertClosed(tab, window, index)});
+ });
+ }
+
+ // Sort windows and tabs
+ recentlyClosed.sort((a, b) => b.lastModified - a.lastModified);
+ return Promise.resolve(recentlyClosed);
+ },
+ },
+ };
+});
--- a/browser/components/extensions/ext-utils.js
+++ b/browser/components/extensions/ext-utils.js
@@ -720,16 +720,33 @@ ExtensionTabManager.prototype = {
if (icon) {
result.favIconUrl = icon;
}
}
return result;
},
+ // Converts tabs returned from SessionStore.getClosedTabData and
+ // SessionStore.getClosedWindowData into API tab objects
+ convertClosed(tab, window, index) {
+ let result = {
+ sessionId: index,
+ index: tab.pos ? tab.pos : 0,
+ windowId: WindowManager.getId(window),
+ selected: false,
+ highlighted: false,
+ active: false,
+ pinned: false,
+ incognito: false,
+ };
+
+ return result;
+ },
+
getTabs(window) {
return Array.from(window.gBrowser.tabs)
.filter(tab => !tab.closing)
.map(tab => this.convert(tab));
},
};
@@ -936,16 +953,29 @@ global.WindowManager = {
for (let window of WindowListManager.browserWindows(true)) {
if (this.getId(window) == id) {
return window;
}
}
return null;
},
+ getState(window) {
+ const STATES = {
+ [window.STATE_MAXIMIZED]: "maximized",
+ [window.STATE_MINIMIZED]: "minimized",
+ [window.STATE_NORMAL]: "normal",
+ };
+ let state = STATES[window.windowState];
+ if (window.fullScreen) {
+ state = "fullscreen";
+ }
+ return state;
+ },
+
setState(window, state) {
if (state != "fullscreen" && window.fullScreen) {
window.fullScreen = false;
}
switch (state) {
case "maximized":
window.maximize();
@@ -976,50 +1006,67 @@ global.WindowManager = {
break;
default:
throw new Error(`Unexpected window state: ${state}`);
}
},
convert(extension, window, getInfo) {
- const STATES = {
- [window.STATE_MAXIMIZED]: "maximized",
- [window.STATE_MINIMIZED]: "minimized",
- [window.STATE_NORMAL]: "normal",
- };
- let state = STATES[window.windowState];
- if (window.fullScreen) {
- state = "fullscreen";
- }
-
let xulWindow = window.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIDocShell)
.treeOwner.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIXULWindow);
let result = {
id: this.getId(window),
focused: window.document.hasFocus(),
top: window.screenY,
left: window.screenX,
width: window.outerWidth,
height: window.outerHeight,
incognito: PrivateBrowsingUtils.isWindowPrivate(window),
type: this.windowType(window),
- state,
+ state: this.getState(window),
alwaysOnTop: xulWindow.zLevel >= Ci.nsIXULWindow.raisedZ,
};
if (getInfo && getInfo.populate) {
result.tabs = TabManager.for(extension).getTabs(window);
}
return result;
},
+
+ // Converts windows returned from SessionStore.getClosedWindowData
+ // into API window objects
+ convertClosed(window, index, extension) {
+ let result = {
+ sessionId: index,
+ focused: false,
+ top: window.screenY,
+ left: window.screenX,
+ width: window.width,
+ height: window.height,
+ incognito: false,
+ // TODO: can we support any type other than "normal"?
+ type: "normal",
+ state: this.getState(window),
+ alwaysOnTop: false,
+ };
+
+ if (window.tabs.length) {
+ result.tabs = [];
+ window.tabs.forEach((tab, index) => {
+ result.tabs.push(TabManager.for(extension).convertClosed(tab, window, index));
+ });
+ }
+
+ return result;
+ },
};
// Manages listeners for window opening and closing. A window is
// considered open when the "load" event fires on it. A window is
// closed when a "domwindowclosed" notification fires for it.
global.WindowListManager = {
_openListeners: new Set(),
_closeListeners: new Set(),
--- a/browser/components/extensions/extensions-browser.manifest
+++ b/browser/components/extensions/extensions-browser.manifest
@@ -1,25 +1,27 @@
# scripts
category webextension-scripts bookmarks chrome://browser/content/ext-bookmarks.js
category webextension-scripts browserAction chrome://browser/content/ext-browserAction.js
category webextension-scripts commands chrome://browser/content/ext-commands.js
category webextension-scripts contextMenus chrome://browser/content/ext-contextMenus.js
category webextension-scripts desktop-runtime chrome://browser/content/ext-desktop-runtime.js
category webextension-scripts history chrome://browser/content/ext-history.js
category webextension-scripts pageAction chrome://browser/content/ext-pageAction.js
+category webextension-scripts sessions chrome://browser/content/ext-sessions.js
category webextension-scripts tabs chrome://browser/content/ext-tabs.js
category webextension-scripts utils chrome://browser/content/ext-utils.js
category webextension-scripts windows chrome://browser/content/ext-windows.js
# scripts that must run in the same process as addon code.
category webextension-scripts-addon tabs chrome://browser/content/ext-c-tabs.js
# schemas
category webextension-schemas bookmarks chrome://browser/content/schemas/bookmarks.json
category webextension-schemas browser_action chrome://browser/content/schemas/browser_action.json
category webextension-schemas commands chrome://browser/content/schemas/commands.json
category webextension-schemas context_menus chrome://browser/content/schemas/context_menus.json
category webextension-schemas context_menus_internal chrome://browser/content/schemas/context_menus_internal.json
category webextension-schemas history chrome://browser/content/schemas/history.json
category webextension-schemas page_action chrome://browser/content/schemas/page_action.json
+category webextension-schemas sessions chrome://browser/content/schemas/sessions.json
category webextension-schemas tabs chrome://browser/content/schemas/tabs.json
category webextension-schemas windows chrome://browser/content/schemas/windows.json
--- a/browser/components/extensions/jar.mn
+++ b/browser/components/extensions/jar.mn
@@ -14,12 +14,13 @@ browser.jar:
content/browser/extension.svg
content/browser/ext-bookmarks.js
content/browser/ext-browserAction.js
content/browser/ext-commands.js
content/browser/ext-contextMenus.js
content/browser/ext-desktop-runtime.js
content/browser/ext-history.js
content/browser/ext-pageAction.js
+ content/browser/ext-sessions.js
content/browser/ext-tabs.js
content/browser/ext-utils.js
content/browser/ext-windows.js
content/browser/ext-c-tabs.js
--- a/browser/components/extensions/schemas/jar.mn
+++ b/browser/components/extensions/schemas/jar.mn
@@ -5,10 +5,11 @@
browser.jar:
content/browser/schemas/bookmarks.json
content/browser/schemas/browser_action.json
content/browser/schemas/commands.json
content/browser/schemas/context_menus.json
content/browser/schemas/context_menus_internal.json
content/browser/schemas/history.json
content/browser/schemas/page_action.json
+ content/browser/schemas/sessions.json
content/browser/schemas/tabs.json
content/browser/schemas/windows.json
new file mode 100644
--- /dev/null
+++ b/browser/components/extensions/schemas/sessions.json
@@ -0,0 +1,143 @@
+// Copyright 2013 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+[
+ {
+ "namespace": "manifest",
+ "types": [
+ {
+ "$extend": "Permission",
+ "choices": [{
+ "type": "string",
+ "enum": [
+ "sessions"
+ ]
+ }]
+ }
+ ]
+ },
+ {
+ "namespace": "sessions",
+ "description": "Use the <code>chrome.sessions</code> API to query and restore tabs and windows from a browsing session.",
+ "permissions": ["sessions"],
+ "types": [
+ {
+ "id": "Filter",
+ "type": "object",
+ "properties": {
+ "maxResults": {
+ "type": "integer",
+ "minimum": 0,
+ "maximum": 25,
+ "optional": true,
+ "description": "The maximum number of entries to be fetched in the requested list. Omit this parameter to fetch the maximum number of entries ($(ref:sessions.MAX_SESSION_RESULTS))."
+ }
+ }
+ },
+ {
+ "id": "Session",
+ "type": "object",
+ "properties": {
+ "lastModified": {"type": "integer", "description": "The time when the window or tab was closed or modified, represented in milliseconds since the epoch."},
+ "tab": {"$ref": "tabs.Tab", "optional": true, "description": "The $(ref:tabs.Tab), if this entry describes a tab. Either this or $(ref:sessions.Session.window) will be set."},
+ "window": {"$ref": "windows.Window", "optional": true, "description": "The $(ref:windows.Window), if this entry describes a window. Either this or $(ref:sessions.Session.tab) will be set."}
+ }
+ },
+ {
+ "id": "Device",
+ "type": "object",
+ "properties": {
+ "info": {"type": "string"},
+ "deviceName": {"type": "string", "description": "The name of the foreign device."},
+ "sessions": {"type": "array", "items": {"$ref": "Session"}, "description": "A list of open window sessions for the foreign device, sorted from most recently to least recently modified session."}
+ }
+ }
+ ],
+ "functions": [
+ {
+ "name": "getRecentlyClosed",
+ "type": "function",
+ "description": "Gets the list of recently closed tabs and/or windows.",
+ "async": "callback",
+ "parameters": [
+ {
+ "$ref": "Filter",
+ "name": "filter",
+ "optional": true
+ },
+ {
+ "type": "function",
+ "name": "callback",
+ "parameters": [
+ {
+ "name": "sessions", "type": "array", "items": { "$ref": "Session" }, "description": "The list of closed entries in reverse order that they were closed (the most recently closed tab or window will be at index <code>0</code>). The entries may contain either tabs or windows."
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "name": "getDevices",
+ "type": "function",
+ "description": "Retrieves all devices with synced sessions.",
+ "async": "callback",
+ "parameters": [
+ {
+ "$ref": "Filter",
+ "name": "filter",
+ "optional": true
+ },
+ {
+ "type": "function",
+ "name": "callback",
+ "parameters": [
+ {
+ "name": "devices", "type": "array", "items": { "$ref": "Device" }, "description": "The list of $(ref:sessions.Device) objects for each synced session, sorted in order from device with most recently modified session to device with least recently modified session. $(ref:tabs.Tab) objects are sorted by recency in the $(ref:windows.Window) of the $(ref:sessions.Session) objects."
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "name": "restore",
+ "type": "function",
+ "description": "Reopens a $(ref:windows.Window) or $(ref:tabs.Tab), with an optional callback to run when the entry has been restored.",
+ "async": "callback",
+ "parameters": [
+ {
+ "type": "string",
+ "name": "sessionId",
+ "optional": true,
+ "description": "The $(ref:windows.Window.sessionId), or $(ref:tabs.Tab.sessionId) to restore. If this parameter is not specified, the most recently closed session is restored."
+ },
+ {
+ "type": "function",
+ "name": "callback",
+ "optional": true,
+ "parameters": [
+ {
+ "$ref": "Session",
+ "name": "restoredSession",
+ "description": "A $(ref:sessions.Session) containing the restored $(ref:windows.Window) or $(ref:tabs.Tab) object."
+ }
+ ]
+ }
+ ]
+ }
+ ],
+ "events": [
+ {
+ "name": "onChanged",
+ "description": "Fired when recently closed tabs and/or windows are changed. This event does not monitor synced sessions changes.",
+ "type": "function"
+ }
+ ],
+ "properties": {
+ "MAX_SESSION_RESULTS": {
+ "value": 25,
+ "description": "The maximum number of $(ref:sessions.Session) that will be included in a requested list."
+ }
+ }
+ }
+]
--- a/browser/components/extensions/test/browser/browser.ini
+++ b/browser/components/extensions/test/browser/browser.ini
@@ -50,16 +50,17 @@ tags = webextensions
[browser_ext_pageAction_title.js]
[browser_ext_popup_api_injection.js]
[browser_ext_popup_background.js]
[browser_ext_popup_corners.js]
[browser_ext_popup_shutdown.js]
[browser_ext_runtime_openOptionsPage.js]
[browser_ext_runtime_openOptionsPage_uninstall.js]
[browser_ext_runtime_setUninstallURL.js]
+[browser_ext_sessions.js]
[browser_ext_simple.js]
[browser_ext_tab_runtimeConnect.js]
[browser_ext_tabs_audio.js]
[browser_ext_tabs_captureVisibleTab.js]
[browser_ext_tabs_create.js]
[browser_ext_tabs_create_invalid_url.js]
[browser_ext_tabs_detectLanguage.js]
[browser_ext_tabs_duplicate.js]
new file mode 100644
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_sessions.js
@@ -0,0 +1,131 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+add_task(function* () {
+
+ let createdWindowProps = [];
+ let initialTimestamps = [];
+
+ function openAndCloseWindow() {
+ let win = yield BrowserTestUtils.openNewBrowserWindow();
+ yield BrowserTestUtils.loadURI(win.gBrowser.selectedBrowser, "http://mochi.test:8888/");
+ yield BrowserTestUtils.browserLoaded(win.gBrowser.selectedBrowser);
+ let props = {};
+ for (let prop of ["screenY", "screenX", "outerWidth", "outerHeight"]) {
+ props[prop] = win[prop];
+ }
+ createdWindowProps.push(props);
+ yield BrowserTestUtils.closeWindow(win);
+ }
+
+ function checkWindow(window, localCreatedWindowProps) {
+ for (let prop of ["focused", "incognito", "alwaysOnTop"]) {
+ is(window[prop], false, `closed window has the expected value for ${prop}`);
+ }
+ for (let prop of ["state", "type"]) {
+ is(window[prop], "normal", `closed window has the expected value for ${prop}`);
+ }
+ let winProps = localCreatedWindowProps.pop();
+ is(window.top, winProps.screenY, "closed window has the expected value for top");
+ is(window.left, winProps.screenX, "closed window has the expected value for left");
+ is(window.width, winProps.outerWidth, "closed window has the expected value for width");
+ is(window.height, winProps.outerHeight, "closed window has the expected value for height");
+ }
+
+ function checkTab(tab, currentWindowId) {
+ for (let prop of ["selected", "highlighted", "active", "pinned", "incognito"]) {
+ is(tab[prop], false, `closed tab has the expected value for ${prop}`);
+ }
+ is(tab.windowId, currentWindowId, "closed tab has the expected value for windowId");
+ }
+
+ function checkRecentlyClosed(recentlyClosed, expectedCount, currentWindowId) {
+ let localCreatedWindowProps = createdWindowProps.slice();
+ is(recentlyClosed.length, expectedCount, "the expected number of closed windows was found");
+ for (let item of recentlyClosed) {
+ if (item.window) {
+ checkWindow(item.window, localCreatedWindowProps);
+ } else if (item.tab) {
+ checkTab(item.tab, currentWindowId);
+ }
+ }
+ }
+
+ function removeOldItems(item) {
+ return !initialTimestamps.includes(item.lastModified)
+ }
+
+ function background() {
+
+ let currentWindowId;
+
+ browser.tabs.query({active: true, currentWindow: true}).then(tabs => {
+ currentWindowId = tabs[0].windowId;
+ return browser.sessions.getRecentlyClosed();
+ }).then(recentlyClosed => {
+ browser.test.sendMessage("initialData", {recentlyClosed, currentWindowId});
+ });
+
+ browser.test.onMessage.addListener(msg => {
+ if (msg == "check-sessions") {
+ browser.sessions.getRecentlyClosed().then(recentlyClosed => {
+ browser.test.sendMessage("recentlyClosed", recentlyClosed);
+ });
+ }
+ });
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["sessions", "tabs"],
+ },
+ background,
+ });
+
+ // Open and close a window that will be ignored, to prove that we are removing previous entries
+ yield openAndCloseWindow();
+
+ yield extension.startup();
+
+ let {recentlyClosed, currentWindowId} = yield extension.awaitMessage("initialData");
+ initialTimestamps = recentlyClosed.map(item => item.lastModified);
+
+ yield openAndCloseWindow();
+ extension.sendMessage("check-sessions");
+ recentlyClosed = yield extension.awaitMessage("recentlyClosed");
+ checkRecentlyClosed(recentlyClosed.filter(removeOldItems), 1, currentWindowId);
+
+ yield openAndCloseWindow();
+ extension.sendMessage("check-sessions");
+ recentlyClosed = yield extension.awaitMessage("recentlyClosed");
+ checkRecentlyClosed(recentlyClosed.filter(removeOldItems), 2, currentWindowId);
+
+ let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, "http://mochi.test:8888/");
+ yield BrowserTestUtils.removeTab(tab);
+ extension.sendMessage("check-sessions");
+ recentlyClosed = yield extension.awaitMessage("recentlyClosed");
+ checkRecentlyClosed(recentlyClosed.filter(removeOldItems), 3, currentWindowId);
+
+ tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, "http://mochi.test:8888/");
+ yield BrowserTestUtils.removeTab(tab);
+ extension.sendMessage("check-sessions");
+ recentlyClosed = yield extension.awaitMessage("recentlyClosed");
+ checkRecentlyClosed(recentlyClosed.filter(removeOldItems), 4, currentWindowId);
+
+ yield openAndCloseWindow();
+ extension.sendMessage("check-sessions");
+ recentlyClosed = yield extension.awaitMessage("recentlyClosed");
+ let finalResult = recentlyClosed.filter(removeOldItems);
+ checkRecentlyClosed(finalResult, 5, currentWindowId);
+
+ ok(finalResult[0].window, "first item is a window");
+ ok(!finalResult[0].tab, "first item is not a tab");
+ ok(finalResult[1].tab, "second item is a tab");
+ ok(!finalResult[1].window, "second item is not a window");
+ ok(finalResult[2].tab, "third item is a tab");
+ ok(finalResult[3].window, "fourth item is a window");
+ ok(finalResult[4].window, "fifth item is a window");
+
+ yield extension.unload();
+});