use gdk::{
gio,
glib::{self, FileError},
};
use headless_webview::{
http::{
Request as HttpRequest, RequestBuilder as HttpRequestBuilder, Response as HttpResponse,
},
webview::web_context::{WebContextData, WebContextImpl},
Error, Result,
};
use std::{
collections::{HashSet, VecDeque},
rc::Rc,
sync::{
atomic::{AtomicBool, Ordering::SeqCst},
Mutex,
},
};
use url::Url;
use webkit2gtk::{
traits::*, ApplicationInfo, CookiePersistentStorage, LoadEvent, UserContentManager, WebContext,
WebContextBuilder, WebView, WebsiteDataManagerBuilder,
};
#[derive(Debug)]
pub struct GdkWebContext {
context: WebContext,
manager: UserContentManager,
webview_uri_loader: Rc<WebviewUriLoader>,
registered_protocols: HashSet<String>,
automation: bool,
app_info: Option<ApplicationInfo>,
}
impl WebContextImpl for GdkWebContext {
fn new(data: &WebContextData) -> Self {
use webkit2gtk::traits::*;
let mut context_builder = WebContextBuilder::new();
if let Some(data_directory) = data.data_directory() {
let data_manager = WebsiteDataManagerBuilder::new()
.local_storage_directory(
&data_directory
.join("localstorage")
.to_string_lossy()
.into_owned(),
)
.indexeddb_directory(
&data_directory
.join("databases")
.join("indexeddb")
.to_string_lossy()
.into_owned(),
)
.build();
if let Some(cookie_manager) = data_manager.cookie_manager() {
cookie_manager.set_persistent_storage(
&data_directory
.join("cookies")
.to_string_lossy()
.into_owned(),
CookiePersistentStorage::Text,
);
}
context_builder = context_builder.website_data_manager(&data_manager);
}
let context = context_builder.build();
let automation = false;
context.set_automation_allowed(automation);
let app_info = ApplicationInfo::new();
app_info.set_name(env!("CARGO_PKG_NAME"));
app_info.set_version(
env!("CARGO_PKG_VERSION_MAJOR")
.parse()
.expect("invalid wry version major"),
env!("CARGO_PKG_VERSION_MINOR")
.parse()
.expect("invalid wry version minor"),
env!("CARGO_PKG_VERSION_PATCH")
.parse()
.expect("invalid wry version patch"),
);
Self {
context,
automation,
manager: UserContentManager::new(),
registered_protocols: Default::default(),
webview_uri_loader: Rc::default(),
app_info: Some(app_info),
}
}
fn set_allows_automation(&mut self, flag: bool) {
use webkit2gtk::traits::*;
self.automation = flag;
self.context.set_automation_allowed(flag);
}
}
impl GdkWebContext {
pub(crate) fn context(&self) -> &WebContext {
&self.context
}
pub(crate) fn manager(&self) -> &UserContentManager {
&self.manager
}
pub(crate) fn register_uri_scheme<F>(&mut self, name: &str, handler: F) -> Result<()>
where
F: Fn(&HttpRequest) -> Result<HttpResponse> + 'static,
{
actually_register_uri_scheme(self, name, handler)?;
if self.registered_protocols.insert(name.to_string()) {
Ok(())
} else {
Err(Error::DuplicateCustomProtocol(name.to_string()))
}
}
pub fn try_register_uri_scheme<F>(&mut self, name: &str, handler: F) -> Result<()>
where
F: Fn(&HttpRequest) -> Result<HttpResponse> + 'static,
{
if self.registered_protocols.insert(name.to_string()) {
actually_register_uri_scheme(self, name, handler)
} else {
Err(Error::DuplicateCustomProtocol(name.to_string()))
}
}
pub(crate) fn queue_load_uri(&self, webview: Rc<WebView>, url: Url) {
self.webview_uri_loader.push(webview, url)
}
pub(crate) fn flush_queue_loader(&self) {
Rc::clone(&self.webview_uri_loader).flush()
}
pub(crate) fn allows_automation(&self) -> bool {
self.automation
}
pub(crate) fn register_automation(&mut self, webview: WebView) {
use webkit2gtk::traits::*;
if let (true, Some(app_info)) = (self.automation, self.app_info.take()) {
self.context.connect_automation_started(move |_, auto| {
let webview = webview.clone();
auto.set_application_info(&app_info);
auto.connect_create_web_view(None, move |_| webview.clone());
});
}
}
}
fn actually_register_uri_scheme<F>(
context: &mut GdkWebContext,
name: &str,
handler: F,
) -> Result<()>
where
F: Fn(&HttpRequest) -> Result<HttpResponse> + 'static,
{
use webkit2gtk::traits::*;
let context = &context.context;
context
.security_manager()
.ok_or(Error::MissingManager)?
.register_uri_scheme_as_secure(name);
context.register_uri_scheme(name, move |request| {
if let Some(uri) = request.uri() {
let uri = uri.as_str();
let http_request = HttpRequestBuilder::new()
.uri(uri)
.method("GET")
.body(Vec::new())
.unwrap();
match handler(&http_request) {
Ok(http_response) => {
let buffer = http_response.body();
let input = gio::MemoryInputStream::from_bytes(&glib::Bytes::from(buffer));
request.finish(&input, buffer.len() as i64, http_response.mimetype())
}
Err(_) => request.finish_error(&mut glib::Error::new(
FileError::Exist,
"Could not get requested file.",
)),
}
} else {
request.finish_error(&mut glib::Error::new(
FileError::Exist,
"Could not get uri.",
));
}
});
Ok(())
}
#[derive(Debug, Default)]
struct WebviewUriLoader {
lock: AtomicBool,
queue: Mutex<VecDeque<(Rc<WebView>, Url)>>,
}
impl WebviewUriLoader {
fn is_locked(&self) -> bool {
self.lock.swap(true, SeqCst)
}
fn unlock(&self) {
self.lock.store(false, SeqCst)
}
fn push(&self, webview: Rc<WebView>, url: Url) {
let mut queue = self.queue.lock().expect("poisoned load queue");
queue.push_back((webview, url))
}
fn pop(&self) -> Option<(Rc<WebView>, Url)> {
let mut queue = self.queue.lock().expect("poisoned load queue");
queue.pop_front()
}
fn flush(self: Rc<Self>) {
if !self.is_locked() {
if let Some((webview, url)) = self.pop() {
webview.connect_load_changed(move |_, event| {
if let LoadEvent::Finished = event {
self.unlock();
Rc::clone(&self).flush();
};
});
webview.load_uri(url.as_str());
} else {
self.unlock();
}
}
}
}