Bug 1352598 - Add WebExtension API for access to search. r=aswan

MozReview-Commit-ID: 4pV2DGMcV7G

--HG--
extra : rebase_source : 811187b96b19d433249404791bbbfdff47bceebe
This commit is contained in:
Michael Kaply 2018-06-18 10:39:12 -05:00
Родитель 8a8a847acc
Коммит 49742b1fa3
11 изменённых файлов: 279 добавлений и 23 удалений

Просмотреть файл

@ -143,6 +143,14 @@
["geckoProfiler"]
]
},
"search": {
"url": "chrome://browser/content/parent/ext-search.js",
"schema": "chrome://browser/content/schemas/search.json",
"scopes": ["addon_parent"],
"paths": [
["search"]
]
},
"sessions": {
"url": "chrome://browser/content/parent/ext-sessions.js",
"schema": "chrome://browser/content/schemas/sessions.json",

Просмотреть файл

@ -30,6 +30,7 @@ browser.jar:
content/browser/parent/ext-omnibox.js (parent/ext-omnibox.js)
content/browser/parent/ext-pageAction.js (parent/ext-pageAction.js)
content/browser/parent/ext-pkcs11.js (parent/ext-pkcs11.js)
content/browser/parent/ext-search.js (parent/ext-search.js)
content/browser/parent/ext-sessions.js (parent/ext-sessions.js)
content/browser/parent/ext-sidebarAction.js (parent/ext-sidebarAction.js)
content/browser/parent/ext-tabs.js (parent/ext-tabs.js)

Просмотреть файл

@ -21,6 +21,7 @@ module.exports = {
"openOptionsPage": true,
"pageActionFor": true,
"replaceUrlInTab": true,
"searchInitialized": true,
"sidebarActionFor": true,
"tabGetSender": true,
"tabTracker": true,

Просмотреть файл

@ -223,6 +223,16 @@ global.TabContext = class extends EventEmitter {
}
};
// This promise is used to wait for the search service to be initialized.
// None of the code in the WebExtension modules requests that initialization.
// It is assumed that it is started at some point. If tests start to fail
// because this promise never resolves, that's likely the cause.
XPCOMUtils.defineLazyGetter(global, "searchInitialized", () => {
if (Services.search.isInitialized) {
return Promise.resolve();
}
return ExtensionUtils.promiseObserved("browser-search-service", (_, data) => data == "init-complete");
});
class WindowTracker extends WindowTrackerBase {
addProgressListener(window, listener) {

Просмотреть файл

@ -20,27 +20,6 @@ const HOMEPAGE_CONFIRMED_TYPE = "homepageNotification";
const HOMEPAGE_SETTING_TYPE = "prefs";
const HOMEPAGE_SETTING_NAME = "homepage_override";
// This promise is used to wait for the search service to be initialized.
// None of the code in this module requests that initialization. It is assumed
// that it is started at some point. If tests start to fail because this
// promise never resolves, that's likely the cause.
const searchInitialized = () => {
if (Services.search.isInitialized) {
return;
}
return new Promise(resolve => {
const SEARCH_SERVICE_TOPIC = "browser-search-service";
Services.obs.addObserver(function observer(subject, topic, data) {
if (data != "init-complete") {
return;
}
Services.obs.removeObserver(observer, SEARCH_SERVICE_TOPIC);
resolve();
}, SEARCH_SERVICE_TOPIC);
});
};
XPCOMUtils.defineLazyGetter(this, "homepagePopup", () => {
return new ExtensionControlledPopup({
confirmedType: HOMEPAGE_CONFIRMED_TYPE,
@ -135,7 +114,7 @@ this.chrome_settings_overrides = class extends ExtensionAPI {
if (item) {
ExtensionSettingsStore.removeSetting(
id, DEFAULT_SEARCH_STORE_TYPE, ENGINE_ADDED_SETTING_NAME);
await searchInitialized();
await searchInitialized;
let engine = Services.search.getEngineByName(item.value);
try {
Services.search.removeEngine(engine);
@ -211,7 +190,7 @@ this.chrome_settings_overrides = class extends ExtensionAPI {
});
}
if (manifest.chrome_settings_overrides.search_provider) {
await searchInitialized();
await searchInitialized;
extension.callOnClose({
close: () => {
if (extension.shutdownReason == "ADDON_DISABLE") {

Просмотреть файл

@ -0,0 +1,97 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set sts=2 sw=2 et tw=80: */
"use strict";
ChromeUtils.defineModuleGetter(this, "Services",
"resource://gre/modules/Services.jsm");
XPCOMUtils.defineLazyPreferenceGetter(this, "searchLoadInBackground",
"browser.search.context.loadInBackground");
Cu.importGlobalProperties(["fetch", "btoa"]);
var {
ExtensionError,
} = ExtensionUtils;
async function getDataURI(resourceURI) {
let response = await fetch(resourceURI);
let buffer = await response.arrayBuffer();
// Remove charset from content type
let contentType = response.headers.get("content-type").split(",");
let bytes = new Uint8Array(buffer);
let str = String.fromCharCode.apply(null, bytes);
return `data:${contentType[0]};base64,${btoa(str)}`;
}
this.search = class extends ExtensionAPI {
getAPI(context) {
return {
search: {
async get() {
await searchInitialized;
let engines = Services.search.getEngines();
let visibleEngines = engines.filter(engine => !engine.hidden);
return Promise.all(visibleEngines.map(async engine => {
let favicon_url = null;
if (engine.iconURI) {
if (engine.iconURI.spec.startsWith("resource:") ||
engine.iconURI.spec.startsWith("chrome:")) {
// Convert internal URLs to data URLs
favicon_url = await getDataURI(engine.iconURI.spec);
} else {
favicon_url = engine.iconURI.spec;
}
}
return {
name: engine.name,
is_default: engine === Services.search.currentEngine,
alias: engine.alias,
favicon_url,
};
}));
},
async search(name, searchTerms, tabId) {
await searchInitialized;
let engine = Services.search.getEngineByName(name);
if (!engine) {
throw new ExtensionError(`${name} was not found`);
}
let submission = engine.getSubmission(searchTerms, null, "webextension");
let options = {
postData: submission.postData,
triggeringPrincipal: context.principal,
};
if (tabId === null) {
let browser = context.pendingEventBrowser || context.xulBrowser;
let {gBrowser} = browser.ownerGlobal;
if (!gBrowser || !gBrowser.addTab) {
// In some cases (about:addons, sidebar, maybe others), we need
// to go up one more level.
browser = browser.ownerDocument.docShell.chromeEventHandler;
({gBrowser} = browser.ownerGlobal);
}
if (!gBrowser || !gBrowser.addTab) {
throw new ExtensionError("Unable to locate a browser.");
}
let nativeTab = gBrowser.addTab(submission.uri.spec, options);
if (!searchLoadInBackground) {
gBrowser.selectedTab = nativeTab;
}
} else {
let tab = tabTracker.getTab(tabId);
tab.linkedBrowser.loadURI(submission.uri.spec, options);
}
},
},
};
}
};

Просмотреть файл

@ -20,6 +20,7 @@ browser.jar:
content/browser/schemas/omnibox.json
content/browser/schemas/page_action.json
content/browser/schemas/pkcs11.json
content/browser/schemas/search.json
content/browser/schemas/sessions.json
content/browser/schemas/sidebar_action.json
content/browser/schemas/tabs.json

Просмотреть файл

@ -0,0 +1,64 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
[
{
"namespace": "search",
"description": "Use browser.search to interact with search engines.",
"types": [
{
"id": "SearchEngine",
"type": "object",
"description": "An object encapsulating a search engine",
"properties": {
"name": {
"type": "string"
},
"is_default": {
"type": "boolean"
},
"alias": {
"type": "string",
"optional": true
},
"favicon_url": {
"type": "string",
"optional": true,
"format": "url"
}
}
}
],
"functions": [
{
"name": "get",
"type": "function",
"description": "Gets a list of search engines.",
"async": true,
"parameters": []
},
{
"name": "search",
"type": "function",
"requireUserInput": true,
"description": "Perform a search.",
"parameters": [
{
"name": "engineName",
"type": "string"
},
{
"name": "searchTerms",
"type": "string"
},
{
"type": "integer",
"name": "tabId",
"optional": true
}
]
}
]
}
]

Просмотреть файл

@ -134,6 +134,7 @@ skip-if = !e10s || !crashreporter # the tab's process is killed during the test.
[browser_ext_port_disconnect_on_window_close.js]
[browser_ext_runtime_openOptionsPage.js]
[browser_ext_runtime_openOptionsPage_uninstall.js]
[browser_ext_search.js]
[browser_ext_runtime_setUninstallURL.js]
[browser_ext_sessions_forgetClosedTab.js]
[browser_ext_sessions_forgetClosedWindow.js]

Просмотреть файл

@ -0,0 +1,93 @@
/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set sts=2 sw=2 et tw=80: */
"use strict";
add_task(async function test_search() {
const TEST_ID = "test_search@tests.mozilla.com";
const SEARCH_TERM = "test";
const SEARCH_URL = "https://localhost/?q={searchTerms}";
async function background() {
await browser.tabs.create({url: "about:blank"});
let engines = await browser.search.get();
browser.test.sendMessage("engines", engines);
browser.browserAction.onClicked.addListener(tab => {
browser.tabs.onUpdated.addListener(async function(tabId, info, changedTab) {
if (tabId == tab.id && info.status === "complete" &&
changedTab.url != "about:blank") {
await browser.tabs.remove(tabId);
browser.test.sendMessage("searchLoaded", changedTab.url);
}
});
browser.search.search("Search Test", "test", tab.id); // Can't use SEARCH_TERM here
});
}
let extension = ExtensionTestUtils.loadExtension({
manifest: {
permissions: ["tabs"],
name: TEST_ID,
"browser_action": {},
"chrome_settings_overrides": {
"search_provider": {
"name": "Search Test",
"search_url": SEARCH_URL,
},
},
},
background,
useAddonManager: "temporary",
});
await extension.startup();
let addonEngines = await extension.awaitMessage("engines");
let engines = Services.search.getEngines().filter(engine => !engine.hidden);
is(addonEngines.length, engines.length, "Engine lengths are the same.");
let defaultEngine = addonEngines.filter(engine => engine.is_default === true);
is(defaultEngine.length, 1, "One default engine");
is(defaultEngine[0].name, Services.search.currentEngine.name, "Default engine is correct");
await clickBrowserAction(extension);
let url = await extension.awaitMessage("searchLoaded");
is(url, SEARCH_URL.replace("{searchTerms}", SEARCH_TERM), "Loaded page matches search");
await extension.unload();
});
add_task(async function test_search_notab() {
const TEST_ID = "test_search@tests.mozilla.com";
const SEARCH_TERM = "test";
const SEARCH_URL = "https://localhost/?q={searchTerms}";
async function background() {
browser.browserAction.onClicked.addListener(_ => {
browser.tabs.onUpdated.addListener(async (tabId, info, changedTab) => {
if (info.status === "complete") {
await browser.tabs.remove(tabId);
browser.test.sendMessage("searchLoaded", changedTab.url);
}
});
browser.search.search("Search Test", "test"); // Can't use SEARCH_TERM here
});
}
let extension = ExtensionTestUtils.loadExtension({
manifest: {
permissions: ["tabs"],
name: TEST_ID,
"browser_action": {},
"chrome_settings_overrides": {
"search_provider": {
"name": "Search Test",
"search_url": SEARCH_URL,
},
},
},
background,
useAddonManager: "temporary",
});
await extension.startup();
await clickBrowserAction(extension);
let url = await extension.awaitMessage("searchLoaded");
is(url, SEARCH_URL.replace("{searchTerms}", SEARCH_TERM), "Loaded page matches search");
await extension.unload();
});

Просмотреть файл

@ -16,6 +16,7 @@ let expectedContentApisTargetSpecific = [
];
let expectedBackgroundApisTargetSpecific = [
"search.get",
"tabs.MutedInfoReason",
"tabs.TAB_ID_NONE",
"tabs.TabStatus",