зеркало из https://github.com/mozilla/gecko-dev.git
Bug 1518841 - Allow embedders to load WebExtensions in GeckoView. r=snorp,esawin
Depends On D16913 Differential Revision: https://phabricator.services.mozilla.com/D16268 --HG-- extra : moz-landing-system : lando
This commit is contained in:
Родитель
61c0cff76e
Коммит
ee86fd5efc
|
@ -47,6 +47,13 @@ GeckoViewStartup.prototype = {
|
|||
module: "resource://gre/modules/GeckoViewConsole.jsm",
|
||||
});
|
||||
|
||||
GeckoViewUtils.addLazyGetter(this, "GeckoViewWebExtension", {
|
||||
module: "resource://gre/modules/GeckoViewWebExtension.jsm",
|
||||
ged: [
|
||||
"GeckoView:RegisterWebExtension",
|
||||
],
|
||||
});
|
||||
|
||||
GeckoViewUtils.addLazyPrefObserver({
|
||||
name: "geckoview.console.enabled",
|
||||
default: false,
|
||||
|
|
|
@ -191,6 +191,7 @@ package org.mozilla.geckoview {
|
|||
method @android.support.annotation.UiThread public void orientationChanged();
|
||||
method @android.support.annotation.UiThread public void orientationChanged(int);
|
||||
method @android.support.annotation.AnyThread public void readFromParcel(@android.support.annotation.NonNull android.os.Parcel);
|
||||
method @android.support.annotation.UiThread @android.support.annotation.NonNull public org.mozilla.geckoview.GeckoResult<java.lang.Void> registerWebExtension(@android.support.annotation.NonNull org.mozilla.geckoview.WebExtension);
|
||||
method @android.support.annotation.UiThread public void setDelegate(@android.support.annotation.Nullable org.mozilla.geckoview.GeckoRuntime.Delegate);
|
||||
method @android.support.annotation.AnyThread public void shutdown();
|
||||
field public static final java.lang.String ACTION_CRASHED = "org.mozilla.gecko.ACTION_CRASHED";
|
||||
|
@ -918,6 +919,13 @@ package org.mozilla.geckoview {
|
|||
method @android.support.annotation.UiThread public synchronized void setView(@android.support.annotation.Nullable android.view.View);
|
||||
}
|
||||
|
||||
public class WebExtension {
|
||||
ctor public WebExtension(@android.support.annotation.NonNull java.lang.String, @android.support.annotation.NonNull java.lang.String);
|
||||
ctor public WebExtension(@android.support.annotation.NonNull java.lang.String);
|
||||
field @android.support.annotation.NonNull public final java.lang.String id;
|
||||
field @android.support.annotation.NonNull public final java.lang.String location;
|
||||
}
|
||||
|
||||
@android.support.annotation.AnyThread public abstract class WebMessage {
|
||||
ctor protected WebMessage(@android.support.annotation.NonNull org.mozilla.geckoview.WebMessage.Builder);
|
||||
field @android.support.annotation.NonNull public final java.util.Map<java.lang.String, java.lang.String> headers;
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
document.body.style.border = "5px solid red";
|
Двоичные данные
mobile/android/geckoview/src/androidTest/assets/web_extensions/borderify/icons/border-48.png
Normal file
Двоичные данные
mobile/android/geckoview/src/androidTest/assets/web_extensions/borderify/icons/border-48.png
Normal file
Двоичный файл не отображается.
После Ширина: | Высота: | Размер: 225 B |
|
@ -0,0 +1,15 @@
|
|||
{
|
||||
"manifest_version": 2,
|
||||
"name": "Borderify",
|
||||
"version": "1.0",
|
||||
"description": "Adds a red border to all webpages matching example.com.",
|
||||
"icons": {
|
||||
"48": "icons/border-48.png"
|
||||
},
|
||||
"content_scripts": [
|
||||
{
|
||||
"matches": ["*://*.example.com/*"],
|
||||
"js": ["borderify.js"]
|
||||
}
|
||||
]
|
||||
}
|
|
@ -0,0 +1,84 @@
|
|||
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
|
||||
* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
package org.mozilla.geckoview.test
|
||||
|
||||
import org.mozilla.geckoview.test.rule.GeckoSessionTestRule.ReuseSession
|
||||
|
||||
import android.support.test.filters.MediumTest
|
||||
import android.support.test.runner.AndroidJUnit4
|
||||
import org.hamcrest.core.IsEqual.equalTo
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Assert.fail
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.mozilla.geckoview.*
|
||||
import org.mozilla.geckoview.test.rule.GeckoSessionTestRule.WithDevToolsAPI
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
@MediumTest
|
||||
@ReuseSession(false)
|
||||
class WebExtensionTest : BaseSessionTest() {
|
||||
@Test
|
||||
@WithDevToolsAPI
|
||||
fun registerWebExtension() {
|
||||
mainSession.loadUri("example.com")
|
||||
sessionRule.waitForPageStop()
|
||||
|
||||
// First let's check that the color of the border is empty before loading
|
||||
// the WebExtension
|
||||
val colorBefore = sessionRule.evaluateJS(mainSession, "document.body.style.borderColor")
|
||||
assertThat("The border color should be empty when loading without extensions.",
|
||||
colorBefore as String, equalTo(""))
|
||||
|
||||
// Load the WebExtension that will add a border to the body
|
||||
sessionRule.waitForResult(sessionRule.runtime.registerWebExtension(
|
||||
WebExtension("resource://android/assets/web_extensions/borderify/")
|
||||
))
|
||||
|
||||
mainSession.reload()
|
||||
sessionRule.waitForPageStop()
|
||||
|
||||
// Check that the WebExtension was applied by checking the border color
|
||||
val color = sessionRule.evaluateJS(mainSession, "document.body.style.borderColor")
|
||||
assertThat("Content script should have been applied",
|
||||
color as String, equalTo("red"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun badFileType() {
|
||||
testRegisterError("resource://android/bad/location/error",
|
||||
"does not point to a folder or an .xpi")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun badLocationXpi() {
|
||||
testRegisterError("resource://android/bad/location/error.xpi",
|
||||
"NS_ERROR_FILE_NOT_FOUND")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun badLocationFolder() {
|
||||
testRegisterError("resource://android/bad/location/error/",
|
||||
"NS_ERROR_FILE_NOT_FOUND")
|
||||
}
|
||||
|
||||
private fun testRegisterError(location: String, expectedError: String) {
|
||||
try {
|
||||
sessionRule.waitForResult(sessionRule.runtime.registerWebExtension(
|
||||
WebExtension(location)
|
||||
))
|
||||
} catch (ex: Exception) {
|
||||
// Let's make sure the error message contains the WebExtension URL
|
||||
assertTrue(ex.message!!.contains(location))
|
||||
|
||||
// and it contains the expected error message
|
||||
assertTrue(ex.message!!.contains(expectedError))
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
fail("The above code should throw.")
|
||||
}
|
||||
}
|
|
@ -264,6 +264,54 @@ public final class GeckoRuntime implements Parcelable {
|
|||
return create(context, new GeckoRuntimeSettings());
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a {@link WebExtension} that will be run with this GeckoRuntime.
|
||||
*
|
||||
* <p>At this time, WebExtensions don't have access to any UI element and
|
||||
* cannot communicate with the application. Any UI element will be
|
||||
* ignored.</p>
|
||||
*
|
||||
* Example:
|
||||
* <pre><code>
|
||||
* runtime.registerWebExtension(new WebExtension(
|
||||
* "resource://android/assets/web_extensions/my_webextension/"));
|
||||
*
|
||||
* runtime.registerWebExtension(new WebExtension(
|
||||
* "file:///path/to/web_extension/my_webextension2.xpi",
|
||||
* "mywebextension2@example.com"));
|
||||
* </code></pre>
|
||||
*
|
||||
* To learn more about WebExtensions refer to
|
||||
* <a href="https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions">
|
||||
* Mozilla/Add-ons/WebExtensions
|
||||
* </a>.
|
||||
*
|
||||
* @param webExtension {@link WebExtension} to register
|
||||
*
|
||||
* @return A {@link GeckoResult} that will complete when the WebExtension
|
||||
* has been installed.
|
||||
*/
|
||||
@UiThread
|
||||
public @NonNull GeckoResult<Void> registerWebExtension(
|
||||
final @NonNull WebExtension webExtension) {
|
||||
final GeckoSession.CallbackResult<Void> result =
|
||||
new GeckoSession.CallbackResult<Void>() {
|
||||
@Override
|
||||
public void sendSuccess(Object response) {
|
||||
complete(null);
|
||||
}
|
||||
};
|
||||
|
||||
final GeckoBundle bundle = new GeckoBundle(1);
|
||||
bundle.putString("locationUri", webExtension.location.toString());
|
||||
bundle.putString("id", webExtension.id);
|
||||
|
||||
EventDispatcher.getInstance().dispatch("GeckoView:RegisterWebExtension",
|
||||
bundle, result);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new runtime with the given settings and attach it to the given
|
||||
* context.
|
||||
|
|
|
@ -0,0 +1,74 @@
|
|||
package org.mozilla.geckoview;
|
||||
|
||||
import android.support.annotation.NonNull;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Represents a WebExtension that may be used by GeckoView.
|
||||
*/
|
||||
public class WebExtension {
|
||||
/**
|
||||
* <code>file:</code> or <code>resource:</code> URI that points to the
|
||||
* install location of this WebExtension. When the WebExtension is included
|
||||
* with the APK the file can be specified using the
|
||||
* <code>resource://android</code> alias. E.g.
|
||||
*
|
||||
* <pre><code>
|
||||
* resource://android/assets/web_extensions/my_webextension/
|
||||
* </code></pre>
|
||||
*
|
||||
* Will point to folder
|
||||
* <code>/assets/web_extensions/my_webextension/</code> in the APK.
|
||||
*/
|
||||
public final @NonNull String location;
|
||||
/**
|
||||
* Unique identifier for this WebExtension
|
||||
*/
|
||||
public final @NonNull String id;
|
||||
|
||||
/**
|
||||
* Builds a WebExtension instance that can be loaded in GeckoView using
|
||||
* {@link GeckoRuntime#registerWebExtension}
|
||||
*
|
||||
* @param location The WebExtension install location. It must be either a
|
||||
* <code>resource:</code> URI to a folder inside the APK or
|
||||
* a <code>file:</code> URL to a <code>.xpi</code> file.
|
||||
* @param id Unique identifier for this WebExtension. This identifier must
|
||||
* either be a GUID or a string formatted like an email address.
|
||||
* E.g. <pre><code>
|
||||
* "extensionname@example.org"
|
||||
* "{daf44bf7-a45e-4450-979c-91cf07434c3d}"
|
||||
* </code></pre>
|
||||
*
|
||||
* See also: <ul>
|
||||
* <li><a href="https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/manifest.json/browser_specific_settings">
|
||||
* WebExtensions/manifest.json/browser_specific_settings
|
||||
* </a>
|
||||
* <li><a href="https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/WebExtensions_and_the_Add-on_ID#When_do_you_need_an_add-on_ID">
|
||||
* WebExtensions/WebExtensions_and_the_Add-on_ID
|
||||
* </a>
|
||||
* </ul>
|
||||
*/
|
||||
public WebExtension(final @NonNull String location, final @NonNull String id) {
|
||||
this.location = location;
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a WebExtension instance that can be loaded in GeckoView using
|
||||
* {@link GeckoRuntime#registerWebExtension}
|
||||
* The <code>id</code> for this web extension will be automatically
|
||||
* generated.
|
||||
*
|
||||
* @param location The WebExtension install location. It must be either a
|
||||
* <code>resource:</code> URI to a folder inside the APK or
|
||||
* a <code>file:</code> URL to a <code>.xpi</code> file.
|
||||
*/
|
||||
public WebExtension(final @NonNull String location) {
|
||||
this.location = location;
|
||||
this.id = "{" + UUID.randomUUID().toString() + "}";
|
||||
}
|
||||
|
||||
// TODO (Bug 1518843) add messaging support
|
||||
}
|
|
@ -65,6 +65,11 @@ exclude: true
|
|||
|
||||
[67.1]: ../GeckoSession.html#getDefaultUserAgent--
|
||||
|
||||
- Initial WebExtension support. [`GeckoRuntime#registerWebExtension`][67.15]
|
||||
allows embedders to register a local web extension.
|
||||
|
||||
[67.15]: ../GeckoRuntime.html#registerWebExtension-org.mozilla.geckoview.WebExtension-
|
||||
|
||||
## v66
|
||||
- Removed redundant field `trackingMode` from [`SecurityInformation`][66.6].
|
||||
Use `TrackingProtectionDelegate.onTrackerBlocked` for notification of blocked
|
||||
|
@ -184,4 +189,4 @@ exclude: true
|
|||
[65.24]: ../CrashReporter.html#sendCrashReport-android.content.Context-android.os.Bundle-java.lang.String-
|
||||
[65.25]: ../GeckoResult.html
|
||||
|
||||
[api-version]: a1740e5cb61e34b3180b80f33b0b33243a34d588
|
||||
[api-version]: b26e5e12a78512a9c18d1ba3441864ca66d3dde8
|
||||
|
|
|
@ -29,6 +29,15 @@ android {
|
|||
}
|
||||
}
|
||||
|
||||
// By default the android plugins ignores folders that start with `_`, but
|
||||
// we need those in web extensions.
|
||||
// See also:
|
||||
// - https://issuetracker.google.com/issues/36911326
|
||||
// - https://stackoverflow.com/questions/9206117/how-to-workaround-autoomitting-fiiles-folders-starting-with-underscore-in
|
||||
aaptOptions {
|
||||
ignoreAssetsPattern '!.svn:!.git:!.ds_store:!*.scc:.*:!CVS:!thumbs.db:!picasa.ini:!*~'
|
||||
}
|
||||
|
||||
project.configureProductFlavors.delegate = it
|
||||
project.configureProductFlavors()
|
||||
}
|
||||
|
|
|
@ -0,0 +1,74 @@
|
|||
/* 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/. */
|
||||
|
||||
"use strict";
|
||||
|
||||
var EXPORTED_SYMBOLS = ["GeckoViewWebExtension"];
|
||||
|
||||
const {XPCOMUtils} = ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||
const {GeckoViewUtils} = ChromeUtils.import("resource://gre/modules/GeckoViewUtils.jsm");
|
||||
|
||||
XPCOMUtils.defineLazyModuleGetters(this, {
|
||||
Extension: "resource://gre/modules/Extension.jsm",
|
||||
});
|
||||
|
||||
XPCOMUtils.defineLazyGetter(this, "require", () => {
|
||||
const { require } = ChromeUtils.import("resource://devtools/shared/Loader.jsm", {});
|
||||
return require;
|
||||
});
|
||||
|
||||
XPCOMUtils.defineLazyGetter(this, "Services", () => {
|
||||
const Services = require("Services");
|
||||
return Services;
|
||||
});
|
||||
|
||||
const {debug, warn} = GeckoViewUtils.initLogging("Console"); // eslint-disable-line no-unused-vars
|
||||
|
||||
var GeckoViewWebExtension = {
|
||||
async registerWebExtension(aId, aUri, aCallback) {
|
||||
const params = {
|
||||
id: aId,
|
||||
resourceURI: aUri,
|
||||
temporarilyInstalled: true,
|
||||
builtIn: true,
|
||||
};
|
||||
|
||||
let file;
|
||||
if (aUri instanceof Ci.nsIFileURL) {
|
||||
file = aUri.file;
|
||||
}
|
||||
|
||||
try {
|
||||
await Extension.getBootstrapScope(aId, file)
|
||||
.startup(params, undefined);
|
||||
} catch (ex) {
|
||||
aCallback.onError(`Error registering WebExtension at: ${aUri.spec}. ${ex}`);
|
||||
return;
|
||||
}
|
||||
|
||||
aCallback.onSuccess();
|
||||
},
|
||||
|
||||
onEvent(aEvent, aData, aCallback) {
|
||||
debug `onEvent ${aEvent} ${aData}`;
|
||||
|
||||
switch (aEvent) {
|
||||
case "GeckoView:RegisterWebExtension": {
|
||||
const uri = Services.io.newURI(aData.locationUri);
|
||||
if (uri == null || (!(uri instanceof Ci.nsIFileURL) &&
|
||||
!(uri instanceof Ci.nsIJARURI))) {
|
||||
aCallback.onError(`Extension does not point to a resource URI or a file URL. extension=${aData.locationUri}`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (uri.fileName != "" && uri.fileExtension != "xpi") {
|
||||
aCallback.onError(`Extension does not point to a folder or an .xpi file. Hint: the path needs to end with a "/" to be considered a folder. extension=${aData.locationUri}`);
|
||||
return;
|
||||
}
|
||||
|
||||
this.registerWebExtension(aData.id, uri, aCallback);
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
|
@ -23,6 +23,7 @@ EXTRA_JS_MODULES += [
|
|||
'GeckoViewTab.jsm',
|
||||
'GeckoViewTelemetry.jsm',
|
||||
'GeckoViewUtils.jsm',
|
||||
'GeckoViewWebExtension.jsm',
|
||||
'LoadURIDelegate.jsm',
|
||||
'Messaging.jsm',
|
||||
]
|
||||
|
|
Загрузка…
Ссылка в новой задаче