blob: dad8d4545091e88a62b9c848bff74b8344c8a00b [file] [log] [blame]
// Copyright (c) 2022, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
@JS()
library;
import 'dart:async';
import 'dart:convert';
// TODO: https://github.com/dart-lang/webdev/issues/2508
// ignore: deprecated_member_use
import 'dart:js_util';
// TODO: https://github.com/dart-lang/webdev/issues/2508
// ignore: deprecated_member_use
import 'package:js/js.dart';
import 'chrome_api.dart';
import 'data_serializers.dart';
import 'logger.dart';
import 'utils.dart';
// A default response for the sendResponse callback.
//
// Prevents the message port from closing. See:
// https://developer.chrome.com/docs/extensions/mv3/messaging/#simple
final defaultResponse = jsify({'response': 'received'});
enum Script {
background,
copier,
debuggerPanel,
detector,
popup;
factory Script.fromString(String value) {
return Script.values.byName(value);
}
}
enum MessageType {
isAuthenticated,
connectFailure,
appId,
debugInfo,
debugStateChange,
devToolsUrl,
multipleAppsDetected;
factory MessageType.fromString(String value) {
return MessageType.values.byName(value);
}
}
class Message {
final Script to;
final Script from;
final MessageType type;
final String body;
final String? error;
Message({
required this.to,
required this.from,
required this.type,
required this.body,
this.error,
});
factory Message.fromJSON(String json) {
final decoded = jsonDecode(json) as Map<String, dynamic>;
return Message(
to: Script.fromString(decoded['to'] as String),
from: Script.fromString(decoded['from'] as String),
type: MessageType.fromString(decoded['type'] as String),
body: decoded['body'] as String,
error: decoded['error'] as String?,
);
}
String toJSON() {
return jsonEncode({
'to': to.name,
'from': from.name,
'type': type.name,
'body': body,
if (error != null) 'error': error,
});
}
}
void interceptMessage<T>({
required String? message,
required MessageType expectedType,
required Script expectedSender,
required Script expectedRecipient,
required MessageSender sender,
required void Function(T message) messageHandler,
}) {
if (message == null) return;
if (!_isLegitimateSender(sender)) return;
try {
final decodedMessage = Message.fromJSON(message);
if (decodedMessage.type != expectedType ||
decodedMessage.to != expectedRecipient ||
decodedMessage.from != expectedSender) {
return;
}
if (T == String) {
messageHandler(decodedMessage.body as T);
} else {
messageHandler(
serializers.deserialize(jsonDecode(decodedMessage.body)) as T,
);
}
} catch (error) {
debugError(
'Error intercepting $expectedType from '
'$expectedSender to $expectedRecipient: $error',
);
}
}
/// Send a message using the chrome.runtime.sendMessage API.
Future<bool> sendRuntimeMessage({
required MessageType type,
required String body,
required Script sender,
required Script recipient,
}) =>
_sendMessage(type: type, body: body, sender: sender, recipient: recipient);
/// Send a message using the chrome.tabs.sendMessage API.
Future<bool> sendTabsMessage({
required int tabId,
required MessageType type,
required String body,
required Script sender,
required Script recipient,
}) => _sendMessage(
tabId: tabId,
type: type,
body: body,
sender: sender,
recipient: recipient,
);
Future<bool> _sendMessage({
required MessageType type,
required String body,
required Script sender,
required Script recipient,
int? tabId,
}) {
final message =
Message(to: recipient, from: sender, type: type, body: body).toJSON();
final completer = Completer<bool>();
void responseHandler([dynamic _]) {
final error = chrome.runtime.lastError;
if (error != null) {
debugError(
'Error sending $type to $recipient from $sender: ${error.message}',
);
}
completer.complete(error != null);
}
if (tabId != null) {
chrome.tabs.sendMessage(
tabId,
message,
// options
null,
allowInterop(responseHandler),
);
} else {
chrome.runtime.sendMessage(
// id
null,
message,
// options
null,
allowInterop(responseHandler),
);
}
return completer.future;
}
// Verify the message sender is our extension.
bool _isLegitimateSender(MessageSender sender) {
// Check that the sender ID matches our extension ID:
if (sender.id != chrome.runtime.id) return false;
final senderUri = Uri.parse(sender.origin ?? '');
final senderHost = senderUri.host;
final isDartAppHost =
senderHost == 'localhost' ||
senderHost == '127.0.0.1' ||
_isGoogleHost(senderHost);
final isExtensionOrigin =
senderHost == chrome.runtime.id && senderUri.scheme == 'chrome-extension';
if (isDartAppHost || isExtensionOrigin) return true;
// If the sender's host is unexpected, display an error.
displayNotification(
'Unexpected sender ${sender.origin}. Please file a bug at go/dde-bug or https://github.com/dart-lang/webdev',
isError: true,
);
return false;
}
bool _isGoogleHost(String host) {
const googleSuffices = ['.googlers.com', '.google.com', '.googleprod.com'];
return googleSuffices.any((suffix) => host.endsWith(suffix));
}