Bug 1599585 - Implement enable/disable extensions. r=esawin,snorp

Differential Revision: https://phabricator.services.mozilla.com/D58859

--HG--
extra : moz-landing-system : lando
This commit is contained in:
Agi Sferro 2020-01-07 22:30:59 +00:00
Родитель dcd4934531
Коммит 6ac412a45a
6 изменённых файлов: 273 добавлений и 42 удалений

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

@ -1484,6 +1484,12 @@ package org.mozilla.geckoview {
field public static final int VULNERABLE_UPDATE_AVAILABLE = 4;
}
public static class WebExtension.DisabledFlags {
ctor public DisabledFlags();
field public static final int BLOCKLIST_DISABLED = 4;
field public static final int USER_DISABLED = 2;
}
public static class WebExtension.Flags {
ctor protected Flags();
field public static final long ALLOW_CONTENT_MESSAGING = 1L;
@ -1533,6 +1539,8 @@ package org.mozilla.geckoview {
field @Nullable public final String creatorName;
field @Nullable public final String creatorUrl;
field @Nullable public final String description;
field public final int disabledFlags;
field public final boolean enabled;
field @Nullable public final String homepageUrl;
field @NonNull public final WebExtension.Icon icon;
field public final boolean isRecommended;
@ -1568,6 +1576,8 @@ package org.mozilla.geckoview {
}
public class WebExtensionController {
method @AnyThread @NonNull public GeckoResult<WebExtension> disable(@NonNull WebExtension, int);
method @AnyThread @NonNull public GeckoResult<WebExtension> enable(@NonNull WebExtension, int);
method @UiThread @Nullable public WebExtensionController.PromptDelegate getPromptDelegate();
method @UiThread @Nullable public WebExtensionController.TabDelegate getTabDelegate();
method @NonNull @AnyThread public GeckoResult<WebExtension> install(@NonNull String);
@ -1577,6 +1587,11 @@ package org.mozilla.geckoview {
method @NonNull @AnyThread public GeckoResult<Void> uninstall(@NonNull WebExtension);
}
public static class WebExtensionController.EnableSource {
ctor public EnableSource();
field public static final int USER = 1;
}
@UiThread public static interface WebExtensionController.PromptDelegate {
method @Nullable default public GeckoResult<AllowOrDeny> onInstallPrompt(@NonNull WebExtension);
}

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

@ -4,8 +4,6 @@
package org.mozilla.geckoview.test
import android.support.test.InstrumentationRegistry
import android.support.test.filters.MediumTest
import android.support.test.runner.AndroidJUnit4
import org.hamcrest.core.StringEndsWith.endsWith
@ -23,8 +21,8 @@ import org.mozilla.geckoview.*
import org.mozilla.geckoview.test.rule.GeckoSessionTestRule
import org.mozilla.geckoview.test.rule.GeckoSessionTestRule.AssertCalled
import org.mozilla.geckoview.test.util.Callbacks
import org.mozilla.geckoview.test.util.TestServer
import java.net.URI
import org.mozilla.geckoview.WebExtension.DisabledFlags
import org.mozilla.geckoview.WebExtensionController.EnableSource
import java.util.UUID
@ -64,9 +62,7 @@ class WebExtensionTest : BaseSessionTest() {
// First let's check that the color of the border is empty before loading
// the WebExtension
val colorBefore = mainSession.evaluateJS("document.body.style.borderColor")
assertThat("The border color should be empty when loading without extensions.",
colorBefore as String, equalTo(""))
assertBodyBorderEqualTo("")
val borderify = WebExtension("resource://android/assets/web_extensions/borderify/",
controller)
@ -78,9 +74,7 @@ class WebExtensionTest : BaseSessionTest() {
sessionRule.waitForPageStop()
// Check that the WebExtension was applied by checking the border color
val color = mainSession.evaluateJS("document.body.style.borderColor")
assertThat("Content script should have been applied",
color as String, equalTo("red"))
assertBodyBorderEqualTo("red")
// Unregister WebExtension and check again
sessionRule.waitForResult(sessionRule.runtime.unregisterWebExtension(borderify))
@ -89,9 +83,74 @@ class WebExtensionTest : BaseSessionTest() {
sessionRule.waitForPageStop()
// Check that the WebExtension was not applied after being unregistered
val colorAfter = mainSession.evaluateJS("document.body.style.borderColor")
assertThat("Content script should have been applied",
colorAfter as String, equalTo(""))
assertBodyBorderEqualTo("")
}
private fun assertBodyBorderEqualTo(expected: String) {
val color = mainSession.evaluateJS("document.body.style.borderColor")
assertThat("The border color should be '$expected'",
color as String, equalTo(expected))
}
@Test
fun enableDisable() {
mainSession.loadUri("example.com")
sessionRule.waitForPageStop()
sessionRule.delegateDuringNextWait(object : WebExtensionController.PromptDelegate {
@AssertCalled
override fun onInstallPrompt(extension: WebExtension): GeckoResult<AllowOrDeny> {
return GeckoResult.fromValue(AllowOrDeny.ALLOW)
}
})
// First let's check that the color of the border is empty before loading
// the WebExtension
assertBodyBorderEqualTo("")
var borderify = sessionRule.waitForResult(
controller.install("resource://android/assets/web_extensions/borderify.xpi"))
mainSession.reload()
sessionRule.waitForPageStop()
assertThat("Extension should be enabled after installing", borderify.metaData!!.enabled,
equalTo(true))
assertThat("Extension should be user disabled after calling disable",
borderify.metaData!!.disabledFlags, equalTo(0))
// Border should be ready because the extension is enabled
assertBodyBorderEqualTo("red")
borderify = sessionRule.waitForResult(controller.disable(borderify, EnableSource.USER))
mainSession.reload()
sessionRule.waitForPageStop()
assertThat("Extension should be user disabled after calling disable",
borderify.metaData!!.enabled, equalTo(false))
assertThat("Extension should be user disabled after calling disable",
borderify.metaData!!.disabledFlags and DisabledFlags.USER_DISABLED > 0,
equalTo(true))
assertThat("Extension should not be blocklist disabled after calling disable",
borderify.metaData!!.disabledFlags and DisabledFlags.BLOCKLIST_DISABLED > 0,
equalTo(false))
// Border should be empty because the extension is disabled
assertBodyBorderEqualTo("")
borderify = sessionRule.waitForResult(controller.enable(borderify, EnableSource.USER))
mainSession.reload()
sessionRule.waitForPageStop()
assertThat("Extension should be user disabled after calling disable",
borderify.metaData!!.enabled, equalTo(true))
assertThat("Extension should be user disabled after calling disable",
borderify.metaData!!.disabledFlags, equalTo(0))
assertBodyBorderEqualTo("red")
sessionRule.waitForResult(controller.uninstall(borderify))
mainSession.reload()
sessionRule.waitForPageStop()
// Border should be empty because the extension is not installed anymore
assertBodyBorderEqualTo("")
}
@Test
@ -101,9 +160,7 @@ class WebExtensionTest : BaseSessionTest() {
// First let's check that the color of the border is empty before loading
// the WebExtension
val colorBefore = mainSession.evaluateJS("document.body.style.borderColor")
assertThat("The border color should be empty when loading without extensions.",
colorBefore as String, equalTo(""))
assertBodyBorderEqualTo("")
sessionRule.delegateDuringNextWait(object : WebExtensionController.PromptDelegate {
@AssertCalled
@ -114,8 +171,7 @@ class WebExtensionTest : BaseSessionTest() {
assertEquals(extension.metaData!!.version, "1.0")
// TODO: Bug 1601067
// assertEquals(extension.isBuiltIn, false)
// TODO: Bug 1599585
// assertEquals(extension.isEnabled, false)
assertEquals(extension.metaData!!.enabled, true)
assertEquals(extension.metaData!!.signedState,
WebExtension.SignedStateFlags.SIGNED)
assertEquals(extension.metaData!!.blocklistState,
@ -132,9 +188,7 @@ class WebExtensionTest : BaseSessionTest() {
sessionRule.waitForPageStop()
// Check that the WebExtension was applied by checking the border color
val color = mainSession.evaluateJS("document.body.style.borderColor")
assertThat("Content script should have been applied",
color as String, equalTo("red"))
assertBodyBorderEqualTo("red")
var list = sessionRule.waitForResult(controller.list())
assertEquals(list.size, 1)
@ -150,9 +204,7 @@ class WebExtensionTest : BaseSessionTest() {
sessionRule.waitForPageStop()
// Check that the WebExtension was not applied after being unregistered
val colorAfter = mainSession.evaluateJS("document.body.style.borderColor")
assertThat("Content script should have been applied",
colorAfter as String, equalTo(""))
assertBodyBorderEqualTo("")
}
@Test

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

@ -61,9 +61,6 @@ public class WebExtension {
// TODO: make public
final boolean isBuiltIn;
// TODO: make public
final boolean isEnabled;
/** Called whenever a delegate is set or unset on this {@link WebExtension} instance.
/* package */ interface DelegateController {
void onMessageDelegate(final String nativeApp, final MessageDelegate delegate);
@ -113,7 +110,6 @@ public class WebExtension {
id = bundle.getString("webExtensionId");
flags = bundle.getInt("webExtensionFlags", 0);
isBuiltIn = bundle.getBoolean("isBuiltIn", false);
isEnabled = bundle.getBoolean("isEnabled", false);
if (bundle.containsKey("metaData")) {
metaData = new MetaData(bundle.getBundle("metaData"));
} else {
@ -155,7 +151,6 @@ public class WebExtension {
this.flags = flags;
// TODO:
this.isEnabled = false;
this.isBuiltIn = false;
this.metaData = null;
}
@ -1223,6 +1218,23 @@ public class WebExtension {
BlocklistStateFlags.VULNERABLE_NO_UPDATE})
@interface BlocklistState {}
public static class DisabledFlags {
/** The extension has been disabled by the user */
public final static int USER_DISABLED = 1 << 1;
/** The extension has been disabled by the blocklist. The details of why this extension
* was blocked can be found in {@link MetaData#blocklistState}. */
public final static int BLOCKLIST_DISABLED = 1 << 2;
// TODO: Bug 1604222
final static int APP_DISABLED = 1 << 3;
}
@Retention(RetentionPolicy.SOURCE)
@IntDef(flag = true,
value = { DisabledFlags.USER_DISABLED, DisabledFlags.BLOCKLIST_DISABLED })
@interface EnabledFlags {}
/** Provides information about a {@link WebExtension}. */
public class MetaData {
/** Main {@link Icon} branding for this {@link WebExtension}.
@ -1324,6 +1336,27 @@ public class WebExtension {
*/
public final @SignedState int signedState;
/**
* Disabled binary flags for this extension.
*
* This will be either equal to <code>0</code> if the extension
* is enabled or will contain one or more flags from {@link DisabledFlags}.
*
* e.g. if the extension has been disabled by the user, the value in
* {@link DisabledFlags#USER_DISABLED} will be equal to <code>1</code>:
*
* <pre><code>
* boolean isUserDisabled = metaData.disabledFlags
* &amp; DisabledFlags.USER_DISABLED &gt; 0;
* </code></pre>
*/
public final @EnabledFlags int disabledFlags;
/**
* Whether this extension is enabled or not.
*/
public final boolean enabled;
/** Override for testing. */
protected MetaData() {
icon = null;
@ -1340,6 +1373,8 @@ public class WebExtension {
isRecommended = false;
blocklistState = BlocklistStateFlags.NOT_BLOCKED;
signedState = SignedStateFlags.UNKNOWN;
disabledFlags = 0;
enabled = true;
}
/* package */ MetaData(final GeckoBundle bundle) {
@ -1364,6 +1399,21 @@ public class WebExtension {
this.signedState = SignedStateFlags.UNKNOWN;
}
int disabledFlags = 0;
final String[] disabledFlagsString = bundle.getStringArray("disabledFlags");
this.enabled = disabledFlagsString.length == 0;
for (final String flag : disabledFlagsString) {
if (flag.equals("userDisabled")) {
disabledFlags |= DisabledFlags.USER_DISABLED;
} else if (flag.equals("blocklistDisabled")) {
disabledFlags |= DisabledFlags.BLOCKLIST_DISABLED;
} else {
Log.e(LOGTAG, "Unrecognized disabledFlag state: " + flag);
}
}
this.disabledFlags = disabledFlags;
if (bundle.containsKey("icons")) {
icon = new Icon(bundle.getBundle("icons"));
} else {

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

@ -1,6 +1,7 @@
package org.mozilla.geckoview;
import android.support.annotation.AnyThread;
import android.support.annotation.IntDef;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.annotation.UiThread;
@ -13,6 +14,8 @@ import org.mozilla.gecko.util.BundleEventListener;
import org.mozilla.gecko.util.EventCallback;
import org.mozilla.gecko.util.GeckoBundle;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
@ -411,12 +414,48 @@ public class WebExtensionController {
return result;
}
// TODO: Bug 1599585 make public
GeckoResult<WebExtension> enable(final WebExtension extension) {
@Retention(RetentionPolicy.SOURCE)
@IntDef({ EnableSource.USER })
@interface EnableSources {}
/** Contains the possible values for the <code>source</code> parameter in {@link #enable} and
* {@link #disable}. */
public static class EnableSource {
/** Action has been requested by the user. */
public final static int USER = 1;
// TODO: Bug 1604222
/** Action requested by the app itself, e.g. to disable an extension that is
* not supported in this version of the app. */
final static int APP = 2;
static String toString(final @EnableSources int flag) {
if (flag == USER) {
return "user";
} else {
throw new IllegalArgumentException("Value provided in flags is not valid.");
}
}
}
/**
* Enable an extension that has been disabled. If the extension is already enabled, this
* method has no effect.
*
* @param extension The {@link WebExtension} to be enabled.
* @param source The agent that initiated this action, e.g. if the action has been initiated
* by the user,use {@link EnableSource#USER}.
* @return the new {@link WebExtension} instance, updated to reflect the enablement.
*/
@AnyThread
@NonNull
public GeckoResult<WebExtension> enable(final @NonNull WebExtension extension,
final @EnableSources int source) {
final WebExtensionResult result = new WebExtensionResult("extension");
final GeckoBundle bundle = new GeckoBundle(1);
final GeckoBundle bundle = new GeckoBundle(2);
bundle.putString("webExtensionId", extension.id);
bundle.putString("source", EnableSource.toString(source));
EventDispatcher.getInstance().dispatch("GeckoView:WebExtension:Enable",
bundle, result);
@ -427,12 +466,24 @@ public class WebExtensionController {
});
}
// TODO: Bug 1599585 make public
GeckoResult<WebExtension> disable(final WebExtension extension) {
/**
* Disable an extension that is enabled. If the extension is already disabled, this
* method has no effect.
*
* @param extension The {@link WebExtension} to be disabled.
* @param source The agent that initiated this action, e.g. if the action has been initiated
* by the user, use {@link EnableSource#USER}.
* @return the new {@link WebExtension} instance, updated to reflect the disablement.
*/
@AnyThread
@NonNull
public GeckoResult<WebExtension> disable(final @NonNull WebExtension extension,
final @EnableSources int source) {
final WebExtensionResult result = new WebExtensionResult("extension");
final GeckoBundle bundle = new GeckoBundle(1);
final GeckoBundle bundle = new GeckoBundle(2);
bundle.putString("webExtensionId", extension.id);
bundle.putString("source", EnableSource.toString(source));
EventDispatcher.getInstance().dispatch("GeckoView:WebExtension:Disable",
bundle, result);

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

@ -13,6 +13,14 @@ exclude: true
⚠️ breaking change
## v74
- Added [`WebExtensionController.enable`][74.1] and [`disable`][74.2] to
enable and disable extensions.
([bug 1599585]({{bugzilla}}1599585))
[74.1]: {{javadoc_uri}}/WebExtensionController.html#enable-org.mozilla.geckoview.WebExtension-int-
[74.2]: {{javadoc_uri}}/WebExtensionController.html#disable-org.mozilla.geckoview.WebExtension-int-
## v73
- Added [`WebExtensionController.install`][73.1] and [`uninstall`][73.2] to
manage installed extensions
@ -522,4 +530,4 @@ exclude: true
[65.24]: {{javadoc_uri}}/CrashReporter.html#sendCrashReport-android.content.Context-android.os.Bundle-java.lang.String-
[65.25]: {{javadoc_uri}}/GeckoResult.html
[api-version]: 2a944df46a5560f6b45f421536fd3051f9c32885
[api-version]: 45f21fe6c3c30f903b65a573540901e8f45affca

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

@ -252,7 +252,7 @@ function exportExtension(aAddon, aPermissions, aSourceURI) {
optionsBrowserStyle,
isRecommended,
blocklistState,
isActive,
userDisabled,
isBuiltin,
id,
} = aAddon;
@ -265,15 +265,22 @@ function exportExtension(aAddon, aPermissions, aSourceURI) {
}
const openOptionsPageInTab =
optionsBrowserStyle === AddonManager.OPTIONS_TYPE_TAB;
const disabledFlags = [];
if (userDisabled) {
disabledFlags.push("userDisabled");
}
if (blocklistState !== Ci.nsIBlocklistService.STATE_NOT_BLOCKED) {
disabledFlags.push("blocklistDisabled");
}
return {
webExtensionId: id,
locationURI: aSourceURI != null ? aSourceURI.spec : "",
isEnabled: isActive,
isBuiltIn: isBuiltin,
metaData: {
permissions: aPermissions ? aPermissions.permissions : [],
origins: aPermissions ? aPermissions.origins : [],
description,
disabledFlags,
version,
creatorName,
creatorURL,
@ -495,6 +502,30 @@ var GeckoViewWebExtension = {
}
},
async enableWebExtension(aId, aSource) {
const extension = await this.extensionById(aId);
if (aSource === "user") {
extension.enable();
}
return exportExtension(
extension,
extension.userPermissions,
/* aSourceURI */ null
);
},
async disableWebExtension(aId, aSource) {
const extension = await this.extensionById(aId);
if (aSource === "user") {
extension.disable();
}
return exportExtension(
extension,
extension.userPermissions,
/* aSourceURI */ null
);
},
async onEvent(aEvent, aData, aCallback) {
debug`onEvent ${aEvent} ${aData}`;
@ -629,14 +660,38 @@ var GeckoViewWebExtension = {
}
case "GeckoView:WebExtension:Enable": {
// TODO
aCallback.onError(`Not implemented`);
try {
const { source, webExtensionId } = aData;
if (source !== "user") {
throw new Error("Illegal source parameter");
}
const extension = await this.enableWebExtension(
webExtensionId,
source
);
aCallback.onSuccess({ extension });
} catch (ex) {
debug`Failed enable ${ex}`;
aCallback.onError(`Unexpected error: ${ex}`);
}
break;
}
case "GeckoView:WebExtension:Disable": {
// TODO
aCallback.onError(`Not implemented`);
try {
const { source, webExtensionId } = aData;
if (source !== "user") {
throw new Error("Illegal source parameter");
}
const extension = await this.disableWebExtension(
webExtensionId,
source
);
aCallback.onSuccess({ extension });
} catch (ex) {
debug`Failed disable ${ex}`;
aCallback.onError(`Unexpected error: ${ex}`);
}
break;
}