ltk_webkit/wpe/
scripting.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
// SPDX-License-Identifier: LGPL-2.1-only
// Copyright (C) 2026 Liberux Labs, S. L. <info@liberux.net>

//! Page scripting: whether JavaScript runs, and evaluating a script in the page.

use std::ffi::{ c_char, c_uint, c_void, CString };
use std::ptr;

use gio::ffi::GAsyncResult;
use glib_sys::{ g_error_free, g_free, gpointer, GError };
use gobject_sys::{ g_object_unref, GObject };
use wpe_webkit_sys::*;

use super::browsing::owned;
use super::WebView;

#[ repr( C ) ]
pub( super ) struct JSCValue
{
	_private: [ u8; 0 ],
}

// Not in the generated bindings: the result is a JSCValue, a type they do not cover.
extern "C"
{
	fn webkit_web_view_evaluate_javascript_finish( web_view: *mut WebKitWebView, result: *mut GAsyncResult, error: *mut *mut GError ) -> *mut c_void;
	pub( super ) fn webkit_web_view_call_async_javascript_function_finish( web_view: *mut WebKitWebView, result: *mut GAsyncResult, error: *mut *mut GError ) -> *mut JSCValue;
	fn jsc_value_to_json( value: *mut c_void, indent: c_uint ) -> *mut c_char;
}

type ScriptDone = Box<dyn FnOnce( Result<String, String> )>;

const CONSOLE_ENV: &str = "LTK_WEBKIT_CONSOLE";

/// With `LTK_WEBKIT_CONSOLE=1` the page's console messages go to stdout, to debug a page in a host that has no developer tools.
pub( crate ) unsafe fn console_to_stdout_if_asked( webview: *mut WebKitWebView )
{
	if std::env::var_os( CONSOLE_ENV ).as_deref() != Some( std::ffi::OsStr::new( "1" ) ) { return; }
	// SAFETY: the caller passes a live view; its settings object lives as long as it does.
	unsafe
	{
		let settings = webkit_web_view_get_settings( webview );
		if !settings.is_null() { webkit_settings_set_enable_write_console_messages_to_stdout( settings, 1 ); }
	}
}

impl WebView
{
	pub fn set_javascript_enabled( &self, enabled: bool )
	{
		let Some( webview ) = self.webview_ptr() else { return };
		// SAFETY: the settings object belongs to the live view.
		unsafe
		{
			let settings = webkit_web_view_get_settings( webview );
			if !settings.is_null() { webkit_settings_set_enable_javascript( settings, enabled as i32 ); }
		}
	}

	pub fn javascript_enabled( &self ) -> bool
	{
		// SAFETY: the settings object belongs to the live view.
		self.webview_ptr().is_some_and( |w| unsafe
		{
			let settings = webkit_web_view_get_settings( w );
			!settings.is_null() && webkit_settings_get_enable_javascript( settings ) != 0
		} )
	}

	/// Run `script` in the page's main world. `done` gets the result as JSON (`null` for `undefined`) or the error message, from a later [`WebView::tick`].
	pub fn evaluate_javascript( &self, script: &str, done: impl FnOnce( Result<String, String> ) + 'static )
	{
		let Some( webview ) = self.webview_ptr() else { return };
		let Ok( script_c ) = CString::new( script ) else { return };
		let data = Box::into_raw( Box::new( Box::new( done ) as ScriptDone ) ) as gpointer;
		// SAFETY: WebKit copies the script; `on_evaluated` runs exactly once and reclaims `data`.
		unsafe { webkit_web_view_evaluate_javascript( webview, script_c.as_ptr(), -1, ptr::null(), ptr::null(), ptr::null_mut(), Some( on_evaluated ), data ); }
	}
}

/// Run `body` as the body of an async function in the isolated world `world`, which shares the page's DOM but not its globals; `done` gets what its promise settles to, as JSON.
pub( crate ) fn call_async_in_world( webview: *mut WebKitWebView, body: &str, world: &'static [ u8 ], done: impl FnOnce( Result<String, String> ) + 'static )
{
	let Ok( body_c ) = CString::new( body ) else { return done( Err( "script contains NUL".to_string() ) ) };
	let data = Box::into_raw( Box::new( Box::new( done ) as ScriptDone ) ) as gpointer;
	// SAFETY: the caller passes a live view; WebKit copies the strings; `on_called` runs exactly once and reclaims `data`.
	unsafe
	{
		webkit_web_view_call_async_javascript_function( webview, body_c.as_ptr(), -1, ptr::null_mut(), world.as_ptr() as *const c_char, ptr::null(), ptr::null_mut(), Some( on_called ), data );
	}
}

unsafe extern "C" fn on_evaluated( source: *mut GObject, result: *mut GAsyncResult, user_data: gpointer )
{
	// SAFETY: produced by `Box::into_raw` in `evaluate_javascript`, reclaimed once here.
	let done = unsafe { Box::from_raw( user_data as *mut ScriptDone ) };
	let mut err: *mut GError = ptr::null_mut();
	// SAFETY: `source` is the view the evaluation was started on.
	let value = unsafe { webkit_web_view_evaluate_javascript_finish( source as *mut WebKitWebView, result, &mut err ) };
	( *done )( unsafe { json_of( value, err ) } );
}

unsafe extern "C" fn on_called( source: *mut GObject, result: *mut GAsyncResult, user_data: gpointer )
{
	// SAFETY: produced by `Box::into_raw` in `call_async_in_world`, reclaimed once here.
	let done = unsafe { Box::from_raw( user_data as *mut ScriptDone ) };
	let mut err: *mut GError = ptr::null_mut();
	// SAFETY: `source` is the view the call was started on.
	let value = unsafe { webkit_web_view_call_async_javascript_function_finish( source as *mut WebKitWebView, result, &mut err ) } as *mut c_void;
	( *done )( unsafe { json_of( value, err ) } );
}

/// Consumes both: the value is a full reference and the JSON string is ours to free.
unsafe fn json_of( value: *mut c_void, err: *mut GError ) -> Result<String, String>
{
	// SAFETY: as the callers obtained them from a `_finish` call.
	unsafe
	{
		if value.is_null()
		{
			let message = super::error_message_or_default( err, "unknown" );
			if !err.is_null() { g_error_free( err ); }
			return Err( message );
		}
		let json = jsc_value_to_json( value, 0 );
		let text = if json.is_null() { "null".to_string() } else { owned( json ) };
		if !json.is_null() { g_free( json as gpointer ); }
		g_object_unref( value as *mut GObject );
		Ok( text )
	}
}