Bug 1675644 - Flush extension messages per-session and per-nativeApp. r=esawin

The extension code _tries_ to flush messages when the relevant delegate is attached.

The logic, however, is pretty flawed: we currently only flush runtime-messages
(i.e. not coming from a WebExtension Page) and we flush all messages when the
first delegate is attached, even though there could be messages for different
nativeApp values which don't have a delegate attached yet.

We also erroneusly return a rejected promise to javascript when a message is queued up.

This patch addresses the above by:

- Never rejecting a pending connection request, the connection request will be
  resolved when the delegate for the right nativeApp is attached.
- Making the pending messages queue per-nativeApp and per-session.
- Flushing pending messages when a session delegate is attached.

Differential Revision: https://phabricator.services.mozilla.com/D96645
This commit is contained in:
Agi Sferro 2020-11-11 22:46:22 +00:00
Родитель ba7fba8f98
Коммит db6763404e
9 изменённых файлов: 202 добавлений и 32 удалений

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

@ -0,0 +1,14 @@
{
"manifest_version": 2,
"name": "Test messages sent from extensions when restoring",
"version": "1.0",
"applications": {
"gecko": {
"id": "extension-page-restoring@tests.mozilla.org"
}
},
"permissions": [
"geckoViewAddons",
"nativeMessaging"
]
}

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

@ -0,0 +1,5 @@
browser.runtime.sendNativeMessage("browser1", "HELLO_FROM_PAGE_1");
browser.runtime.sendNativeMessage("browser2", "HELLO_FROM_PAGE_2");
const port = browser.runtime.connectNative("browser1");
port.postMessage("HELLO_FROM_PORT");

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

@ -0,0 +1,9 @@
<!DOCTYPE html><html>
<head>
<meta charset="utf-8">
</head>
<body>
<h1>Hello World!</h1>
<script src=tab-script.js></script>
</body>
</html>

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

@ -1,7 +1,5 @@
browser.runtime
.sendNativeMessage("badNativeApi", "errorerrorerror")
// This message should not be handled
.catch(runTest);
// This message should not be handled
browser.runtime.sendNativeMessage("badNativeApi", "errorerrorerror");
async function runTest() {
const response = await browser.runtime.sendNativeMessage(
@ -27,3 +25,5 @@ async function runTest() {
port.postMessage("testContentPortMessage");
}
runTest();

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

@ -1,7 +1,4 @@
browser.runtime
.sendNativeMessage("badNativeApi", "errorerrorerror")
// This message should not be handled
.catch(runTest);
browser.runtime.sendNativeMessage("badNativeApi", "errorerrorerror");
async function runTest() {
await browser.runtime.sendNativeMessage(
@ -10,3 +7,5 @@ async function runTest() {
);
browser.runtime.connectNative("browser");
}
runTest();

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

@ -1,7 +1,4 @@
browser.runtime
.sendNativeMessage("badNativeApi", "errorerrorerror")
// This message should not be handled
.catch(runTest);
browser.runtime.sendNativeMessage("badNativeApi", "errorerrorerror");
async function runTest() {
const response = await browser.runtime.sendNativeMessage(
@ -27,3 +24,5 @@ async function runTest() {
port.postMessage("testBackgroundPortMessage");
}
runTest();

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

@ -11,6 +11,7 @@ import org.hamcrest.core.StringEndsWith.endsWith
import org.hamcrest.core.IsEqual.equalTo
import org.json.JSONObject
import org.junit.Assert.*
import org.junit.Assume.assumeThat
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
@ -45,6 +46,8 @@ class WebExtensionTest : BaseSessionTest() {
"resource://android/assets/web_extensions/openoptionspage-1/"
private const val OPENOPTIONSPAGE_2_BACKGROUND: String =
"resource://android/assets/web_extensions/openoptionspage-2/"
private const val EXTENSION_PAGE_RESTORE: String =
"resource://android/assets/web_extensions/extension-page-restore/"
}
private val controller
@ -768,6 +771,94 @@ class WebExtensionTest : BaseSessionTest() {
newPrivateSession.close()
}
// Verifies that the following messages are received from an extension page loaded in the session
// - HELLO_FROM_PAGE_1 from nativeApp browser1
// - HELLO_FROM_PAGE_2 from nativeApp browser2
// - connection request from browser1
// - HELLO_FROM_PORT from the port opened at the above step
private fun testExtensionMessages(extension: WebExtension, session: GeckoSession) {
val messageResult2 = GeckoResult<String>()
session.webExtensionController.setMessageDelegate(
extension, object : WebExtension.MessageDelegate {
override fun onMessage(nativeApp: String, message: Any,
sender: WebExtension.MessageSender): GeckoResult<Any>? {
messageResult2.complete(message as String);
return null
}
}, "browser2")
val message2 = sessionRule.waitForResult(messageResult2)
assertThat("Message is received correctly", message2,
equalTo("HELLO_FROM_PAGE_2"))
val messageResult1 = GeckoResult<String>()
val portResult = GeckoResult<WebExtension.Port>()
session.webExtensionController.setMessageDelegate(
extension, object : WebExtension.MessageDelegate {
override fun onMessage(nativeApp: String, message: Any,
sender: WebExtension.MessageSender): GeckoResult<Any>? {
messageResult1.complete(message as String);
return null
}
override fun onConnect(port: WebExtension.Port) {
portResult.complete(port)
}
}, "browser1")
val message1 = sessionRule.waitForResult(messageResult1)
assertThat("Message is received correctly", message1,
equalTo("HELLO_FROM_PAGE_1"))
val port = sessionRule.waitForResult(portResult)
val portMessageResult = GeckoResult<String>()
port.setDelegate(object : WebExtension.PortDelegate {
override fun onPortMessage(message: Any, port: WebExtension.Port) {
portMessageResult.complete(message as String)
}
})
val portMessage = sessionRule.waitForResult(portMessageResult)
assertThat("Message is received correctly", portMessage,
equalTo("HELLO_FROM_PORT"))
}
// This test:
// - loads an extension that tries to send some messages when loading tab.html
// - verifies that the messages are received when loading the tab normally
// - verifies that the messages are received when restoring the tab in a fresh session
@Test
fun testRestoringExtensionPagePreservesMessages() {
// TODO: Bug 1648158
assumeThat(sessionRule.env.isFission, equalTo(false))
val extension = sessionRule.waitForResult(
controller.installBuiltIn(EXTENSION_PAGE_RESTORE))
sessionRule.session.loadUri("${extension.metaData.baseUrl}tab.html")
sessionRule.waitForPageStop()
var savedState : GeckoSession.SessionState? = null
sessionRule.waitUntilCalled(object : Callbacks.ProgressDelegate {
@AssertCalled(count=1)
override fun onSessionStateChange(session: GeckoSession, state: GeckoSession.SessionState) {
savedState = state
}
})
// Test that messages are received in the main session
testExtensionMessages(extension, sessionRule.session)
val newSession = sessionRule.createOpenSession()
newSession.restoreState(savedState!!)
newSession.waitForPageStop()
// Test that messages are received in a restored state
testExtensionMessages(extension, newSession)
sessionRule.waitForResult(controller.uninstall(extension))
}
// This test
// - Create and assign WebExtension TabDelegate to handle closing of tabs
// - Create new GeckoSession for WebExtension to close

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

@ -901,6 +901,11 @@ public class WebExtension {
final WebExtension.MessageDelegate delegate,
final String nativeApp) {
mMessageDelegates.put(new Sender(webExtension.id, nativeApp), delegate);
if (runtime != null && delegate != null) {
runtime.getWebExtensionController()
.releasePendingMessages(webExtension, nativeApp, mSession);
}
}
public WebExtension.MessageDelegate getMessageDelegate(final WebExtension webExtension,

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

@ -5,6 +5,8 @@ import androidx.annotation.IntDef;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.UiThread;
import android.os.Build;
import android.util.Log;
import org.json.JSONException;
@ -17,9 +19,11 @@ import org.mozilla.gecko.util.GeckoBundle;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.UUID;
import static org.mozilla.geckoview.WebExtension.InstallException.ErrorCodes.ERROR_POSTPONED;
@ -32,8 +36,8 @@ public class WebExtensionController {
private PromptDelegate mPromptDelegate;
private final WebExtension.Listener<WebExtension.TabDelegate> mListener;
// Map [ extensionId -> Message ]
private final MultiMap<String, Message> mPendingMessages;
// Map [ (extensionId, nativeApp, session) -> message ]
private final MultiMap<MessageRecipient, Message> mPendingMessages;
private final MultiMap<String, Message> mPendingNewTab;
private static class Message {
@ -124,8 +128,26 @@ public class WebExtensionController {
}
}
/* package */ void releasePendingMessages(final WebExtension extension, final String nativeApp,
final GeckoSession session) {
Log.i(LOGTAG, "releasePendingMessages:"
+ " extension=" + extension.id
+ " nativeApp=" + nativeApp
+ " session=" + session);
final List<Message> messages = mPendingMessages.remove(
new MessageRecipient(nativeApp, extension.id, session));
if (messages == null) {
return;
}
for (final Message message : messages) {
WebExtensionController.this.handleMessage(message.event, message.bundle,
message.callback, message.session);
}
}
private class DelegateController implements WebExtension.DelegateController {
private WebExtension mExtension;
private final WebExtension mExtension;
public DelegateController(final WebExtension extension) {
mExtension = extension;
@ -135,17 +157,6 @@ public class WebExtensionController {
public void onMessageDelegate(final String nativeApp,
final WebExtension.MessageDelegate delegate) {
mListener.setMessageDelegate(mExtension, delegate, nativeApp);
if (delegate == null) {
return;
}
for (final Message message : mPendingMessages.get(mExtension.id)) {
WebExtensionController.this.handleMessage(message.event, message.bundle,
message.callback, message.session);
}
mPendingMessages.remove(mExtension.id);
}
@Override
@ -1112,12 +1123,45 @@ public class WebExtensionController {
delegate = mListener.getMessageDelegate(sender.webExtension, nativeApp);
}
if (delegate == null) {
callback.sendError("Native app not found or this WebExtension does not have permissions.");
return null;
return delegate;
}
private static class MessageRecipient {
final public String webExtensionId;
final public String nativeApp;
final public GeckoSession session;
public MessageRecipient(final String webExtensionId, final String nativeApp,
final GeckoSession session) {
this.webExtensionId = webExtensionId;
this.nativeApp = nativeApp;
this.session = session;
}
return delegate;
private static boolean equals(final Object a, final Object b) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
return Objects.equals(a, b);
}
return (a == b) || (a != null && a.equals(b));
}
@Override
public boolean equals(final Object other) {
if (!(other instanceof MessageRecipient)) {
return false;
}
final MessageRecipient o = (MessageRecipient) other;
return equals(webExtensionId, o.webExtensionId) &&
equals(nativeApp, o.nativeApp) &&
equals(session, o.session);
}
@Override
public int hashCode() {
return Arrays.hashCode(new Object[] { webExtensionId, nativeApp, session });
}
}
private void connect(final String nativeApp, final long portId, final Message message,
@ -1132,7 +1176,9 @@ public class WebExtensionController {
final WebExtension.MessageDelegate delegate = getDelegate(nativeApp, sender,
message.callback);
if (delegate == null) {
mPendingMessages.add(sender.webExtension.id, message);
mPendingMessages.add(
new MessageRecipient(nativeApp, sender.webExtension.id, sender.session),
message);
return;
}
@ -1155,7 +1201,9 @@ public class WebExtensionController {
final WebExtension.MessageDelegate delegate = getDelegate(nativeApp, sender,
callback);
if (delegate == null) {
mPendingMessages.add(sender.webExtension.id, message);
mPendingMessages.add(
new MessageRecipient(nativeApp, sender.webExtension.id, sender.session),
message);
return;
}