Skip to main content

aidoku/imports/
js.rs

1//! Module for running JavaScript and managing web views.
2use super::{
3	FFIResult, Rid,
4	net::Request,
5	std::{destroy, read_string_and_destroy},
6};
7use crate::{
8	AidokuError,
9	alloc::{String, Vec},
10	imports::std::read,
11};
12
13#[link(wasm_import_module = "js")]
14unsafe extern "C" {
15	fn context_create() -> Rid;
16	fn context_eval(context: Rid, string_ptr: *const u8, len: usize) -> FFIResult;
17	fn context_eval_async(context: Rid, string_ptr: *const u8, len: usize) -> FFIResult;
18	fn context_get(context: Rid, string_ptr: *const u8, len: usize) -> FFIResult;
19
20	fn webview_create() -> Rid;
21	fn webview_set_rule_list(webview: Rid, string_ptr: *const u8, len: usize) -> FFIResult;
22	fn webview_load(webview: Rid, request: Rid) -> FFIResult;
23	fn webview_load_html(
24		webview: Rid,
25		string_ptr: *const u8,
26		len: usize,
27		url_ptr: *const u8,
28		url_len: usize,
29	) -> FFIResult;
30	fn webview_wait_for_load(webview: Rid) -> FFIResult;
31	fn webview_eval(webview: Rid, string_ptr: *const u8, len: usize) -> FFIResult;
32	fn webview_eval_async(webview: Rid, string_ptr: *const u8, len: usize) -> FFIResult;
33	fn webview_add_user_script(
34		webview: Rid,
35		string_ptr: *const u8,
36		len: usize,
37		at_document_end: bool,
38		for_main_frame_only: bool,
39	) -> FFIResult;
40	fn webview_get_cookies(webview: Rid) -> FFIResult;
41	fn webview_delete_cookie(
42		webview: Rid,
43		name_ptr: *const u8,
44		name_len: usize,
45		value_ptr: *const u8,
46		value_len: usize,
47		domain_ptr: *const u8,
48		domain_len: usize,
49	) -> FFIResult;
50}
51
52/// Error type for JavaScript operations.
53#[derive(PartialEq, Eq, Debug, Clone)]
54pub enum JsError {
55	MissingResult,
56	InvalidContext,
57	InvalidString,
58	InvalidHandler,
59	InvalidRequest,
60	InvalidRuleList,
61	FailedEncoding,
62	MissingCookie,
63}
64
65impl JsError {
66	fn from(value: FFIResult) -> Option<Self> {
67		match value {
68			-1 => Some(Self::MissingResult),
69			-2 => Some(Self::InvalidContext),
70			-3 => Some(Self::InvalidString),
71			-4 => Some(Self::InvalidHandler),
72			-5 => Some(Self::InvalidRequest),
73			-6 => Some(Self::InvalidRuleList),
74			-7 => Some(Self::FailedEncoding),
75			-8 => Some(Self::MissingCookie),
76			_ => None,
77		}
78	}
79}
80
81/// A context for evaluating JavaScript code.
82pub struct JsContext {
83	rid: Rid,
84}
85
86impl JsContext {
87	/// Creates a new JavaScript context.
88	pub fn new() -> Self {
89		let rid = unsafe { context_create() };
90		Self { rid }
91	}
92
93	/// Evaluates JavaScript code in the context.
94	pub fn eval(&self, js: &str) -> Result<String, JsError> {
95		let js_bytes = js.as_bytes();
96		let result = unsafe { context_eval(self.rid, js_bytes.as_ptr(), js_bytes.len()) };
97		if let Some(error) = JsError::from(result) {
98			Err(error)
99		} else {
100			Ok(read_string_and_destroy(result).unwrap_or_default())
101		}
102	}
103
104	/// Evaluates asynchronous JavaScript code in the context.
105	pub fn eval_async(&self, js: &str) -> Result<String, JsError> {
106		let js_bytes = js.as_bytes();
107		let result = unsafe { context_eval_async(self.rid, js_bytes.as_ptr(), js_bytes.len()) };
108		if let Some(error) = JsError::from(result) {
109			Err(error)
110		} else {
111			Ok(read_string_and_destroy(result).unwrap_or_default())
112		}
113	}
114
115	/// Retrieves the value of a JavaScript variable in the context.
116	pub fn get(&self, variable: &str) -> Result<String, JsError> {
117		let var_bytes = variable.as_bytes();
118		let result = unsafe { context_get(self.rid, var_bytes.as_ptr(), var_bytes.len()) };
119		if let Some(error) = JsError::from(result) {
120			Err(error)
121		} else {
122			Ok(read_string_and_destroy(result).unwrap_or_default())
123		}
124	}
125}
126
127impl Default for JsContext {
128	fn default() -> Self {
129		Self::new()
130	}
131}
132
133impl Drop for JsContext {
134	fn drop(&mut self) {
135		unsafe { destroy(self.rid) }
136	}
137}
138
139/// A web view that can be used to load web content.
140///
141/// This web view won't be displayed to the user. It is intended for use in the background.
142pub struct WebView {
143	rid: Rid,
144}
145
146impl WebView {
147	/// Creates a new web view.
148	pub fn new() -> Self {
149		let rid = unsafe { webview_create() };
150		Self { rid }
151	}
152
153	/// Sets a content rule list for the web view.
154	///
155	/// For information on formatting the rule list json, see Apple's documentation on
156	/// [Creating a content blocker](https://developer.apple.com/documentation/SafariServices/creating-a-content-blocker).
157	pub fn set_rule_list(&self, json: &str) -> Result<(), JsError> {
158		let json_bytes = json.as_bytes();
159		let result =
160			unsafe { webview_set_rule_list(self.rid, json_bytes.as_ptr(), json_bytes.len()) };
161		if let Some(error) = JsError::from(result) {
162			Err(error)
163		} else {
164			Ok(())
165		}
166	}
167
168	/// Loads a web page in the web view.
169	pub fn load(&self, request: Request) -> Result<(), JsError> {
170		let request_descriptor = request.rid;
171		let result = unsafe { webview_load(self.rid, request_descriptor) };
172		if let Some(error) = JsError::from(result) {
173			Err(error)
174		} else {
175			Ok(())
176		}
177	}
178
179	/// Loads a web page in the web view, blocking until the page is loaded.
180	pub fn load_blocking(&self, request: Request) -> Result<(), JsError> {
181		self.load(request)?;
182		self.wait_for_load();
183		Ok(())
184	}
185
186	/// Loads the given HTML content in the web view.
187	pub fn load_html(&self, html: &str, base_url: Option<&str>) -> Result<(), JsError> {
188		let html_bytes = html.as_bytes();
189		let url_bytes = base_url.map(|s| s.as_bytes()).unwrap_or_default();
190		let result = unsafe {
191			webview_load_html(
192				self.rid,
193				html_bytes.as_ptr(),
194				html_bytes.len(),
195				url_bytes.as_ptr(),
196				url_bytes.len(),
197			)
198		};
199		if let Some(error) = JsError::from(result) {
200			Err(error)
201		} else {
202			Ok(())
203		}
204	}
205
206	/// Loads HTML content in the web view, blocking until the content is loaded.
207	pub fn load_html_blocking(&self, html: &str, base_url: Option<&str>) -> Result<(), JsError> {
208		self.load_html(html, base_url)?;
209		self.wait_for_load();
210		Ok(())
211	}
212
213	/// Blocks the current thread until the web view is loaded.
214	pub fn wait_for_load(&self) {
215		unsafe { webview_wait_for_load(self.rid) };
216	}
217
218	/// Evaluates JavaScript code in the web view, blocking until the result is available.
219	pub fn eval(&self, js: &str) -> Result<String, JsError> {
220		let js_bytes = js.as_bytes();
221		let result = unsafe { webview_eval(self.rid, js_bytes.as_ptr(), js_bytes.len()) };
222		if let Some(error) = JsError::from(result) {
223			Err(error)
224		} else {
225			Ok(read_string_and_destroy(result).unwrap_or_default())
226		}
227	}
228
229	/// Evaluates asynchronous JavaScript code in the web view, blocking until the result is available.
230	pub fn eval_async(&self, js: &str) -> Result<String, JsError> {
231		let js_bytes = js.as_bytes();
232		let result = unsafe { webview_eval_async(self.rid, js_bytes.as_ptr(), js_bytes.len()) };
233		if let Some(error) = JsError::from(result) {
234			Err(error)
235		} else {
236			Ok(read_string_and_destroy(result).unwrap_or_default())
237		}
238	}
239
240	/// Adds a user script to the web view.
241	pub fn add_user_script(&self, script: WebViewUserScript) -> Result<(), JsError> {
242		let source_bytes = script.source.as_bytes();
243		let result = unsafe {
244			webview_add_user_script(
245				self.rid,
246				source_bytes.as_ptr(),
247				source_bytes.len(),
248				script.at_document_end,
249				script.for_main_frame_only,
250			)
251		};
252		if let Some(error) = JsError::from(result) {
253			Err(error)
254		} else {
255			Ok(())
256		}
257	}
258
259	/// Returns all cookies in the web view cookie store.
260	pub fn get_cookies(&self) -> Result<Vec<Cookie>, AidokuError> {
261		let result = unsafe { webview_get_cookies(self.rid) };
262		if let Some(error) = JsError::from(result) {
263			Err(error.into())
264		} else {
265			read(result)
266		}
267	}
268
269	/// Deletes a cookie from the web view cookie store.
270	pub fn delete_cookie(&self, cookie: Cookie) -> Result<(), JsError> {
271		let result = unsafe {
272			webview_delete_cookie(
273				self.rid,
274				cookie.name.as_ptr(),
275				cookie.name.len(),
276				cookie.value.as_ptr(),
277				cookie.value.len(),
278				cookie.domain.as_ptr(),
279				cookie.domain.len(),
280			)
281		};
282		if let Some(error) = JsError::from(result) {
283			Err(error)
284		} else {
285			Ok(())
286		}
287	}
288
289	/// Deletes all cookies in the web view cookie store.
290	pub fn delete_all_cookies(&self) -> Result<(), JsError> {
291		let result = unsafe {
292			webview_delete_cookie(
293				self.rid,
294				core::ptr::null(),
295				-1i32 as usize,
296				core::ptr::null(),
297				0,
298				core::ptr::null(),
299				0,
300			)
301		};
302		if let Some(error) = JsError::from(result) {
303			Err(error)
304		} else {
305			Ok(())
306		}
307	}
308}
309
310impl Default for WebView {
311	fn default() -> Self {
312		Self::new()
313	}
314}
315
316impl Drop for WebView {
317	fn drop(&mut self) {
318		unsafe { destroy(self.rid) }
319	}
320}
321
322#[derive(Default)]
323/// An object that represents a script that can be injected into webpages
324pub struct WebViewUserScript {
325	/// The script source.
326	pub source: String,
327	/// Whether script should be injected at the end of a document or the start.
328	pub at_document_end: bool,
329	/// Whether the script should be injected into all frames or just the main frame.
330	pub for_main_frame_only: bool,
331}
332
333impl WebViewUserScript {
334	/// Creates a new user script.
335	pub fn new(source: String) -> Self {
336		Self {
337			source,
338			..Default::default()
339		}
340	}
341}
342
343#[derive(Debug, serde::Deserialize)]
344pub struct Cookie {
345	pub name: String,
346	pub value: String,
347	pub expires_date: Option<i64>,
348	pub domain: String,
349	pub path: String,
350	pub is_secure: bool,
351	pub is_http_only: bool,
352}