blob: 536356c6336bea04de724dfe03fb991bb993706a [file] [log] [blame]
// Copyright 2019 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:async';
import 'dart:io';
import 'package:fidl/fidl.dart';
import 'package:fidl_fuchsia_io/fidl_async.dart' as fidl_io;
import 'package:fidl_fuchsia_web/fidl_async.dart' as fidl_web;
import 'package:fuchsia_logger/logger.dart';
import 'package:fuchsia_scenic_flutter/child_view_connection.dart';
import 'package:fuchsia_services/services.dart';
import 'package:zircon/zircon.dart';
import 'package:fuchsia_scenic/views.dart';
import 'package:fuchsia_vfs/vfs.dart';
import 'utils.dart' as utils;
/// Helper class to help connect and interface with 'fuchsia.web.*' services.
class FuchsiaWebServices {
final fidl_web.ContextProviderProxy _contextProviderProxy =
fidl_web.ContextProviderProxy();
final fidl_web.ContextProxy _contextProxy = fidl_web.ContextProxy();
final fidl_web.FrameProxy _frameProxy = fidl_web.FrameProxy();
final fidl_web.NavigationControllerProxy _navigationControllerProxy =
fidl_web.NavigationControllerProxy();
final fidl_web.NavigationEventListenerBinding
_navigationEventObserverBinding =
fidl_web.NavigationEventListenerBinding();
ChildViewConnection _childViewConnection;
/// Constructs [FuchsiaWebServices] and connects to various 'fuchsia.web.*`
/// services.
FuchsiaWebServices() {
StartupContext.fromStartupInfo()
.incoming
.connectToService(_contextProviderProxy);
// TODO(nkorsote): [service_directory] is effectively the sandbox inside
// which the created Context will run. If you give it a direct handle to
// component's /svc directory then it'll have access to everything the
// component can access. Alternatively, refactor this to use an Outgoing
// Directory.
if (!Directory('/svc').existsSync()) {
log.shout('no /svc directory');
return;
}
final channel = Channel.fromFile('/svc');
final directory = fidl_io.DirectoryProxy()
..ctrl.bind(InterfaceHandle<fidl_io.Directory>(channel));
final composedDir =
ComposedPseudoDir(directory: directory, inheritedNodes: [
'fuchsia.fonts.Provider',
'fuchsia.logger.LogSink',
'fuchsia.media.Audio',
'fuchsia.mediacodec.CodecFactory',
'fuchsia.net.NameLookup',
'fuchsia.netstack.Netstack',
'fuchsia.posix.socket.Provider',
'fuchsia.process.Launcher',
'fuchsia.sysmem.Allocator',
'fuchsia.ui.input.ImeService',
'fuchsia.ui.input.ImeVisibilityService',
'fuchsia.ui.scenic.Scenic',
'fuchsia.vulkan.loader.Loader',
]);
final pair = ChannelPair();
composedDir.serve(InterfaceRequest<fidl_io.Node>(pair.first));
final fidl_web.CreateContextParams contextParams =
fidl_web.CreateContextParams(
serviceDirectory: InterfaceHandle<fidl_io.Directory>(pair.second));
_contextProviderProxy.create(contextParams, _contextProxy.ctrl.request());
_contextProxy.createFrame(frame.ctrl.request());
// Create token pair and pass one end to the webview and the other to child
// view connection which will be used to construct the child view widget
// that the webview will live in.
final tokenPair = ViewTokenPair();
frame.createView(tokenPair.viewToken);
_childViewConnection = ChildViewConnection(tokenPair.viewHolderToken);
frame.getNavigationController(_navigationControllerProxy.ctrl.request());
}
/// Sets the javascript log level for the frame.
Future<void> setJavaScriptLogLevel(fidl_web.ConsoleLogLevel level) {
return frame.setJavaScriptLogLevel(level);
}
/// Returns a connection to a child view.
///
/// It can be used to construct a [ChildView] widget that will display the
/// view's contents.
ChildViewConnection get childViewConnection => _childViewConnection;
/// Returns [fidl_web.NavigationControllerProxy]
fidl_web.NavigationControllerProxy get navigationController =>
_navigationControllerProxy;
/// Returns [fidl_web.FrameProxy]
fidl_web.FrameProxy get frame => _frameProxy;
/// Preforms the all the necessary cleanup.
void dispose() {
_navigationControllerProxy.ctrl.close();
_frameProxy.ctrl.close();
_contextProxy.ctrl.close();
_contextProviderProxy.ctrl.close();
_navigationEventObserverBinding.close();
}
/// Executes a UTF-8 encoded [script] in the frame if the frame's URL has
/// an origin which matches entries in [origins].
///
/// At least one [origins] entry must be specified.
/// If a wildcard "*" is specified in [origins], then the script will be
/// evaluated unconditionally.
///
/// Note that scripts share the same execution context as the document,
/// meaning that document may modify variables, classes, or objects set by
/// the script in arbitrary or unpredictable ways.
///
/// If an error occured, the FrameError will be set to one of these values:
/// BUFFER_NOT_UTF8: [script] is not UTF-8 encoded.
/// INVALID_ORIGIN: The Frame's current URL does not match any of the
/// values in [origins] or [origins] is an empty vector.
// TODO(crbug.com/900391): Investigate if we can run the scripts in
// isolated JS worlds.
Future<String> evaluateJavascript(List<String> origins, String script) async {
final buffer = utils.stringToBuffer(script);
// TODO(nkosote): add catchError and decorate the error based on the error
// code.
final result = await frame.executeJavaScript(origins, buffer);
return utils.bufferToString(result);
}
/// Executes a UTF-8 encoded `script` for every subsequent page load where the
/// [`fuchsia.web.Frame`]'s URL has an origin reflected in `origins`. The script is executed
/// early, prior to the execution of the document's scripts.
///
/// Scripts are identified by a client-managed identifier `id`. Any script previously injected
/// using the same `id` will be replaced.
///
/// The order in which multiple bindings are executed is the same as the order in which the
/// bindings were added. If a script is added which clobbers an existing script of the same
/// `id`, the previous script's precedence in the injection order will be preserved.
///
/// At least one `origins` entry must be specified. If a wildcard `"*"` is specified in
/// `origins`, then the script will be evaluated unconditionally.
///
/// If an error occured, the [`fuchsia.web.FrameError`] will be set to one of these values:
/// - `BUFFER_NOT_UTF8`: `script` is not UTF-8 encoded.
/// - `INVALID_ORIGIN`: `origins` is an empty vector.
Future<void> evaluateJavascriptBeforeLoad(
int id, List<String> origins, String script) async {
final buffer = utils.stringToBuffer(script);
// TODO(miguelfrde): add catchError and decorate the error based on the error
// code.
await frame.addBeforeLoadJavaScript(id, origins, buffer);
}
/// Posts a message to the [fidl_web.Frame]'s onMessage handler.
///
/// [targetOrigin] restricts message delivery to the specified origin. If
/// [targetOrigin] is "*", then the message will be sent to the document
/// regardless of its origin.
///
/// If an error occurred, the FrameError will be set to one of these values:
/// - INTERNAL_ERROR: The WebEngine failed to create a message pipe.
/// - BUFFER_NOT_UTF8: The script in [message]'s [fidl_web.WebMessage.data]
/// property is not UTF-8 encoded.
/// - INVALID_ORIGIN: origins is an empty vector.
/// - NO_DATA_IN_MESSAGE: The [fidl_web.WebMessage.data] property is missing
/// in [message].
Future<void> postMessage(
String targetOrigin,
String message, {
InterfaceRequest<fidl_web.MessagePort> outgoingMessagePortRequest,
}) {
final data = utils.stringToBuffer(message);
var msg = fidl_web.WebMessage(
data: data,
outgoingTransfer: outgoingMessagePortRequest != null
? [
fidl_web.OutgoingTransferable.withMessagePort(
outgoingMessagePortRequest)
]
: null,
);
// TODO(nkosote): add catchError and decorate the error based on the error
// code
return frame.postMessage(targetOrigin, msg);
}
/// Sets the listener for handling page navigation events.
///
/// The [observer] to use. Unregisters any existing listener if null.
void setNavigationEventListener(fidl_web.NavigationEventListener observer) {
frame.setNavigationEventListener(
_navigationEventObserverBinding.wrap(observer));
}
}