| // 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)); |
| } |