geary/src/client/components/components-web-view.vala
Michael Gratton 2892189094 Components.WebView: Fix CI failure building with WebKitGTK >= 2.31
The `webkit_settings_set_enable_plugins` method has been deprecated in
2.31, and calling it emits a warning which trips up the test suite.
2021-01-14 22:00:12 +11:00

837 lines
31 KiB
Vala

/*
* Copyright © 2016 Software Freedom Conservancy Inc.
* Copyright © 2016-2020 Michael Gratton <mike@vee.net>
*
* This software is licensed under the GNU Lesser General Public License
* (version 2.1 or later). See the COPYING file in this distribution.
*/
/**
* Base class for all WebKit2 WebView instances used by the Geary client.
*
* This provides common functionality expected by the client for
* displaying HTML, such as common WebKit settings, desktop font
* integration, Inspector support, and remote and inline image
* handling.
*/
public abstract class Components.WebView : WebKit.WebView, Geary.BaseInterface {
/** URI Scheme and delimiter for internal resource loads. */
public const string INTERNAL_URL_PREFIX = "geary:";
/** URI for internal message body page loads. */
public const string INTERNAL_URL_BODY = INTERNAL_URL_PREFIX + "body";
/** URI Scheme and delimiter for images loaded by Content-ID. */
public const string CID_URL_PREFIX = "cid:";
// Keep these in sync with GearyWebExtension
private const string MESSAGE_RETURN_VALUE_NAME = "__return__";
private const string MESSAGE_EXCEPTION_NAME = "__exception__";
// WebKit message handler names
private const string COMMAND_STACK_CHANGED = "command_stack_changed";
private const string CONTENT_LOADED = "content_loaded";
private const string DOCUMENT_MODIFIED = "document_modified";
private const string PREFERRED_HEIGHT_CHANGED = "preferred_height_changed";
private const string REMOTE_IMAGE_LOAD_BLOCKED = "remote_image_load_blocked";
private const string SELECTION_CHANGED = "selection_changed";
private const double ZOOM_DEFAULT = 1.0;
private const double ZOOM_FACTOR = 0.1;
private const double ZOOM_MAX = 2.0;
private const double ZOOM_MIN = 0.5;
private const string USER_CSS = "user-style.css";
private const string USER_CSS_LEGACY = "user-message.css";
// Workaround WK binding ctor not accepting any args
private class WebsiteDataManager : WebKit.WebsiteDataManager {
public WebsiteDataManager(string base_cache_directory) {
// Use the cache dir for both cache and data since a)
// emails shouldn't be storing data anyway, and b) so WK
// doesn't use the default, shared data dir.
Object(
base_cache_directory: base_cache_directory,
base_data_directory: base_cache_directory
);
}
}
private static WebKit.WebContext? default_context = null;
private static WebKit.UserStyleSheet? user_stylesheet = null;
private static WebKit.UserScript? script = null;
/**
* Initialises WebKit.WebContext for use by the client.
*/
public static void init_web_context(Application.Configuration config,
File web_extension_dir,
File cache_dir) {
WebsiteDataManager data_manager = new WebsiteDataManager(cache_dir.get_path());
WebKit.WebContext context = new WebKit.WebContext.with_website_data_manager(data_manager);
// Use the doc viewer model since each web view instance only
// ever shows a single HTML document.
context.set_cache_model(WebKit.CacheModel.DOCUMENT_VIEWER);
context.register_uri_scheme("cid", (req) => {
WebView? view = req.get_web_view() as WebView;
if (view != null) {
view.handle_cid_request(req);
}
});
context.register_uri_scheme("geary", (req) => {
WebView? view = req.get_web_view() as WebView;
if (view != null) {
view.handle_internal_request(req);
}
});
context.initialize_web_extensions.connect((context) => {
context.set_web_extensions_directory(
web_extension_dir.get_path()
);
context.set_web_extensions_initialization_user_data(
new Variant.boolean(config.enable_debug)
);
});
update_spellcheck(context, config);
config.settings.changed[
Application.Configuration.SPELL_CHECK_LANGUAGES
].connect(() => {
update_spellcheck(context, config);
});
WebView.default_context = context;
}
/**
* Loads static resources used by WebView.
*/
public static void load_resources(GLib.File user_dir)
throws GLib.Error {
WebView.script = load_app_script(
"components-web-view.js"
);
foreach (string name in new string[] { USER_CSS, USER_CSS_LEGACY }) {
GLib.File stylesheet = user_dir.get_child(name);
try {
WebView.user_stylesheet = load_user_stylesheet(stylesheet);
break;
} catch (GLib.IOError.NOT_FOUND err) {
// All good, try the next one or just exit
} catch (GLib.FileError.NOENT err) {
// Ditto
} catch (GLib.Error err) {
warning(
"Could not load %s: %s", stylesheet.get_path(), err.message
);
}
}
}
/** Loads an application-specific WebKit stylesheet. */
protected static WebKit.UserStyleSheet load_app_stylesheet(string name)
throws GLib.Error {
return new WebKit.UserStyleSheet(
GioUtil.read_resource(name),
WebKit.UserContentInjectedFrames.TOP_FRAME,
WebKit.UserStyleLevel.USER,
null,
null
);
}
/** Loads a user stylesheet from disk. */
protected static WebKit.UserStyleSheet? load_user_stylesheet(GLib.File name)
throws GLib.Error {
Geary.Memory.FileBuffer buf = new Geary.Memory.FileBuffer(name, true);
return new WebKit.UserStyleSheet(
buf.get_valid_utf8(),
WebKit.UserContentInjectedFrames.ALL_FRAMES,
WebKit.UserStyleLevel.USER,
null,
null
);
}
/** Loads an application-specific WebKit JavaScript script. */
protected static WebKit.UserScript load_app_script(string name)
throws Error {
return new WebKit.UserScript(
GioUtil.read_resource(name),
WebKit.UserContentInjectedFrames.TOP_FRAME,
WebKit.UserScriptInjectionTime.START,
null,
null
);
}
private static inline void update_spellcheck(WebKit.WebContext context,
Application.Configuration config) {
string[] langs = config.get_spell_check_languages();
context.set_spell_checking_enabled(langs.length > 0);
context.set_spell_checking_languages(langs);
}
private static inline uint to_wk2_font_size(Pango.FontDescription font) {
Gdk.Screen? screen = Gdk.Screen.get_default();
double dpi = screen != null ? screen.get_resolution() : 96.0;
double size = font.get_size();
if (!font.get_size_is_absolute()) {
size = size / Pango.SCALE;
}
return (uint) (size * dpi / 72.0);
}
/**
* Delegate for message handler callbacks.
*
* @see register_message_callback
*/
protected delegate void MessageCallback(GLib.Variant? parameters);
// Work around for not being able to put delegates in a Gee collection.
private class MessageCallable {
public unowned MessageCallback handler;
public MessageCallable(MessageCallback handler) {
this.handler = handler;
}
}
/**
* Determines if the view's content has been fully loaded.
*
* This property is updated immediately before the {@link
* content_loaded} signal is fired, and is triggered by the
* PageState JavaScript object completing its load
* handler. I.e. This will be true after the in-page JavaScript has
* finished making any modifications to the page content.
*
* This will likely be fired after WebKitGTK sets the `is-loading`
* property to `FALSE` and emits `load-changed` with
* `WebKitLoadEvent.LOAD_FINISHED`, since they are related to
* network resource loading, not page content.
*/
public bool is_content_loaded { get; private set; default = false; }
/** Determines if the view has any selected text */
public bool has_selection { get; private set; default = false; }
/** The HTML content's current preferred height in window pixels. */
public int preferred_height {
get {
return (int) GLib.Math.round(
this.webkit_reported_height * this.zoom_level
);
}
}
public string document_font {
get {
return _document_font;
}
set {
_document_font = value;
Pango.FontDescription font = Pango.FontDescription.from_string(value);
WebKit.Settings settings = get_settings();
settings.default_font_family = font.get_family();
settings.default_font_size = to_wk2_font_size(font);
set_settings(settings);
}
}
private string _document_font;
public string monospace_font {
get {
return _monospace_font;
}
set {
_monospace_font = value;
Pango.FontDescription font = Pango.FontDescription.from_string(value);
WebKit.Settings settings = get_settings();
settings.monospace_font_family = font.get_family();
settings.default_monospace_font_size = to_wk2_font_size(font);
set_settings(settings);
}
}
private string _monospace_font;
private weak string? body = null;
private Gee.Map<string,Geary.Memory.Buffer> internal_resources =
new Gee.HashMap<string,Geary.Memory.Buffer>();
private Gee.Map<string,MessageCallable> message_handlers =
new Gee.HashMap<string,MessageCallable>();
private double webkit_reported_height = 0;
/**
* Emitted when the view's content has finished loaded.
*
* See {@link is_content_loaded} for detail about when this is
* emitted.
*/
public signal void content_loaded();
/** Emitted when the web view's undo/redo stack state changes. */
public signal void command_stack_changed(bool can_undo, bool can_redo);
/** Emitted when the web view's content has changed. */
public signal void document_modified();
/** Emitted when the view's selection has changed. */
public signal void selection_changed(bool has_selection);
/** Emitted when a user clicks a link in the view. */
public signal void link_activated(string uri);
/** Emitted when the view has loaded a resource added to it. */
public signal void internal_resource_loaded(string name);
/** Emitted when a remote image load was disallowed. */
public signal void remote_image_load_blocked();
protected WebView(Application.Configuration config,
WebKit.UserContentManager? custom_manager = null,
WebView? related = null) {
WebKit.Settings setts = new WebKit.Settings();
setts.allow_modal_dialogs = false;
setts.default_charset = "UTF-8";
setts.enable_developer_extras = config.enable_inspector;
setts.enable_fullscreen = false;
setts.enable_html5_database = false;
setts.enable_html5_local_storage = false;
setts.enable_java = false;
setts.enable_javascript = true;
setts.enable_javascript_markup = false;
setts.enable_media_stream = false;
setts.enable_offline_web_application_cache = false;
setts.enable_page_cache = false;
#if WEBKIT_PLUGINS_SUPPORTED
setts.enable_plugins = false;
#endif
setts.hardware_acceleration_policy =
WebKit.HardwareAccelerationPolicy.NEVER;
setts.javascript_can_access_clipboard = true;
WebKit.UserContentManager content_manager =
custom_manager ?? new WebKit.UserContentManager();
content_manager.add_script(WebView.script);
if (WebView.user_stylesheet != null) {
content_manager.add_style_sheet(WebView.user_stylesheet);
}
Object(
settings: setts,
user_content_manager: content_manager,
web_context: WebView.default_context
);
base_ref();
init(config);
}
/**
* Constructs a new web view with a new shared WebProcess.
*
* The new view will use the same WebProcess, settings and content
* manager as the given related view's.
*
* @see WebKit.WebView.WebView.with_related_view
*/
protected WebView.with_related_view(Application.Configuration config,
WebView related) {
Object(
related_view: related,
settings: related.get_settings(),
user_content_manager: related.user_content_manager
);
base_ref();
init(config);
}
~WebView() {
base_unref();
}
public override void destroy() {
this.message_handlers.clear();
base.destroy();
}
/**
* Loads a message HTML body into the view.
*/
public new void load_html(string? body, string? base_uri=null) {
this.body = body;
base.load_html(body, base_uri ?? INTERNAL_URL_BODY);
}
/**
* Returns the view's content as an HTML string.
*/
public async string? get_html() throws Error {
return yield call_returning<string?>(Util.JS.callable("getHtml"), null);
}
/**
* Adds an resource that may be accessed from the view via a URL.
*
* Internal resources may be access via both the internal `geary`
* scheme (for resources such as an image inserted via the
* composer) or via the `cid` scheme (for standard HTML email IMG
* elements).
*/
public void add_internal_resource(string id, Geary.Memory.Buffer buf) {
this.internal_resources[id] = buf;
}
/**
* Adds a set of internal resources to the view.
*
* @see add_internal_resource
*/
public void add_internal_resources(Gee.Map<string,Geary.Memory.Buffer> res) {
this.internal_resources.set_all(res);
}
/**
* Allows loading any remote images found during page load.
*
* This must be called before HTML content is loaded to have any
* effect.
*/
public void allow_remote_image_loading() {
this.run_javascript.begin("_gearyAllowRemoteResourceLoads = true", null);
}
/**
* Load any remote images previously that were blocked.
*/
public void load_remote_images() {
this.call_void.begin(Util.JS.callable("loadRemoteImages"), null);
}
/**
* Selects all content in the web view.
*/
public void select_all() {
execute_editing_command(WebKit.EDITING_COMMAND_SELECT_ALL);
}
/**
* Copies selected content and sends it to the clipboard.
*/
public void copy_clipboard() {
execute_editing_command(WebKit.EDITING_COMMAND_COPY);
}
public void zoom_reset() {
this.zoom_level = ZOOM_DEFAULT;
// Notify the preferred height has changed since it depends on
// the zoom level. Same for zoom in and out below.
notify_property("preferred-height");
}
public void zoom_in() {
double new_zoom = this.zoom_level += (this.zoom_level * ZOOM_FACTOR);
if (new_zoom > ZOOM_MAX) {
new_zoom = ZOOM_MAX;
}
this.zoom_level = new_zoom;
notify_property("preferred-height");
}
public void zoom_out() {
double new_zoom = this.zoom_level -= (this.zoom_level * ZOOM_FACTOR);
if (new_zoom < ZOOM_MIN) {
new_zoom = ZOOM_MIN;
}
this.zoom_level = new_zoom;
notify_property("preferred-height");
}
public new async void set_editable(bool enabled,
Cancellable? cancellable)
throws Error {
yield call_void(
Util.JS.callable("setEditable").bool(enabled), cancellable
);
}
/**
* Invokes a {@link Util.JS.Callable} on this web view.
*
* This calls the given callable on the `geary` object for the
* current view, any returned value are ignored.
*/
protected async void call_void(Util.JS.Callable target,
GLib.Cancellable? cancellable)
throws GLib.Error {
yield call_impl(target, cancellable);
}
/**
* Invokes a {@link Util.JS.Callable} on this web view.
*
* This calls the given callable on the `geary` object for the
* current view. The value returned by the call is returned by
* this method.
*
* The type parameter `T` must match the type returned by the
* call, else an error is thrown. Only simple nullable value types
* are supported for T, for more complex return types (arrays,
* dictionaries, etc) specify {@link GLib.Variant} for `T` and
* manually parse that.
*/
protected async T call_returning<T>(Util.JS.Callable target,
GLib.Cancellable? cancellable)
throws GLib.Error {
WebKit.UserMessage? response = yield call_impl(target, cancellable);
if (response == null) {
throw new Util.JS.Error.TYPE(
"Method call %s did not return a value", target.to_string()
);
}
GLib.Variant? param = response.parameters;
T ret_value = null;
var ret_type = typeof(T);
if (ret_type == typeof(GLib.Variant)) {
ret_value = param;
} else {
if (param != null && param.get_type().is_maybe()) {
param = param.get_maybe();
}
if (param != null) {
// Since these replies are coming from JS via
// Util.JS.value_to_variant, they will only be one of
// string, double, bool, array or dict
var param_type = param.classify();
if (ret_type == typeof(string) && param_type == STRING) {
ret_value = param.get_string();
} else if (ret_type == typeof(bool) && param_type == BOOLEAN) {
ret_value = (bool?) param.get_boolean();
} else if (ret_type == typeof(int) && param_type == DOUBLE) {
ret_value = (int?) ((int) param.get_double());
} else if (ret_type == typeof(short) && param_type == DOUBLE) {
ret_value = (short?) ((short) param.get_double());
} else if (ret_type == typeof(char) && param_type == DOUBLE) {
ret_value = (char?) ((char) param.get_double());
} else if (ret_type == typeof(long) && param_type == DOUBLE) {
ret_value = (long?) ((long) param.get_double());
} else if (ret_type == typeof(int64) && param_type == DOUBLE) {
ret_value = (int64?) ((int64) param.get_double());
} else if (ret_type == typeof(uint) && param_type == DOUBLE) {
ret_value = (uint?) ((uint) param.get_double());
} else if (ret_type == typeof(uchar) && param_type == DOUBLE) {
ret_value = (uchar?) ((uchar) param.get_double());
} else if (ret_type == typeof(ushort) && param_type == DOUBLE) {
ret_value = (ushort?) ((ushort) param.get_double());
} else if (ret_type == typeof(ulong) && param_type == DOUBLE) {
ret_value = (ulong?) ((ulong) param.get_double());
} else if (ret_type == typeof(uint64) && param_type == DOUBLE) {
ret_value = (uint64?) ((uint64) param.get_double());
} else if (ret_type == typeof(double) && param_type == DOUBLE) {
ret_value = (double?) param.get_double();
} else if (ret_type == typeof(float) && param_type == DOUBLE) {
ret_value = (float?) ((float) param.get_double());
} else {
throw new Util.JS.Error.TYPE(
"%s is not a supported type for %s",
ret_type.name(), param_type.to_string()
);
}
}
}
return ret_value;
}
/**
* Registers a callback for a specific WebKit user message.
*/
protected void register_message_callback(string name,
MessageCallback handler) {
this.message_handlers.set(name, new MessageCallable(handler));
}
private void init(Application.Configuration config) {
// XXX get the allow prefix from the extension somehow
this.decide_policy.connect(on_decide_policy);
this.web_process_terminated.connect((reason) => {
warning("Web process crashed: %s", reason.to_string());
});
register_message_callback(
COMMAND_STACK_CHANGED, on_command_stack_changed
);
register_message_callback(
CONTENT_LOADED, on_content_loaded
);
register_message_callback(
DOCUMENT_MODIFIED, on_document_modified
);
register_message_callback(
PREFERRED_HEIGHT_CHANGED, on_preferred_height_changed
);
register_message_callback(
REMOTE_IMAGE_LOAD_BLOCKED, on_remote_image_load_blocked
);
register_message_callback(
SELECTION_CHANGED, on_selection_changed
);
this.user_message_received.connect(this.on_message_received);
// Manage zoom level, ensure it's sane
config.bind(Application.Configuration.CONVERSATION_VIEWER_ZOOM_KEY, this, "zoom_level");
if (this.zoom_level < ZOOM_MIN) {
this.zoom_level = ZOOM_MIN;
} else if (this.zoom_level > ZOOM_MAX) {
this.zoom_level = ZOOM_MAX;
}
this.scroll_event.connect(on_scroll_event);
// Watch desktop font settings
Settings system_settings = config.gnome_interface;
system_settings.bind("document-font-name", this,
"document-font", SettingsBindFlags.DEFAULT);
system_settings.bind("monospace-font-name", this,
"monospace-font", SettingsBindFlags.DEFAULT);
}
private async WebKit.UserMessage? call_impl(Util.JS.Callable target,
GLib.Cancellable? cancellable)
throws GLib.Error {
WebKit.UserMessage? response = yield send_message_to_page(
target.to_message(), cancellable
);
if (response != null) {
var response_name = response.name;
if (response_name == MESSAGE_EXCEPTION_NAME) {
var exception = new GLib.VariantDict(response.parameters);
var name = exception.lookup_value("name", GLib.VariantType.STRING) as string;
var message = exception.lookup_value("message", GLib.VariantType.STRING) as string;
var backtrace = exception.lookup_value("backtrace_string", GLib.VariantType.STRING) as string;
var source = exception.lookup_value("source_uri", GLib.VariantType.STRING) as string;
var line = exception.lookup_value("line_number", GLib.VariantType.UINT32);
var column = exception.lookup_value("column_number", GLib.VariantType.UINT32);
var log_message = "Method call %s raised %s exception at %s:%d:%d: %s".printf(
target.to_string(),
name ?? "unknown",
source ?? "unknown",
(line != null ? (int) line.get_uint32() : -1),
(column != null ? (int) column.get_uint32() : -1),
message ?? "unknown"
);
debug(log_message);
if (backtrace != null) {
debug(backtrace);
}
throw new Util.JS.Error.EXCEPTION(log_message);
} else if (response_name != MESSAGE_RETURN_VALUE_NAME) {
throw new Util.JS.Error.TYPE(
"Method call %s returned unknown name: %s",
target.to_string(),
response_name
);
}
}
return response;
}
private void handle_cid_request(WebKit.URISchemeRequest request) {
if (!handle_internal_response(request)) {
request.finish_error(new FileError.NOENT("Unknown CID"));
}
}
private void handle_internal_request(WebKit.URISchemeRequest request) {
if (request.get_uri() == INTERNAL_URL_BODY) {
Geary.Memory.Buffer buf = new Geary.Memory.StringBuffer(this.body);
request.finish(buf.get_input_stream(), buf.size, null);
} else if (!handle_internal_response(request)) {
request.finish_error(new FileError.NOENT("Unknown internal URL"));
}
}
private bool handle_internal_response(WebKit.URISchemeRequest request) {
string name = soup_uri_decode(request.get_path());
Geary.Memory.Buffer? buf = this.internal_resources[name];
bool handled = false;
if (buf != null) {
request.finish(buf.get_input_stream(), buf.size, null);
internal_resource_loaded(name);
handled = true;
}
return handled;
}
// This method is called only when determining if something should
// be loaded for display in the web view as the primary
// resource. It is not used to determine if sub-resources such as
// images or JS will be loaded. So we only allow geary:body loads,
// and notify but ignore if the user attempts to click on a link,
// and deny everything else.
private bool on_decide_policy(WebKit.WebView view,
WebKit.PolicyDecision policy,
WebKit.PolicyDecisionType type) {
if (type == WebKit.PolicyDecisionType.NAVIGATION_ACTION ||
type == WebKit.PolicyDecisionType.NEW_WINDOW_ACTION) {
WebKit.NavigationPolicyDecision nav_policy =
(WebKit.NavigationPolicyDecision) policy;
WebKit.NavigationAction nav_action =
nav_policy.get_navigation_action();
switch (nav_action.get_navigation_type()) {
case WebKit.NavigationType.OTHER:
if (nav_action.get_request().uri == INTERNAL_URL_BODY) {
policy.use();
} else {
policy.ignore();
}
break;
case WebKit.NavigationType.LINK_CLICKED:
// Let the app know a user activated a link, but don't
// try to load it ourselves.
// We need to call ignore() before emitting the signal
// to unblock the WebKit WebProcess, otherwise the
// call chain for mailto links will cause the
// WebProcess to deadlock, and the resulting composer
// will be useless. See Geary Bug 771504
// <https://bugzilla.gnome.org/show_bug.cgi?id=771504>
// and WebKitGTK Bug 182528
// <https://bugs.webkit.org/show_bug.cgi?id=182528>
policy.ignore();
link_activated(nav_action.get_request().uri);
break;
default:
policy.ignore();
break;
}
} else {
policy.ignore();
}
return Gdk.EVENT_STOP;
}
private bool on_scroll_event(Gdk.EventScroll event) {
if ((event.state & Gdk.ModifierType.CONTROL_MASK) != 0) {
double dir = 0;
if (event.direction == Gdk.ScrollDirection.UP)
dir = -1;
else if (event.direction == Gdk.ScrollDirection.DOWN)
dir = 1;
else if (event.direction == Gdk.ScrollDirection.SMOOTH)
dir = event.delta_y;
if (dir < 0) {
zoom_in();
return true;
} else if (dir > 0) {
zoom_out();
return true;
}
}
return false;
}
private void on_preferred_height_changed(GLib.Variant? parameters) {
double height = this.webkit_reported_height;
if (parameters != null && parameters.classify() == DOUBLE) {
height = parameters.get_double();
} else {
warning("Could not get JS preferred height");
}
if (this.webkit_reported_height != height) {
this.webkit_reported_height = height;
notify_property("preferred-height");
}
}
private void on_command_stack_changed(GLib.Variant? parameters) {
if (parameters != null &&
parameters.is_container() &&
parameters.n_children() == 2) {
GLib.Variant can_undo = parameters.get_child_value(0);
GLib.Variant can_redo = parameters.get_child_value(1);
command_stack_changed(
can_undo.classify() == BOOLEAN && can_undo.get_boolean(),
can_redo.classify() == BOOLEAN && can_redo.get_boolean()
);
} else {
warning("Could not get JS command stack state");
}
}
private void on_document_modified(GLib.Variant? parameters) {
document_modified();
}
private void on_remote_image_load_blocked(GLib.Variant? parameters) {
remote_image_load_blocked();
}
private void on_content_loaded(GLib.Variant? parameters) {
this.is_content_loaded = true;
content_loaded();
}
private void on_selection_changed(GLib.Variant? parameters) {
if (parameters != null && parameters.classify() == BOOLEAN) {
selection_changed(parameters.get_boolean());
} else {
warning("Could not get JS selection value");
}
}
private bool on_message_received(WebKit.UserMessage message) {
if (message.name == MESSAGE_EXCEPTION_NAME) {
var detail = new GLib.VariantDict(message.parameters);
var name = detail.lookup_value("name", GLib.VariantType.STRING) as string;
var log_message = detail.lookup_value("message", GLib.VariantType.STRING) as string;
warning(
"Error sending message from JS: %s: %s",
name ?? "unknown",
log_message ?? "unknown"
);
} else if (this.message_handlers.has_key(message.name)) {
debug(
"Message received: %s(%s)",
message.name,
message.parameters != null ? message.parameters.print(true) : ""
);
MessageCallable callback = this.message_handlers.get(message.name);
callback.handler(message.parameters);
} else {
warning("Message with unknown handler received: %s", message.name);
}
return true;
}
}
// XXX this needs to be moved into the libsoup bindings
extern string soup_uri_decode(string part);