feat(webview): Add custom protocol (scheme) support

This is a basic implementation of the custom protocol, with built-in mimetype extraction from the content, backed by URI detection.

To make it clear, I added an example and can be run with:

```
cargo run --example webview_custom_protocol --features webview
```
This commit is contained in:
David Lemarier 2021-04-25 12:07:45 -04:00
parent 813f452deb
commit c9d665963a
No known key found for this signature in database
GPG key ID: 414D7F0DBBB895CA
7 changed files with 239 additions and 3 deletions

View file

@ -26,6 +26,7 @@ objc_id = "0.1.1"
os_info = "3.0.1" os_info = "3.0.1"
uuid = { version = "0.8", features = ["v4"], optional = true } uuid = { version = "0.8", features = ["v4"], optional = true }
url = "2.1.1" url = "2.1.1"
infer = "0.4"
[dev-dependencies] [dev-dependencies]
eval = "0.4" eval = "0.4"

View file

@ -0,0 +1,104 @@
//! This example showcases setting up a basic application and window, setting up some views to
//! work with autolayout, and some basic ways to handle colors.
use cacao::webview::{WebView, WebViewConfig, WebViewDelegate};
use cacao::macos::{App, AppDelegate};
use cacao::macos::menu::{Menu, MenuItem};
use cacao::macos::toolbar::Toolbar;
use cacao::macos::window::{Window, WindowConfig, WindowDelegate, WindowToolbarStyle};
struct BasicApp {
window: Window<AppWindow>
}
impl AppDelegate for BasicApp {
fn did_finish_launching(&self) {
App::activate();
self.window.show();
}
}
#[derive(Default)]
pub struct WebViewInstance;
impl WebViewDelegate for WebViewInstance {
fn on_custom_protocol_request(&self, path: &str) -> Option<Vec<u8>> {
let requested_asset_path = path.replace("cacao://", "");
let index_html = r#"
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
</head>
<body>
<h1>Welcome 🍫</h1>
<a href="/hello.html">Link</a>
</body>
</html>"#;
let link_html = r#"
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
</head>
<body>
<h1>Hello!</h1>
<a href="/index.html">Back home</a>
</body>
</html>"#;
return match requested_asset_path.as_str() {
"/hello.html" => Some(link_html.as_bytes().into()),
_ => Some(index_html.as_bytes().into()),
}
}
}
struct AppWindow {
content: WebView<WebViewInstance>
}
impl AppWindow {
pub fn new() -> Self {
let mut webview_config = WebViewConfig::default();
// register the protocol in the webview
webview_config.add_custom_protocol("cacao");
AppWindow {
content: WebView::with(webview_config, WebViewInstance::default())
}
}
pub fn load_url(&self, url: &str) {
self.content.load_url(url);
}
}
impl WindowDelegate for AppWindow {
const NAME: &'static str = "WindowDelegate";
fn did_load(&mut self, window: Window) {
window.set_title("Browser Example");
window.set_autosave_name("CacaoBrowserExample");
window.set_minimum_content_size(400., 400.);
window.set_content_view(&self.content);
// load custom protocol
self.load_url("cacao://");
}
}
fn main() {
App::new("com.test.window", BasicApp {
window: Window::with(WindowConfig::default(), AppWindow::new())
}).run();
}

View file

@ -4,6 +4,7 @@
use std::ffi::c_void; use std::ffi::c_void;
use std::sync::Once; use std::sync::Once;
use std::ptr::null;
use block::Block; use block::Block;
@ -12,7 +13,7 @@ use objc::runtime::{Class, Object, Sel};
use objc::{class, msg_send, sel, sel_impl}; use objc::{class, msg_send, sel, sel_impl};
use crate::foundation::{id, nil, YES, NO, NSString, NSArray, NSInteger}; use crate::foundation::{id, nil, YES, NO, NSString, NSArray, NSInteger};
use crate::webview::{WEBVIEW_DELEGATE_PTR, WebViewDelegate}; use crate::webview::{WEBVIEW_DELEGATE_PTR, WebViewDelegate, mimetype::MimeType};
use crate::webview::actions::{NavigationAction, NavigationResponse};//, OpenPanelParameters}; use crate::webview::actions::{NavigationAction, NavigationResponse};//, OpenPanelParameters};
//use crate::webview::enums::{NavigationPolicy, NavigationResponsePolicy}; //use crate::webview::enums::{NavigationPolicy, NavigationResponsePolicy};
use crate::utils::load; use crate::utils::load;
@ -53,6 +54,39 @@ extern fn on_message<T: WebViewDelegate>(this: &Object, _: Sel, _: id, script_me
} }
} }
/// Fires when a custom protocol URI is requested from the underlying `WKWebView`.
extern fn start_url_scheme_task<T: WebViewDelegate>(this: &Object, _: Sel, _webview: id, task: id) {
let delegate = load::<T>(this, WEBVIEW_DELEGATE_PTR);
unsafe {
let request: id = msg_send![task, request];
let url: id = msg_send![request, URL];
let uri = NSString::from_retained(msg_send![url, absoluteString]);
let uri_str = uri.to_str();
if let Some(content) = delegate.on_custom_protocol_request(uri_str) {
let mime = MimeType::parse(&content, uri_str);
let nsurlresponse: id = msg_send![class!(NSURLResponse), alloc];
let response: id = msg_send![nsurlresponse, initWithURL:url MIMEType:NSString::new(&mime)
expectedContentLength:content.len() textEncodingName:null::<c_void>()];
let _: () = msg_send![task, didReceiveResponse: response];
// Send data
let bytes = content.as_ptr() as *mut c_void;
let data: id = msg_send![class!(NSData), alloc];
let data: id = msg_send![data, initWithBytes:bytes length:content.len()];
let _: () = msg_send![task, didReceiveData: data];
// Finish
let () = msg_send![task, didFinish];
}
}
}
/// Fires when a custom protocol completed the task from the underlying `WKWebView`.
extern fn stop_url_scheme_task<T: WebViewDelegate>(_: &Object, _: Sel, _webview: id, _task: id) {}
/// Fires when deciding a navigation policy - i.e, should something be allowed or not. /// Fires when deciding a navigation policy - i.e, should something be allowed or not.
extern fn decide_policy_for_action<T: WebViewDelegate>(this: &Object, _: Sel, _: id, action: id, handler: usize) { extern fn decide_policy_for_action<T: WebViewDelegate>(this: &Object, _: Sel, _: id, action: id, handler: usize) {
let delegate = load::<T>(this, WEBVIEW_DELEGATE_PTR); let delegate = load::<T>(this, WEBVIEW_DELEGATE_PTR);
@ -159,6 +193,10 @@ pub fn register_webview_delegate_class<T: WebViewDelegate>() -> *const Class {
// WKScriptMessageHandler // WKScriptMessageHandler
decl.add_method(sel!(userContentController:didReceiveScriptMessage:), on_message::<T> as extern fn(&Object, _, _, id)); decl.add_method(sel!(userContentController:didReceiveScriptMessage:), on_message::<T> as extern fn(&Object, _, _, id));
// Custom protocol handler
decl.add_method(sel!(webView:startURLSchemeTask:), start_url_scheme_task::<T> as extern fn(&Object, Sel, id, id));
decl.add_method(sel!(webView:stopURLSchemeTask:), stop_url_scheme_task::<T> as extern fn(&Object, Sel, id, id));
// WKUIDelegate // WKUIDelegate
decl.add_method(sel!(webView:runJavaScriptAlertPanelWithMessage:initiatedByFrame:completionHandler:), alert::<T> as extern fn(&Object, _, _, id, _, _)); decl.add_method(sel!(webView:runJavaScriptAlertPanelWithMessage:initiatedByFrame:completionHandler:), alert::<T> as extern fn(&Object, _, _, id, _, _));
decl.add_method(sel!(webView:runOpenPanelWithParameters:initiatedByFrame:completionHandler:), run_open_panel::<T> as extern fn(&Object, _, _, id, _, usize)); decl.add_method(sel!(webView:runOpenPanelWithParameters:initiatedByFrame:completionHandler:), run_open_panel::<T> as extern fn(&Object, _, _, id, _, usize));

View file

@ -13,7 +13,8 @@ use crate::webview::enums::InjectAt;
#[derive(Debug)] #[derive(Debug)]
pub struct WebViewConfig { pub struct WebViewConfig {
pub objc: Id<Object>, pub objc: Id<Object>,
pub handlers: Vec<String> pub handlers: Vec<String>,
pub protocols: Vec<String>
} }
impl Default for WebViewConfig { impl Default for WebViewConfig {
@ -26,7 +27,8 @@ impl Default for WebViewConfig {
WebViewConfig { WebViewConfig {
objc: config, objc: config,
handlers: vec![] handlers: vec![],
protocols: vec![]
} }
} }
} }
@ -55,6 +57,12 @@ impl WebViewConfig {
} }
} }
/// Register the given protocol to the underlying `WKWebView`.
/// Example; protocol_name: `demo` will allow request to `demo://`
pub fn add_custom_protocol(&mut self, protocol_name: &str) {
self.protocols.push(protocol_name.to_string());
}
/// Enables access to the underlying inspector view for `WKWebView`. /// Enables access to the underlying inspector view for `WKWebView`.
pub fn enable_developer_extras(&mut self) { pub fn enable_developer_extras(&mut self) {
let key = NSString::new("developerExtrasEnabled"); let key = NSString::new("developerExtrasEnabled");

73
src/webview/mimetype.rs Normal file
View file

@ -0,0 +1,73 @@
use std::fmt;
const MIMETYPE_PLAIN: &str = "text/plain";
/// [Web Compatible MimeTypes](https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types#important_mime_types_for_web_developers)
pub enum MimeType {
CSS,
CSV,
HTML,
ICO,
JS,
JSON,
JSONLD,
OCTETSTREAM,
RTF,
SVG,
}
impl std::fmt::Display for MimeType {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let mime = match self {
MimeType::CSS => "text/css",
MimeType::CSV => "text/csv",
MimeType::HTML => "text/html",
MimeType::ICO => "image/vnd.microsoft.icon",
MimeType::JS => "text/javascript",
MimeType::JSON => "application/json",
MimeType::JSONLD => "application/ld+json",
MimeType::OCTETSTREAM => "application/octet-stream",
MimeType::RTF => "application/rtf",
MimeType::SVG => "image/svg+xml",
};
write!(f, "{}", mime)
}
}
impl MimeType {
/// parse a URI suffix to convert text/plain mimeType to their actual web compatible mimeType.
pub fn parse_from_uri(uri: &str) -> MimeType {
let suffix = uri.split('.').last();
match suffix {
Some("bin") => Self::OCTETSTREAM,
Some("css") => Self::CSS,
Some("csv") => Self::CSV,
Some("html") => Self::HTML,
Some("ico") => Self::ICO,
Some("js") => Self::JS,
Some("json") => Self::JSON,
Some("jsonld") => Self::JSONLD,
Some("rtf") => Self::RTF,
Some("svg") => Self::SVG,
// Assume HTML when a TLD is found for eg. `protocol:://example.com`
Some(_) => Self::HTML,
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Common_types
// using octet stream according to this:
None => Self::OCTETSTREAM,
}
}
/// infer mimetype from content (or) URI if needed.
pub fn parse(content: &[u8], uri: &str) -> String {
let mime = match infer::get(&content) {
Some(info) => info.mime_type(),
None => MIMETYPE_PLAIN,
};
if mime == MIMETYPE_PLAIN {
return Self::parse_from_uri(uri).to_string();
}
mime.to_string()
}
}

View file

@ -38,6 +38,7 @@ pub(crate) mod class;
use class::{register_webview_class, register_webview_delegate_class}; use class::{register_webview_class, register_webview_delegate_class};
//pub(crate) mod process_pool; //pub(crate) mod process_pool;
mod mimetype;
mod traits; mod traits;
pub use traits::WebViewDelegate; pub use traits::WebViewDelegate;
@ -50,6 +51,7 @@ fn allocate_webview(
unsafe { unsafe {
// Not a fan of this, but we own it anyway, so... meh. // Not a fan of this, but we own it anyway, so... meh.
let handlers = std::mem::take(&mut config.handlers); let handlers = std::mem::take(&mut config.handlers);
let protocols = std::mem::take(&mut config.protocols);
let configuration = config.into_inner(); let configuration = config.into_inner();
if let Some(delegate) = &objc_delegate { if let Some(delegate) = &objc_delegate {
@ -66,6 +68,11 @@ fn allocate_webview(
let name = NSString::new(&handler); let name = NSString::new(&handler);
let _: () = msg_send![content_controller, addScriptMessageHandler:*delegate name:&*name]; let _: () = msg_send![content_controller, addScriptMessageHandler:*delegate name:&*name];
} }
for protocol in protocols {
let name = NSString::new(&protocol);
let _: () = msg_send![configuration, setURLSchemeHandler:*delegate forURLScheme:&*name];
}
} }
let zero: CGRect = Rect::zero().into(); let zero: CGRect = Rect::zero().into();

View file

@ -33,6 +33,11 @@ pub trait WebViewDelegate {
/// Note that at the moment, you really should handle bridging JSON/stringification yourself. /// Note that at the moment, you really should handle bridging JSON/stringification yourself.
fn on_message(&self, _name: &str, _body: &str) {} fn on_message(&self, _name: &str, _body: &str) {}
/// Called when a custom protocol URI is requested.
fn on_custom_protocol_request(&self, _uri: &str) -> Option<Vec<u8>> {
None
}
/// Given a callback handler, you can decide what policy should be taken for a given browser /// Given a callback handler, you can decide what policy should be taken for a given browser
/// action. By default, this is `NavigationPolicy::Allow`. /// action. By default, this is `NavigationPolicy::Allow`.
fn policy_for_navigation_action<F: Fn(NavigationPolicy)>(&self, _action: NavigationAction, handler: F) { fn policy_for_navigation_action<F: Fn(NavigationPolicy)>(&self, _action: NavigationAction, handler: F) {