mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-10-08 19:04:45 +00:00
Bug 1599580 - Implement install/uninstall extension. r=snorp,esawin
Differential Revision: https://phabricator.services.mozilla.com/D55730 --HG-- extra : moz-landing-system : lando
This commit is contained in:
parent
b6568ba03f
commit
d87305d23c
@ -74,6 +74,7 @@ GeckoViewStartup.prototype = {
|
||||
"GeckoView:PageAction:Click",
|
||||
"GeckoView:RegisterWebExtension",
|
||||
"GeckoView:UnregisterWebExtension",
|
||||
"GeckoView:WebExtension:Get",
|
||||
"GeckoView:WebExtension:Disable",
|
||||
"GeckoView:WebExtension:Enable",
|
||||
"GeckoView:WebExtension:Install",
|
||||
|
@ -1395,6 +1395,7 @@ package org.mozilla.geckoview {
|
||||
field public final long flags;
|
||||
field @NonNull public final String id;
|
||||
field @NonNull public final String location;
|
||||
field @Nullable public final WebExtension.MetaData metaData;
|
||||
}
|
||||
|
||||
@AnyThread public static class WebExtension.Action {
|
||||
@ -1416,6 +1417,16 @@ package org.mozilla.geckoview {
|
||||
method @UiThread @Nullable default public GeckoResult<GeckoSession> onTogglePopup(@NonNull WebExtension, @NonNull WebExtension.Action);
|
||||
}
|
||||
|
||||
public static class WebExtension.BlocklistStateFlags {
|
||||
ctor public BlocklistStateFlags();
|
||||
field public static final int BLOCKED = 2;
|
||||
field public static final int NOT_BLOCKED = 0;
|
||||
field public static final int OUTDATED = 3;
|
||||
field public static final int SOFTBLOCKED = 1;
|
||||
field public static final int VULNERABLE_NO_UPDATE = 5;
|
||||
field public static final int VULNERABLE_UPDATE_AVAILABLE = 4;
|
||||
}
|
||||
|
||||
public static class WebExtension.Flags {
|
||||
ctor protected Flags();
|
||||
field public static final long ALLOW_CONTENT_MESSAGING = 1L;
|
||||
@ -1427,6 +1438,22 @@ package org.mozilla.geckoview {
|
||||
method @AnyThread @NonNull public GeckoResult<Bitmap> get(int);
|
||||
}
|
||||
|
||||
public static class WebExtension.InstallException extends Exception {
|
||||
ctor protected InstallException();
|
||||
field public final int code;
|
||||
}
|
||||
|
||||
public static class WebExtension.InstallException.ErrorCodes {
|
||||
ctor protected ErrorCodes();
|
||||
field public static final int ERROR_CORRUPT_FILE = -3;
|
||||
field public static final int ERROR_FILE_ACCESS = -4;
|
||||
field public static final int ERROR_INCORRECT_HASH = -2;
|
||||
field public static final int ERROR_INCORRECT_ID = -7;
|
||||
field public static final int ERROR_NETWORK_FAILURE = -1;
|
||||
field public static final int ERROR_SIGNEDSTATE_REQUIRED = -5;
|
||||
field public static final int ERROR_UNEXPECTED_ADDON_TYPE = -6;
|
||||
}
|
||||
|
||||
@UiThread public static interface WebExtension.MessageDelegate {
|
||||
method @Nullable default public void onConnect(@NonNull WebExtension.Port);
|
||||
method @Nullable default public GeckoResult<Object> onMessage(@NonNull String, @NonNull Object, @NonNull WebExtension.MessageSender);
|
||||
@ -1443,6 +1470,22 @@ package org.mozilla.geckoview {
|
||||
field @NonNull public final WebExtension webExtension;
|
||||
}
|
||||
|
||||
public class WebExtension.MetaData {
|
||||
ctor protected MetaData();
|
||||
field public final int blocklistState;
|
||||
field @Nullable public final String creatorName;
|
||||
field @Nullable public final String creatorUrl;
|
||||
field @Nullable public final String description;
|
||||
field @Nullable public final String homepageUrl;
|
||||
field @NonNull public final WebExtension.Icon icon;
|
||||
field public final boolean isRecommended;
|
||||
field @Nullable public final String name;
|
||||
field @NonNull public final String[] origins;
|
||||
field @NonNull public final String[] permissions;
|
||||
field public final int signedState;
|
||||
field @NonNull public final String version;
|
||||
}
|
||||
|
||||
@UiThread public static class WebExtension.Port {
|
||||
ctor protected Port();
|
||||
method public void disconnect();
|
||||
@ -1457,9 +1500,27 @@ package org.mozilla.geckoview {
|
||||
method default public void onPortMessage(@NonNull Object, @NonNull WebExtension.Port);
|
||||
}
|
||||
|
||||
public static class WebExtension.SignedStateFlags {
|
||||
ctor public SignedStateFlags();
|
||||
field public static final int MISSING = 0;
|
||||
field public static final int PRELIMINARY = 1;
|
||||
field public static final int PRIVILEGED = 4;
|
||||
field public static final int SIGNED = 2;
|
||||
field public static final int SYSTEM = 3;
|
||||
field public static final int UNKNOWN = -1;
|
||||
}
|
||||
|
||||
public class WebExtensionController {
|
||||
method @UiThread @Nullable public WebExtensionController.PromptDelegate getPromptDelegate();
|
||||
method @UiThread @Nullable public WebExtensionController.TabDelegate getTabDelegate();
|
||||
method @NonNull @AnyThread public GeckoResult<WebExtension> install(@NonNull String);
|
||||
method @UiThread public void setPromptDelegate(@Nullable WebExtensionController.PromptDelegate);
|
||||
method @UiThread public void setTabDelegate(@Nullable WebExtensionController.TabDelegate);
|
||||
method @NonNull @AnyThread public GeckoResult<Void> uninstall(@NonNull WebExtension);
|
||||
}
|
||||
|
||||
@UiThread public static interface WebExtensionController.PromptDelegate {
|
||||
method @Nullable default public GeckoResult<AllowOrDeny> onInstallPrompt(@NonNull WebExtension);
|
||||
}
|
||||
|
||||
public static interface WebExtensionController.TabDelegate {
|
||||
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -12,8 +12,11 @@ import org.hamcrest.core.StringEndsWith.endsWith
|
||||
import org.hamcrest.core.IsEqual.equalTo
|
||||
import org.json.JSONObject
|
||||
import org.junit.Assert
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Assert.fail
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.mozilla.geckoview.*
|
||||
@ -37,6 +40,16 @@ class WebExtensionTest : BaseSessionTest() {
|
||||
val MESSAGING_CONTENT: String = "resource://android/assets/web_extensions/messaging-content/"
|
||||
}
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
sessionRule.addExternalDelegateUntilTestEnd(
|
||||
WebExtensionController.PromptDelegate::class,
|
||||
sessionRule.runtime.webExtensionController::setPromptDelegate,
|
||||
{ sessionRule.runtime.webExtensionController.promptDelegate = null },
|
||||
object : WebExtensionController.PromptDelegate {}
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun registerWebExtension() {
|
||||
mainSession.loadUri("example.com")
|
||||
@ -73,6 +86,130 @@ class WebExtensionTest : BaseSessionTest() {
|
||||
colorAfter as String, equalTo(""))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun installWebExtension() {
|
||||
mainSession.loadUri("example.com")
|
||||
sessionRule.waitForPageStop()
|
||||
|
||||
// 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(""))
|
||||
|
||||
sessionRule.delegateDuringNextWait(object : WebExtensionController.PromptDelegate {
|
||||
@AssertCalled
|
||||
override fun onInstallPrompt(extension: WebExtension): GeckoResult<AllowOrDeny> {
|
||||
assertEquals(extension.metaData!!.description,
|
||||
"Adds a red border to all webpages matching example.com.")
|
||||
assertEquals(extension.metaData!!.name, "Borderify")
|
||||
assertEquals(extension.metaData!!.version, "1.0")
|
||||
// TODO: Bug 1601067
|
||||
// assertEquals(extension.isBuiltIn, false)
|
||||
// TODO: Bug 1599585
|
||||
// assertEquals(extension.isEnabled, false)
|
||||
assertEquals(extension.metaData!!.signedState,
|
||||
WebExtension.SignedStateFlags.SIGNED)
|
||||
assertEquals(extension.metaData!!.blocklistState,
|
||||
WebExtension.BlocklistStateFlags.NOT_BLOCKED)
|
||||
|
||||
return GeckoResult.fromValue(AllowOrDeny.ALLOW)
|
||||
}
|
||||
})
|
||||
|
||||
val borderify = sessionRule.waitForResult(
|
||||
sessionRule.runtime.webExtensionController.install(
|
||||
"resource://android/assets/web_extensions/borderify.xpi"))
|
||||
|
||||
mainSession.reload()
|
||||
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"))
|
||||
|
||||
// Unregister WebExtension and check again
|
||||
sessionRule.waitForResult(sessionRule.runtime.webExtensionController.uninstall(borderify))
|
||||
|
||||
mainSession.reload()
|
||||
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(""))
|
||||
}
|
||||
|
||||
private fun testInstallError(name: String, expectedError: Int) {
|
||||
sessionRule.delegateDuringNextWait(object : WebExtensionController.PromptDelegate {
|
||||
@AssertCalled(count = 0)
|
||||
override fun onInstallPrompt(extension: WebExtension): GeckoResult<AllowOrDeny> {
|
||||
return GeckoResult.fromValue(AllowOrDeny.ALLOW)
|
||||
}
|
||||
})
|
||||
|
||||
sessionRule.waitForResult(
|
||||
sessionRule.runtime.webExtensionController.install(
|
||||
"resource://android/assets/web_extensions/$name")
|
||||
.accept({
|
||||
// We should not be able to install unsigned extensions
|
||||
assertTrue(false)
|
||||
}, { exception ->
|
||||
val installException = exception as WebExtension.InstallException
|
||||
assertEquals(installException.code, expectedError)
|
||||
}))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun installUnsignedExtensionSignatureNotRequired() {
|
||||
sessionRule.setPrefsUntilTestEnd(mapOf(
|
||||
"xpinstall.signatures.required" to false
|
||||
))
|
||||
|
||||
sessionRule.delegateDuringNextWait(object : WebExtensionController.PromptDelegate {
|
||||
override fun onInstallPrompt(extension: WebExtension): GeckoResult<AllowOrDeny> {
|
||||
return GeckoResult.fromValue(AllowOrDeny.ALLOW)
|
||||
}
|
||||
})
|
||||
|
||||
val borderify = sessionRule.waitForResult(
|
||||
sessionRule.runtime.webExtensionController.install(
|
||||
"resource://android/assets/web_extensions/borderify-unsigned.xpi")
|
||||
.then { extension ->
|
||||
assertEquals(extension!!.metaData!!.signedState,
|
||||
WebExtension.SignedStateFlags.MISSING)
|
||||
assertEquals(extension.metaData!!.blocklistState,
|
||||
WebExtension.BlocklistStateFlags.NOT_BLOCKED)
|
||||
assertEquals(extension.metaData!!.name, "Borderify")
|
||||
GeckoResult.fromValue(extension)
|
||||
})
|
||||
|
||||
sessionRule.waitForResult(
|
||||
sessionRule.runtime.webExtensionController.uninstall(borderify))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun installUnsignedExtensionSignatureRequired() {
|
||||
sessionRule.setPrefsUntilTestEnd(mapOf(
|
||||
"xpinstall.signatures.required" to true
|
||||
))
|
||||
testInstallError("borderify-unsigned.xpi",
|
||||
WebExtension.InstallException.ErrorCodes.ERROR_SIGNEDSTATE_REQUIRED)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun installExtensionFileNotFound() {
|
||||
testInstallError("file-not-found.xpi",
|
||||
WebExtension.InstallException.ErrorCodes.ERROR_NETWORK_FAILURE)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun installExtensionMissingId() {
|
||||
testInstallError("borderify-missing-id.xpi",
|
||||
WebExtension.InstallException.ErrorCodes.ERROR_CORRUPT_FILE)
|
||||
}
|
||||
|
||||
// This test
|
||||
// - Registers a web extension
|
||||
// - Listens for messages and waits for a message
|
||||
|
@ -54,8 +54,9 @@ public class WebExtension {
|
||||
*/
|
||||
public final @WebExtensionFlags long flags;
|
||||
|
||||
// TODO: make public
|
||||
final MetaData metaData;
|
||||
/** Provides information about this {@link WebExtension}. */
|
||||
// TODO: move to @NonNull when we remove registerWebExtension
|
||||
public final @Nullable MetaData metaData;
|
||||
|
||||
// TODO: make public
|
||||
final boolean isBuiltIn;
|
||||
@ -63,6 +64,26 @@ public class WebExtension {
|
||||
// TODO: make public
|
||||
final boolean isEnabled;
|
||||
|
||||
/** Called whenever a delegate is set or unset on this {@link WebExtension} instance.
|
||||
/* package */ interface DelegateObserver {
|
||||
void onMessageDelegate(final String nativeApp, final MessageDelegate delegate);
|
||||
void onActionDelegate(final ActionDelegate delegate);
|
||||
}
|
||||
|
||||
private WeakReference<DelegateObserver> mDelegateObserver = new WeakReference<>(null);
|
||||
|
||||
/* package */ void setDelegateObserver(final DelegateObserver observer) {
|
||||
mDelegateObserver = new WeakReference<>(observer);
|
||||
|
||||
if (observer != null) {
|
||||
// Notify observers of already attached delegates
|
||||
for (final Map.Entry<String, MessageDelegate> entry : messageDelegates.entrySet()) {
|
||||
observer.onMessageDelegate(entry.getKey(), entry.getValue());
|
||||
}
|
||||
observer.onActionDelegate(actionDelegate);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delegates that handle messaging between this WebExtension and the app.
|
||||
*/
|
||||
@ -219,6 +240,10 @@ public class WebExtension {
|
||||
@UiThread
|
||||
public void setMessageDelegate(final @Nullable MessageDelegate messageDelegate,
|
||||
final @NonNull String nativeApp) {
|
||||
final DelegateObserver observer = mDelegateObserver.get();
|
||||
if (observer != null) {
|
||||
observer.onMessageDelegate(nativeApp, messageDelegate);
|
||||
}
|
||||
if (messageDelegate == null) {
|
||||
messageDelegates.remove(nativeApp);
|
||||
return;
|
||||
@ -1022,6 +1047,58 @@ public class WebExtension {
|
||||
}
|
||||
}
|
||||
|
||||
/** Extension thrown when an error occurs during extension installation. */
|
||||
public static class InstallException extends Exception {
|
||||
public static class ErrorCodes {
|
||||
/** The download failed due to network problems. */
|
||||
public static final int ERROR_NETWORK_FAILURE = -1;
|
||||
/** The downloaded file did not match the provided hash. */
|
||||
public static final int ERROR_INCORRECT_HASH = -2;
|
||||
/** The downloaded file seems to be corrupted in some way. */
|
||||
public static final int ERROR_CORRUPT_FILE = -3;
|
||||
/** An error occurred trying to write to the filesystem. */
|
||||
public static final int ERROR_FILE_ACCESS = -4;
|
||||
/** The extension must be signed and isn't. */
|
||||
public static final int ERROR_SIGNEDSTATE_REQUIRED = -5;
|
||||
/** The downloaded extension had a different type than expected. */
|
||||
public static final int ERROR_UNEXPECTED_ADDON_TYPE = -6;
|
||||
/** The extension did not have the expected ID. */
|
||||
public static final int ERROR_INCORRECT_ID = -7;
|
||||
|
||||
/** For testing. */
|
||||
protected ErrorCodes() {}
|
||||
}
|
||||
|
||||
@Retention(RetentionPolicy.SOURCE)
|
||||
@IntDef(value = {
|
||||
ErrorCodes.ERROR_NETWORK_FAILURE,
|
||||
ErrorCodes.ERROR_INCORRECT_HASH,
|
||||
ErrorCodes.ERROR_CORRUPT_FILE,
|
||||
ErrorCodes.ERROR_FILE_ACCESS,
|
||||
ErrorCodes.ERROR_SIGNEDSTATE_REQUIRED,
|
||||
ErrorCodes.ERROR_UNEXPECTED_ADDON_TYPE,
|
||||
ErrorCodes.ERROR_INCORRECT_ID
|
||||
})
|
||||
/* package */ @interface Codes {}
|
||||
|
||||
/** One of {@link ErrorCodes} that provides more information about this exception. */
|
||||
public final @Codes int code;
|
||||
|
||||
/** For testing */
|
||||
protected InstallException() {
|
||||
this.code = ErrorCodes.ERROR_NETWORK_FAILURE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "InstallException: " + code;
|
||||
}
|
||||
|
||||
/* package */ InstallException(final @Codes int code) {
|
||||
this.code = code;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the Action delegate for this WebExtension.
|
||||
*
|
||||
@ -1035,6 +1112,11 @@ public class WebExtension {
|
||||
*/
|
||||
@AnyThread
|
||||
public void setActionDelegate(final @Nullable ActionDelegate delegate) {
|
||||
final DelegateObserver observer = mDelegateObserver.get();
|
||||
if (observer != null) {
|
||||
observer.onActionDelegate(delegate);
|
||||
}
|
||||
|
||||
actionDelegate = delegate;
|
||||
|
||||
final GeckoBundle bundle = new GeckoBundle(1);
|
||||
@ -1044,15 +1126,27 @@ public class WebExtension {
|
||||
"GeckoView:ActionDelegate:Attached", bundle);
|
||||
}
|
||||
|
||||
// TODO: make public
|
||||
// Keep in sync with AddonManager.jsm
|
||||
static class SignedStateFlags {
|
||||
final static int UNKNOWN = -1;
|
||||
final static int MISSING = 0;
|
||||
final static int PRELIMINARY = 1;
|
||||
final static int SIGNED = 2;
|
||||
final static int SYSTEM = 3;
|
||||
final static int PRIVILEGED = 4;
|
||||
/** Describes the signed status for a {@link WebExtension}.
|
||||
*
|
||||
* See <a href="https://support.mozilla.org/en-US/kb/add-on-signing-in-firefox">
|
||||
* Add-on signing in Firefox.
|
||||
* </a>
|
||||
*/
|
||||
public static class SignedStateFlags {
|
||||
// Keep in sync with AddonManager.jsm
|
||||
/** This extension may be signed but by a certificate that doesn't
|
||||
* chain to our our trusted certificate. */
|
||||
public final static int UNKNOWN = -1;
|
||||
/** This extension is unsigned. */
|
||||
public final static int MISSING = 0;
|
||||
/** This extension has been preliminarily reviewed. */
|
||||
public final static int PRELIMINARY = 1;
|
||||
/** This extension has been fully reviewed. */
|
||||
public final static int SIGNED = 2;
|
||||
/** This extension is a system add-on. */
|
||||
public final static int SYSTEM = 3;
|
||||
/** This extension is signed with a "Mozilla Extensions" certificate. */
|
||||
public final static int PRIVILEGED = 4;
|
||||
|
||||
/* package */ final static int LAST = PRIVILEGED;
|
||||
}
|
||||
@ -1062,15 +1156,27 @@ public class WebExtension {
|
||||
SignedStateFlags.SIGNED, SignedStateFlags.SYSTEM, SignedStateFlags.PRIVILEGED})
|
||||
@interface SignedState {}
|
||||
|
||||
// TODO: make public
|
||||
// Keep in sync with nsIBlocklistService.idl
|
||||
static class BlocklistStateFlags {
|
||||
final static int NOT_BLOCKED = 0;
|
||||
final static int SOFTBLOCKED = 1;
|
||||
final static int BLOCKED = 2;
|
||||
final static int OUTDATED = 3;
|
||||
final static int VULNERABLE_UPDATE_AVAILABLE = 4;
|
||||
final static int VULNERABLE_NO_UPDATE = 5;
|
||||
/** Describes the blocklist state for a {@link WebExtension}.
|
||||
* See <a href="https://support.mozilla.org/en-US/kb/add-ons-cause-issues-are-on-blocklist">
|
||||
* Add-ons that cause stability or security issues are put on a blocklist
|
||||
* </a>.
|
||||
*/
|
||||
public static class BlocklistStateFlags {
|
||||
// Keep in sync with nsIBlocklistService.idl
|
||||
/** This extension does not appear in the blocklist. */
|
||||
public final static int NOT_BLOCKED = 0;
|
||||
/** This extension is in the blocklist but the problem is not severe
|
||||
* enough to warant forcibly blocking. */
|
||||
public final static int SOFTBLOCKED = 1;
|
||||
/** This extension should be blocked and never used. */
|
||||
public final static int BLOCKED = 2;
|
||||
/** This extension is considered outdated, and there is a known update
|
||||
* available. */
|
||||
public final static int OUTDATED = 3;
|
||||
/** This extension is vulnerable and there is an update. */
|
||||
public final static int VULNERABLE_UPDATE_AVAILABLE = 4;
|
||||
/** This extension is vulnerable and there is no update. */
|
||||
public final static int VULNERABLE_NO_UPDATE = 5;
|
||||
}
|
||||
|
||||
@Retention(RetentionPolicy.SOURCE)
|
||||
@ -1080,22 +1186,106 @@ public class WebExtension {
|
||||
BlocklistStateFlags.VULNERABLE_NO_UPDATE})
|
||||
@interface BlocklistState {}
|
||||
|
||||
// TODO: make public
|
||||
class MetaData {
|
||||
final Icon icon;
|
||||
final String[] permissions;
|
||||
final String[] origins;
|
||||
final String name;
|
||||
final String description;
|
||||
final String version;
|
||||
final String creatorName;
|
||||
final String creatorUrl;
|
||||
final String homepageUrl;
|
||||
final String optionsPageUrl;
|
||||
/** Provides information about a {@link WebExtension}. */
|
||||
public class MetaData {
|
||||
/** Main {@link Icon} branding for this {@link WebExtension}.
|
||||
* Can be used when displaying prompts. */
|
||||
public final @NonNull Icon icon;
|
||||
/** API permissions requested or granted to this extension.
|
||||
*
|
||||
* Permission identifiers match entries in the manifest, see
|
||||
* <a href="https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/manifest.json/permissions#API_permissions">
|
||||
* API permissions
|
||||
* </a>.
|
||||
*/
|
||||
public final @NonNull String[] permissions;
|
||||
/** Host permissions requested or granted to this extension.
|
||||
*
|
||||
* See <a href="https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/manifest.json/permissions#Host_permissions">
|
||||
* Host permissions
|
||||
* </a>.
|
||||
*/
|
||||
public final @NonNull String[] origins;
|
||||
/** Branding name for this extension.
|
||||
*
|
||||
* See <a href="https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/manifest.json/name">
|
||||
* manifest.json/name
|
||||
* </a>
|
||||
*/
|
||||
public final @Nullable String name;
|
||||
/** Branding description for this extension. This string will be
|
||||
* localized using the current GeckoView language setting.
|
||||
*
|
||||
* See <a href="https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/manifest.json/description">
|
||||
* manifest.json/description
|
||||
* </a>
|
||||
*/
|
||||
public final @Nullable String description;
|
||||
/** Version string for this extension.
|
||||
*
|
||||
* See <a href="https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/manifest.json/version">
|
||||
* manifest.json/version
|
||||
* </a>
|
||||
*/
|
||||
public final @NonNull String version;
|
||||
/** Creator name as provided in the manifest.
|
||||
*
|
||||
* See <a href="https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/manifest.json/developer">
|
||||
* manifest.json/developer
|
||||
* </a>
|
||||
*/
|
||||
public final @Nullable String creatorName;
|
||||
/** Creator url as provided in the manifest.
|
||||
*
|
||||
* See <a href="https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/manifest.json/developer">
|
||||
* manifest.json/developer
|
||||
* </a>
|
||||
*/
|
||||
public final @Nullable String creatorUrl;
|
||||
/** Homepage url as provided in the manifest.
|
||||
*
|
||||
* See <a href="https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/manifest.json/homepage_url">
|
||||
* manifest.json/homepage_url
|
||||
* </a>
|
||||
*/
|
||||
public final @Nullable String homepageUrl;
|
||||
/** Options page as provided in the manifest.
|
||||
*
|
||||
* See <a href="https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/manifest.json/options_ui">
|
||||
* manifest.json/options_ui
|
||||
* </a>
|
||||
*/
|
||||
// TODO: Bug 1598792
|
||||
final @Nullable String optionsPageUrl;
|
||||
/** Whether the options page should be open in a Tab or not.
|
||||
*
|
||||
* See <a href="https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/manifest.json/options_ui#Syntax">
|
||||
* manifest.json/options_ui#Syntax
|
||||
* </a>
|
||||
*/
|
||||
// TODO: Bug 1598792
|
||||
final boolean openOptionsPageInTab;
|
||||
final boolean isRecommended;
|
||||
final @BlocklistState int blocklistState;
|
||||
final @SignedState int signedState;
|
||||
/** Whether or not this is a recommended extension.
|
||||
*
|
||||
* See <a href="https://blog.mozilla.org/firefox/firefox-recommended-extensions/">
|
||||
* Recommended Extensions program
|
||||
* </a>
|
||||
*/
|
||||
public final boolean isRecommended;
|
||||
/** Blocklist status for this extension.
|
||||
*
|
||||
* See <a href="https://support.mozilla.org/en-US/kb/add-ons-cause-issues-are-on-blocklist">
|
||||
* Add-ons that cause stability or security issues are put on a blocklist
|
||||
* </a>.
|
||||
*/
|
||||
public final @BlocklistState int blocklistState;
|
||||
/** Signed status for this extension.
|
||||
*
|
||||
* See <a href="https://support.mozilla.org/en-US/kb/add-on-signing-in-firefox">
|
||||
* Add-on signing in Firefox.
|
||||
* </a>.
|
||||
*/
|
||||
public final @SignedState int signedState;
|
||||
|
||||
/** Override for testing. */
|
||||
protected MetaData() {
|
||||
|
@ -1,5 +1,6 @@
|
||||
package org.mozilla.geckoview;
|
||||
|
||||
import android.support.annotation.AnyThread;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.annotation.UiThread;
|
||||
@ -22,8 +23,6 @@ public class WebExtensionController {
|
||||
|
||||
private GeckoRuntime mRuntime;
|
||||
|
||||
private boolean mHandlerRegistered = false;
|
||||
|
||||
private TabDelegate mTabDelegate;
|
||||
private PromptDelegate mPromptDelegate;
|
||||
|
||||
@ -33,12 +32,18 @@ public class WebExtensionController {
|
||||
public GeckoResult<WebExtension> get(final String id) {
|
||||
final WebExtension extension = mData.get(id);
|
||||
if (extension == null) {
|
||||
if (BuildConfig.DEBUG) {
|
||||
// TODO: Bug 1582185 Some gecko tests install WebExtensions that we
|
||||
// don't know about and cause this to trigger.
|
||||
// throw new RuntimeException("Could not find extension: " + extensionId);
|
||||
}
|
||||
Log.e(LOGTAG, "Could not find extension: " + id);
|
||||
final WebExtensionResult result = new WebExtensionResult("extension");
|
||||
|
||||
final GeckoBundle bundle = new GeckoBundle(1);
|
||||
bundle.putString("extensionId", id);
|
||||
|
||||
EventDispatcher.getInstance().dispatch("GeckoView:WebExtension:Get",
|
||||
bundle, result);
|
||||
|
||||
return result.then(ext -> {
|
||||
mData.put(ext.id, ext);
|
||||
return GeckoResult.fromValue(ext);
|
||||
});
|
||||
}
|
||||
|
||||
return GeckoResult.fromValue(extension);
|
||||
@ -48,7 +53,6 @@ public class WebExtensionController {
|
||||
mData.remove(id);
|
||||
}
|
||||
|
||||
// TODO: remove once registerWebExtension is removed
|
||||
public void put(final String id, final WebExtension extension) {
|
||||
mData.put(id, extension);
|
||||
}
|
||||
@ -62,19 +66,54 @@ public class WebExtensionController {
|
||||
|
||||
// Avoids exposing listeners to the API
|
||||
private class Internals implements BundleEventListener,
|
||||
WebExtension.Port.DisconnectDelegate {
|
||||
WebExtension.Port.DisconnectDelegate,
|
||||
WebExtension.DelegateObserver {
|
||||
private boolean mMessageListenersAttached = false;
|
||||
private boolean mActionListenersAttached = false;
|
||||
|
||||
@Override
|
||||
// BundleEventListener
|
||||
public void handleMessage(final String event, final GeckoBundle message,
|
||||
final EventCallback callback) {
|
||||
WebExtensionController.this.handleMessage(event, message, callback, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
// WebExtension.Port.DisconnectDelegate
|
||||
public void onDisconnectFromApp(final WebExtension.Port port) {
|
||||
// If the port has been disconnected from the app side, we don't need to notify anyone and
|
||||
// we just need to remove it from our list of ports.
|
||||
mPorts.remove(port.id);
|
||||
}
|
||||
|
||||
@Override
|
||||
// WebExtension.DelegateObserver
|
||||
public void onMessageDelegate(final String nativeApp,
|
||||
final WebExtension.MessageDelegate delegate) {
|
||||
if (delegate != null && !mMessageListenersAttached) {
|
||||
EventDispatcher.getInstance().registerUiThreadListener(
|
||||
this,
|
||||
"GeckoView:WebExtension:Message",
|
||||
"GeckoView:WebExtension:PortMessage",
|
||||
"GeckoView:WebExtension:Connect",
|
||||
"GeckoView:WebExtension:Disconnect");
|
||||
mMessageListenersAttached = true;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
// WebExtension.DelegateObserver
|
||||
public void onActionDelegate(final WebExtension.ActionDelegate delegate) {
|
||||
if (delegate != null && !mActionListenersAttached) {
|
||||
EventDispatcher.getInstance().registerUiThreadListener(
|
||||
this,
|
||||
"GeckoView:BrowserAction:Update",
|
||||
"GeckoView:BrowserAction:OpenPopup",
|
||||
"GeckoView:PageAction:Update",
|
||||
"GeckoView:PageAction:OpenPopup");
|
||||
mActionListenersAttached = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public interface TabDelegate {
|
||||
@ -139,31 +178,64 @@ public class WebExtensionController {
|
||||
mTabDelegate = delegate;
|
||||
}
|
||||
|
||||
// TODO: make public
|
||||
interface PromptDelegate {
|
||||
default GeckoResult<AllowOrDeny> onInstallPrompt(WebExtension extension) {
|
||||
/**
|
||||
* This delegate will be called whenever an extension is about to be installed or it needs
|
||||
* new permissions, e.g during an update or because it called <code>permissions.request</code>
|
||||
*/
|
||||
@UiThread
|
||||
public interface PromptDelegate {
|
||||
/**
|
||||
* Called whenever a new extension is being installed. This is intended as an
|
||||
* opportunity for the app to prompt the user for the permissions required by
|
||||
* this extension.
|
||||
*
|
||||
* @param extension The {@link WebExtension} that is about to be installed.
|
||||
* You can use {@link WebExtension#metaData} to gather information
|
||||
* about this extension when building the user prompt dialog.
|
||||
* @return A {@link GeckoResult} that completes to either {@link AllowOrDeny#ALLOW ALLOW}
|
||||
* if this extension should be installed or {@link AllowOrDeny#DENY DENY} if
|
||||
* this extension should not be installed. A null value will be interpreted as
|
||||
* {@link AllowOrDeny#DENY DENY}.
|
||||
*/
|
||||
@Nullable
|
||||
default GeckoResult<AllowOrDeny> onInstallPrompt(final @NonNull WebExtension extension) {
|
||||
return null;
|
||||
}
|
||||
|
||||
/*
|
||||
TODO: Bug 1599581
|
||||
default GeckoResult<AllowOrDeny> onUpdatePrompt(
|
||||
WebExtension currentlyInstalled,
|
||||
WebExtension updatedExtension,
|
||||
String[] newPermissions) {
|
||||
return null;
|
||||
}
|
||||
TODO: Bug 1601420
|
||||
default GeckoResult<AllowOrDeny> onOptionalPrompt(
|
||||
WebExtension extension,
|
||||
String[] optionalPermissions) {
|
||||
return null;
|
||||
}
|
||||
} */
|
||||
}
|
||||
|
||||
// TODO: make public
|
||||
PromptDelegate getPromptDelegate() {
|
||||
/**
|
||||
* @return the current {@link PromptDelegate} instance.
|
||||
* @see PromptDelegate
|
||||
*/
|
||||
@UiThread
|
||||
@Nullable
|
||||
public PromptDelegate getPromptDelegate() {
|
||||
return mPromptDelegate;
|
||||
}
|
||||
|
||||
// TODO: make public
|
||||
void setPromptDelegate(final PromptDelegate delegate) {
|
||||
/** Set the {@link PromptDelegate} for this instance. This delegate will be used
|
||||
* to be notified whenever an extension is being installed or needs new permissions.
|
||||
*
|
||||
* @param delegate the delegate instance.
|
||||
* @see PromptDelegate
|
||||
*/
|
||||
@UiThread
|
||||
public void setPromptDelegate(final @Nullable PromptDelegate delegate) {
|
||||
if (delegate == null && mPromptDelegate != null) {
|
||||
EventDispatcher.getInstance().unregisterUiThreadListener(
|
||||
mInternals,
|
||||
@ -183,7 +255,8 @@ public class WebExtensionController {
|
||||
mPromptDelegate = delegate;
|
||||
}
|
||||
|
||||
private static class WebExtensionResult extends CallbackResult<WebExtension> {
|
||||
private static class WebExtensionResult extends GeckoResult<WebExtension>
|
||||
implements EventCallback {
|
||||
private final String mFieldName;
|
||||
|
||||
public WebExtensionResult(final String fieldName) {
|
||||
@ -195,11 +268,59 @@ public class WebExtensionController {
|
||||
final GeckoBundle bundle = (GeckoBundle) response;
|
||||
complete(new WebExtension(bundle.getBundle(mFieldName)));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendError(final Object response) {
|
||||
if (response instanceof GeckoBundle
|
||||
&& ((GeckoBundle) response).containsKey("installError")) {
|
||||
final GeckoBundle bundle = (GeckoBundle) response;
|
||||
final int errorCode = bundle.getInt("installError");
|
||||
completeExceptionally(new WebExtension.InstallException(errorCode));
|
||||
} else {
|
||||
completeExceptionally(new Exception(response.toString()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: make public
|
||||
GeckoResult<WebExtension> install(final String uri) {
|
||||
final CallbackResult<WebExtension> result = new WebExtensionResult("extension");
|
||||
/**
|
||||
* Install an extension.
|
||||
*
|
||||
* An installed extension will persist and will be available even when restarting the
|
||||
* {@link GeckoRuntime}.
|
||||
*
|
||||
* Installed extensions through this method need to be signed by Mozilla, see
|
||||
* <a href="https://extensionworkshop.com/documentation/publish/signing-and-distribution-overview/#distributing-your-addon">
|
||||
* Distributing your add-on
|
||||
* </a>.
|
||||
*
|
||||
* When calling this method, the GeckoView library will download the extension, validate
|
||||
* its manifest and signature, and give you an opportunity to verify its permissions through
|
||||
* {@link PromptDelegate#installPrompt}, you can use this method to prompt the user if
|
||||
* appropriate.
|
||||
*
|
||||
* @param uri URI to the extension's <code>.xpi</code> package. This can be a remote
|
||||
* <code>https:</code> URI or a local <code>file:</code> or <code>resource:</code>
|
||||
* URI. Note: the app needs the appropriate permissions for local URIs.
|
||||
*
|
||||
* @return A {@link GeckoResult} that will complete when the installation process finishes.
|
||||
* For successful installations, the GeckoResult will return the {@link WebExtension}
|
||||
* object that you can use to set delegates and retrieve information about the
|
||||
* WebExtension using {@link WebExtension#metaData}.
|
||||
*
|
||||
* If an error occurs during the installation process, the GeckoResult will complete
|
||||
* exceptionally with a
|
||||
* {@link WebExtension.InstallException InstallException} that will contain
|
||||
* the relevant error code in
|
||||
* {@link WebExtension.InstallException#code InstallException#code}.
|
||||
*
|
||||
* @see PromptDelegate#installPrompt
|
||||
* @see WebExtension.InstallException.ErrorCodes
|
||||
* @see WebExtension#metaData
|
||||
*/
|
||||
@NonNull
|
||||
@AnyThread
|
||||
public GeckoResult<WebExtension> install(final @NonNull String uri) {
|
||||
final WebExtensionResult result = new WebExtensionResult("extension");
|
||||
|
||||
final GeckoBundle bundle = new GeckoBundle(1);
|
||||
bundle.putString("locationUri", uri);
|
||||
@ -207,12 +328,15 @@ public class WebExtensionController {
|
||||
EventDispatcher.getInstance().dispatch("GeckoView:WebExtension:Install",
|
||||
bundle, result);
|
||||
|
||||
return result;
|
||||
return result.then(extension -> {
|
||||
registerWebExtension(extension);
|
||||
return GeckoResult.fromValue(extension);
|
||||
});
|
||||
}
|
||||
|
||||
// TODO: make public
|
||||
// TODO: Bug 1601067 make public
|
||||
GeckoResult<WebExtension> installBuiltIn(final String uri) {
|
||||
final CallbackResult<WebExtension> result = new WebExtensionResult("extension");
|
||||
final WebExtensionResult result = new WebExtensionResult("extension");
|
||||
|
||||
final GeckoBundle bundle = new GeckoBundle(1);
|
||||
bundle.putString("locationUri", uri);
|
||||
@ -220,11 +344,25 @@ public class WebExtensionController {
|
||||
EventDispatcher.getInstance().dispatch("GeckoView:WebExtension:InstallBuiltIn",
|
||||
bundle, result);
|
||||
|
||||
return result;
|
||||
return result.then(extension -> {
|
||||
registerWebExtension(extension);
|
||||
return GeckoResult.fromValue(extension);
|
||||
});
|
||||
}
|
||||
|
||||
// TODO: make public
|
||||
GeckoResult<Void> uninstall(final WebExtension extension) {
|
||||
/**
|
||||
* Uninstall an extension.
|
||||
*
|
||||
* Uninstalling an extension will remove it from the current {@link GeckoRuntime} instance,
|
||||
* delete all its data and trigger a request to close all extension pages currently open.
|
||||
*
|
||||
* @param extension The {@link WebExtension} to be uninstalled.
|
||||
*
|
||||
* @return A {@link GeckoResult} that will complete when the uninstall process is completed.
|
||||
*/
|
||||
@NonNull
|
||||
@AnyThread
|
||||
public GeckoResult<Void> uninstall(final @NonNull WebExtension extension) {
|
||||
final CallbackResult<Void> result = new CallbackResult<Void>() {
|
||||
@Override
|
||||
public void sendSuccess(final Object response) {
|
||||
@ -232,6 +370,8 @@ public class WebExtensionController {
|
||||
}
|
||||
};
|
||||
|
||||
unregisterWebExtension(extension);
|
||||
|
||||
final GeckoBundle bundle = new GeckoBundle(1);
|
||||
bundle.putString("webExtensionId", extension.id);
|
||||
|
||||
@ -241,9 +381,9 @@ public class WebExtensionController {
|
||||
return result;
|
||||
}
|
||||
|
||||
// TODO: make public
|
||||
// TODO: Bug 1599585 make public
|
||||
GeckoResult<WebExtension> enable(final WebExtension extension) {
|
||||
final CallbackResult<WebExtension> result = new WebExtensionResult("extension");
|
||||
final WebExtensionResult result = new WebExtensionResult("extension");
|
||||
|
||||
final GeckoBundle bundle = new GeckoBundle(1);
|
||||
bundle.putString("webExtensionId", extension.id);
|
||||
@ -251,12 +391,15 @@ public class WebExtensionController {
|
||||
EventDispatcher.getInstance().dispatch("GeckoView:WebExtension:Enable",
|
||||
bundle, result);
|
||||
|
||||
return result;
|
||||
return result.then(newExtension -> {
|
||||
registerWebExtension(newExtension);
|
||||
return GeckoResult.fromValue(newExtension);
|
||||
});
|
||||
}
|
||||
|
||||
// TODO: make public
|
||||
// TODO: Bug 1599585 make public
|
||||
GeckoResult<WebExtension> disable(final WebExtension extension) {
|
||||
final CallbackResult<WebExtension> result = new WebExtensionResult("extension");
|
||||
final WebExtensionResult result = new WebExtensionResult("extension");
|
||||
|
||||
final GeckoBundle bundle = new GeckoBundle(1);
|
||||
bundle.putString("webExtensionId", extension.id);
|
||||
@ -264,10 +407,13 @@ public class WebExtensionController {
|
||||
EventDispatcher.getInstance().dispatch("GeckoView:WebExtension:Disable",
|
||||
bundle, result);
|
||||
|
||||
return result;
|
||||
return result.then(newExtension -> {
|
||||
registerWebExtension(newExtension);
|
||||
return GeckoResult.fromValue(newExtension);
|
||||
});
|
||||
}
|
||||
|
||||
// TODO: make public
|
||||
// TODO: Bug 1600742 make public
|
||||
GeckoResult<List<WebExtension>> listInstalled() {
|
||||
final CallbackResult<List<WebExtension>> result = new CallbackResult<List<WebExtension>>() {
|
||||
@Override
|
||||
@ -276,7 +422,9 @@ public class WebExtensionController {
|
||||
.getBundleArray("extensions");
|
||||
final List<WebExtension> list = new ArrayList<>(bundles.length);
|
||||
for (GeckoBundle bundle : bundles) {
|
||||
list.add(new WebExtension(bundle));
|
||||
final WebExtension extension = new WebExtension(bundle);
|
||||
registerWebExtension(extension);
|
||||
list.add(extension);
|
||||
}
|
||||
|
||||
complete(list);
|
||||
@ -289,9 +437,9 @@ public class WebExtensionController {
|
||||
return result;
|
||||
}
|
||||
|
||||
// TODO: make public
|
||||
// TODO: Bug 1599581 make public
|
||||
GeckoResult<WebExtension> update(final WebExtension extension) {
|
||||
final CallbackResult<WebExtension> result = new WebExtensionResult("extension");
|
||||
final WebExtensionResult result = new WebExtensionResult("extension");
|
||||
|
||||
final GeckoBundle bundle = new GeckoBundle(1);
|
||||
bundle.putString("webExtensionId", extension.id);
|
||||
@ -299,7 +447,10 @@ public class WebExtensionController {
|
||||
EventDispatcher.getInstance().dispatch("GeckoView:WebExtension:Update",
|
||||
bundle, result);
|
||||
|
||||
return result;
|
||||
return result.then(newExtension -> {
|
||||
registerWebExtension(newExtension);
|
||||
return GeckoResult.fromValue(newExtension);
|
||||
});
|
||||
}
|
||||
|
||||
/* package */ WebExtensionController(final GeckoRuntime runtime) {
|
||||
@ -307,23 +458,7 @@ public class WebExtensionController {
|
||||
}
|
||||
|
||||
/* package */ void registerWebExtension(final WebExtension webExtension) {
|
||||
if (!mHandlerRegistered) {
|
||||
EventDispatcher.getInstance().registerUiThreadListener(
|
||||
mInternals,
|
||||
"GeckoView:WebExtension:Message",
|
||||
"GeckoView:WebExtension:PortMessage",
|
||||
"GeckoView:WebExtension:Connect",
|
||||
"GeckoView:WebExtension:Disconnect",
|
||||
|
||||
// {Browser,Page}Actions
|
||||
"GeckoView:BrowserAction:Update",
|
||||
"GeckoView:BrowserAction:OpenPopup",
|
||||
"GeckoView:PageAction:Update",
|
||||
"GeckoView:PageAction:OpenPopup"
|
||||
);
|
||||
mHandlerRegistered = true;
|
||||
}
|
||||
|
||||
webExtension.setDelegateObserver(mInternals);
|
||||
mExtensions.put(webExtension.id, webExtension);
|
||||
}
|
||||
|
||||
@ -350,6 +485,9 @@ public class WebExtensionController {
|
||||
} else if ("GeckoView:PageAction:OpenPopup".equals(event)) {
|
||||
openPopup(message, session, WebExtension.Action.TYPE_PAGE_ACTION);
|
||||
return;
|
||||
} else if ("GeckoView:WebExtension:InstallPrompt".equals(event)) {
|
||||
installPrompt(message, callback);
|
||||
return;
|
||||
}
|
||||
|
||||
final String nativeApp = message.getString("nativeApp");
|
||||
@ -381,6 +519,42 @@ public class WebExtensionController {
|
||||
});
|
||||
}
|
||||
|
||||
private void installPrompt(final GeckoBundle message, final EventCallback callback) {
|
||||
final GeckoBundle extensionBundle = message.getBundle("extension");
|
||||
if (extensionBundle == null || !extensionBundle.containsKey("webExtensionId")
|
||||
|| !extensionBundle.containsKey("locationURI")) {
|
||||
if (BuildConfig.DEBUG) {
|
||||
throw new RuntimeException("Missing webExtensionId or locationURI");
|
||||
}
|
||||
|
||||
Log.e(LOGTAG, "Missing webExtensionId or locationURI");
|
||||
return;
|
||||
}
|
||||
|
||||
final WebExtension extension = new WebExtension(extensionBundle);
|
||||
|
||||
if (mPromptDelegate == null) {
|
||||
Log.e(LOGTAG, "Tried to install extension " + extension.id +
|
||||
" but no delegate is registered");
|
||||
return;
|
||||
}
|
||||
|
||||
final GeckoResult<AllowOrDeny> promptResponse = mPromptDelegate.onInstallPrompt(extension);
|
||||
if (promptResponse == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
promptResponse.accept(allowOrDeny -> {
|
||||
GeckoBundle response = new GeckoBundle(1);
|
||||
if (AllowOrDeny.ALLOW.equals(allowOrDeny)) {
|
||||
response.putBoolean("allow", true);
|
||||
} else {
|
||||
response.putBoolean("allow", false);
|
||||
}
|
||||
callback.sendSuccess(response);
|
||||
});
|
||||
}
|
||||
|
||||
private void newTab(final GeckoBundle message, final EventCallback callback) {
|
||||
if (mTabDelegate == null) {
|
||||
callback.sendSuccess(null);
|
||||
@ -411,8 +585,12 @@ public class WebExtensionController {
|
||||
return;
|
||||
}
|
||||
|
||||
mExtensions.get(message.getString("extensionId")).then(extension ->
|
||||
mTabDelegate.onCloseTab(extension, session)
|
||||
mExtensions.get(message.getString("extensionId")).then(
|
||||
extension -> mTabDelegate.onCloseTab(extension, session),
|
||||
// On uninstall, we close all extension pages, in that case
|
||||
// the extension object may be gone already so we can't
|
||||
// send it to the delegate
|
||||
exception -> mTabDelegate.onCloseTab(null, session)
|
||||
).accept(value -> {
|
||||
if (value == AllowOrDeny.ALLOW) {
|
||||
callback.sendSuccess(null);
|
||||
@ -425,6 +603,7 @@ public class WebExtensionController {
|
||||
|
||||
/* package */ void unregisterWebExtension(final WebExtension webExtension) {
|
||||
mExtensions.remove(webExtension.id);
|
||||
webExtension.setDelegateObserver(null);
|
||||
|
||||
// Some ports may still be open so we need to go through the list and close all of the
|
||||
// ports tied to this web extension
|
||||
|
@ -13,6 +13,13 @@ exclude: true
|
||||
|
||||
⚠️ breaking change
|
||||
|
||||
## v73
|
||||
- Added [`WebExtensionController.install`][73.1] and [`uninstall`][73.2] to
|
||||
manage installed extensions
|
||||
|
||||
[73.1]: {{javadoc_uri}}/WebExtensionController.html#install-java.lang.String-
|
||||
[73.2]: {{javadoc_uri}}/WebExtensionController.html#uninstall-org.mozilla.geckoview.WebExtension-
|
||||
|
||||
## v72
|
||||
- Added [`GeckoSession.NavigationDelegate.LoadRequest#hasUserGesture`][72.1]. This indicates
|
||||
if a load was requested while a user gesture was active (e.g., a tap).
|
||||
@ -476,4 +483,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]: 4c9f04038d8478206efac05b518920819faeacea
|
||||
[api-version]: 5856cdf682140fafdd09d74dbc004bf0b6bb7398
|
||||
|
@ -19,12 +19,20 @@ const { GeckoViewUtils } = ChromeUtils.import(
|
||||
const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
|
||||
|
||||
XPCOMUtils.defineLazyModuleGetters(this, {
|
||||
AddonManager: "resource://gre/modules/AddonManager.jsm",
|
||||
EventDispatcher: "resource://gre/modules/Messaging.jsm",
|
||||
Extension: "resource://gre/modules/Extension.jsm",
|
||||
ExtensionChild: "resource://gre/modules/ExtensionChild.jsm",
|
||||
GeckoViewTabBridge: "resource://gre/modules/GeckoViewTab.jsm",
|
||||
});
|
||||
|
||||
XPCOMUtils.defineLazyServiceGetter(
|
||||
this,
|
||||
"mimeService",
|
||||
"@mozilla.org/mime;1",
|
||||
"nsIMIMEService"
|
||||
);
|
||||
|
||||
const { debug, warn } = GeckoViewUtils.initLogging("Console"); // eslint-disable-line no-unused-vars
|
||||
|
||||
/** Provides common logic between page and browser actions */
|
||||
@ -231,6 +239,133 @@ class GeckoViewConnection {
|
||||
}
|
||||
}
|
||||
|
||||
function exportExtension(aAddon, aPermissions, aSourceURI) {
|
||||
const { origins, permissions } = aPermissions;
|
||||
const {
|
||||
creator,
|
||||
description,
|
||||
homepageURL,
|
||||
signedState,
|
||||
name,
|
||||
icons,
|
||||
version,
|
||||
optionsURL,
|
||||
optionsBrowserStyle,
|
||||
isRecommended,
|
||||
blocklistState,
|
||||
isActive,
|
||||
isBuiltin,
|
||||
id,
|
||||
} = aAddon;
|
||||
let creatorName = null;
|
||||
let creatorURL = null;
|
||||
if (creator) {
|
||||
const { name, url } = creator;
|
||||
creatorName = name;
|
||||
creatorURL = url;
|
||||
}
|
||||
const openOptionsPageInTab =
|
||||
optionsBrowserStyle === AddonManager.OPTIONS_TYPE_TAB;
|
||||
return {
|
||||
webExtensionId: id,
|
||||
locationURI: aSourceURI != null ? aSourceURI.spec : "",
|
||||
isEnabled: isActive,
|
||||
isBuiltIn: isBuiltin,
|
||||
metaData: {
|
||||
permissions,
|
||||
origins,
|
||||
description,
|
||||
version,
|
||||
creatorName,
|
||||
creatorURL,
|
||||
homepageURL,
|
||||
name,
|
||||
optionsPageURL: optionsURL,
|
||||
openOptionsPageInTab,
|
||||
isRecommended,
|
||||
blocklistState,
|
||||
signedState,
|
||||
icons,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
class ExtensionInstallListener {
|
||||
constructor(aResolve) {
|
||||
this.resolve = aResolve;
|
||||
}
|
||||
|
||||
onDownloadCancelled(aInstall) {
|
||||
const { error: installError } = aInstall;
|
||||
this.resolve({ installError });
|
||||
}
|
||||
|
||||
onDownloadFailed(aInstall) {
|
||||
const { error: installError } = aInstall;
|
||||
this.resolve({ installError });
|
||||
}
|
||||
|
||||
onDownloadEnded() {
|
||||
// Nothing to do
|
||||
}
|
||||
|
||||
onInstallCancelled(aInstall) {
|
||||
const { error: installError } = aInstall;
|
||||
this.resolve({ installError });
|
||||
}
|
||||
|
||||
onInstallFailed(aInstall) {
|
||||
const { error: installError } = aInstall;
|
||||
this.resolve({ installError });
|
||||
}
|
||||
|
||||
onInstallEnded(aInstall, aAddon) {
|
||||
const extension = exportExtension(
|
||||
aAddon,
|
||||
aAddon.userPermissions,
|
||||
aInstall.sourceURI
|
||||
);
|
||||
this.resolve({ extension });
|
||||
}
|
||||
}
|
||||
|
||||
class ExtensionPromptObserver {
|
||||
constructor() {
|
||||
Services.obs.addObserver(this, "webextension-permission-prompt");
|
||||
}
|
||||
|
||||
async permissionPrompt(aInstall, aAddon, aInfo) {
|
||||
const { sourceURI } = aInstall;
|
||||
const { permissions } = aInfo;
|
||||
const extension = exportExtension(aAddon, permissions, sourceURI);
|
||||
const response = await EventDispatcher.instance.sendRequestForResult({
|
||||
type: "GeckoView:WebExtension:InstallPrompt",
|
||||
extension,
|
||||
});
|
||||
|
||||
if (response.allow) {
|
||||
aInfo.resolve();
|
||||
} else {
|
||||
aInfo.reject();
|
||||
}
|
||||
}
|
||||
|
||||
observe(aSubject, aTopic, aData) {
|
||||
debug`observe ${aTopic}`;
|
||||
|
||||
switch (aTopic) {
|
||||
case "webextension-permission-prompt": {
|
||||
const { info } = aSubject.wrappedJSObject;
|
||||
const { addon, install } = info;
|
||||
this.permissionPrompt(install, addon, info);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
new ExtensionPromptObserver();
|
||||
|
||||
var GeckoViewWebExtension = {
|
||||
async registerWebExtension(aId, aUri, allowContentMessaging, aCallback) {
|
||||
const params = {
|
||||
@ -268,44 +403,106 @@ var GeckoViewWebExtension = {
|
||||
},
|
||||
|
||||
async extensionById(aId) {
|
||||
const scope = this.extensionScopes.get(aId);
|
||||
let scope = this.extensionScopes.get(aId);
|
||||
if (!scope) {
|
||||
return null;
|
||||
// Check if this is an installed extension we haven't seen yet
|
||||
const addon = await AddonManager.getAddonByID(aId);
|
||||
if (!addon) {
|
||||
debug`Could not find extension with id=${aId}`;
|
||||
return null;
|
||||
}
|
||||
scope = {
|
||||
allowContentMessaging: false,
|
||||
extension: addon,
|
||||
};
|
||||
}
|
||||
|
||||
return scope.extension;
|
||||
},
|
||||
|
||||
async installWebExtension(aUri) {
|
||||
const install = await AddonManager.getInstallForURL(aUri.spec);
|
||||
const promise = new Promise(resolve => {
|
||||
install.addListener(new ExtensionInstallListener(resolve));
|
||||
});
|
||||
|
||||
const systemPrincipal = Services.scriptSecurityManager.getSystemPrincipal();
|
||||
const mimeType = mimeService.getTypeFromURI(aUri);
|
||||
AddonManager.installAddonFromWebpage(
|
||||
mimeType,
|
||||
null,
|
||||
systemPrincipal,
|
||||
install
|
||||
);
|
||||
|
||||
return promise;
|
||||
},
|
||||
|
||||
async uninstallWebExtension(aId) {
|
||||
const extension = await this.extensionById(aId);
|
||||
if (!extension) {
|
||||
throw new Error(`Could not find an extension with id='${aId}'.`);
|
||||
}
|
||||
|
||||
return extension.uninstall();
|
||||
},
|
||||
|
||||
async browserActionClick(aId) {
|
||||
const extension = await this.extensionById(aId);
|
||||
if (!extension) {
|
||||
return;
|
||||
}
|
||||
|
||||
const browserAction = this.browserActions.get(extension);
|
||||
if (!browserAction) {
|
||||
return;
|
||||
}
|
||||
|
||||
browserAction.click();
|
||||
},
|
||||
|
||||
async pageActionClick(aId) {
|
||||
const extension = await this.extensionById(aId);
|
||||
if (!extension) {
|
||||
return;
|
||||
}
|
||||
|
||||
const pageAction = this.pageActions.get(extension);
|
||||
if (!pageAction) {
|
||||
return;
|
||||
}
|
||||
|
||||
pageAction.click();
|
||||
},
|
||||
|
||||
async actionDelegateAttached(aId) {
|
||||
const extension = await this.extensionById(aId);
|
||||
if (!extension) {
|
||||
return;
|
||||
}
|
||||
|
||||
const browserAction = this.browserActions.get(extension);
|
||||
if (browserAction) {
|
||||
// Send information about this action to the delegate
|
||||
browserAction.updateOnChange(null);
|
||||
}
|
||||
|
||||
const pageAction = this.pageActions.get(extension);
|
||||
if (pageAction) {
|
||||
pageAction.updateOnChange(null);
|
||||
}
|
||||
},
|
||||
|
||||
async onEvent(aEvent, aData, aCallback) {
|
||||
debug`onEvent ${aEvent} ${aData}`;
|
||||
|
||||
switch (aEvent) {
|
||||
case "GeckoView:BrowserAction:Click": {
|
||||
const extension = await this.extensionById(aData.extensionId);
|
||||
if (!extension) {
|
||||
return;
|
||||
}
|
||||
|
||||
const browserAction = this.browserActions.get(extension);
|
||||
if (!browserAction) {
|
||||
return;
|
||||
}
|
||||
|
||||
browserAction.click();
|
||||
this.browserActionClick(aData.extensionId);
|
||||
break;
|
||||
}
|
||||
case "GeckoView:PageAction:Click": {
|
||||
const extension = await this.extensionById(aData.extensionId);
|
||||
if (!extension) {
|
||||
return;
|
||||
}
|
||||
|
||||
const pageAction = this.pageActions.get(extension);
|
||||
if (!pageAction) {
|
||||
return;
|
||||
}
|
||||
|
||||
pageAction.click();
|
||||
this.pageActionClick(aData.extensionId);
|
||||
break;
|
||||
}
|
||||
case "GeckoView:RegisterWebExtension": {
|
||||
@ -353,21 +550,7 @@ var GeckoViewWebExtension = {
|
||||
}
|
||||
|
||||
case "GeckoView:ActionDelegate:Attached": {
|
||||
const extension = await this.extensionById(aData.extensionId);
|
||||
if (!extension) {
|
||||
return;
|
||||
}
|
||||
|
||||
const browserAction = this.browserActions.get(extension);
|
||||
if (browserAction) {
|
||||
// Send information about this action to the delegate
|
||||
browserAction.updateOnChange(null);
|
||||
}
|
||||
|
||||
const pageAction = this.pageActions.get(extension);
|
||||
if (pageAction) {
|
||||
pageAction.updateOnChange(null);
|
||||
}
|
||||
this.actionDelegateAttached(aData.extensionId);
|
||||
break;
|
||||
}
|
||||
|
||||
@ -383,9 +566,44 @@ var GeckoViewWebExtension = {
|
||||
break;
|
||||
}
|
||||
|
||||
case "GeckoView:WebExtension:Get": {
|
||||
const extension = await this.extensionById(aData.extensionId);
|
||||
if (!extension) {
|
||||
aCallback.onError(
|
||||
`Could not find extension with id: ${aData.extensionId}`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
aCallback.onSuccess({
|
||||
extension: exportExtension(
|
||||
extension,
|
||||
extension.userPermissions,
|
||||
/* aSourceURI */ null
|
||||
),
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
case "GeckoView:WebExtension:Install": {
|
||||
// TODO
|
||||
aCallback.onError(`Not implemented`);
|
||||
const uri = Services.io.newURI(aData.locationUri);
|
||||
if (uri == null) {
|
||||
aCallback.onError(`Could not parse uri: ${uri}`);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await this.installWebExtension(uri);
|
||||
if (result.extension) {
|
||||
aCallback.onSuccess(result);
|
||||
} else {
|
||||
aCallback.onError(result);
|
||||
}
|
||||
} catch (ex) {
|
||||
debug`Install exception error ${ex}`;
|
||||
aCallback.onError(`Unexpected error: ${ex}`);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
@ -396,8 +614,15 @@ var GeckoViewWebExtension = {
|
||||
}
|
||||
|
||||
case "GeckoView:WebExtension:Uninstall": {
|
||||
// TODO
|
||||
aCallback.onError(`Not implemented`);
|
||||
try {
|
||||
await this.uninstallWebExtension(aData.webExtensionId);
|
||||
aCallback.onSuccess();
|
||||
} catch (ex) {
|
||||
debug`Failed uninstall ${ex}`;
|
||||
aCallback.onError(
|
||||
`This extension cannot be uninstalled. Error: ${ex}.`
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user