зеркало из https://github.com/mozilla/gecko-dev.git
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:
Родитель
8a8a847acc
Коммит
49742b1fa3
|
@ -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",
|
||||
|
|
Загрузка…
Ссылка в новой задаче