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:
Agi Sferro 2019-02-25 17:00:18 +00:00
Родитель 61c0cff76e
Коммит ee86fd5efc
12 изменённых файлов: 327 добавлений и 1 удалений

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

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

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 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',
]