From c9d665963a745073fa7b22e42382c30700d2089a Mon Sep 17 00:00:00 2001 From: David Lemarier Date: Sun, 25 Apr 2021 12:07:45 -0400 Subject: [PATCH 1/2] 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 ``` --- Cargo.toml | 1 + examples/webview_custom_protocol.rs | 104 ++++++++++++++++++++++++++++ src/webview/class.rs | 40 ++++++++++- src/webview/config.rs | 12 +++- src/webview/mimetype.rs | 73 +++++++++++++++++++ src/webview/mod.rs | 7 ++ src/webview/traits.rs | 5 ++ 7 files changed, 239 insertions(+), 3 deletions(-) create mode 100644 examples/webview_custom_protocol.rs create mode 100644 src/webview/mimetype.rs diff --git a/Cargo.toml b/Cargo.toml index ee369d0..6e6144e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,6 +26,7 @@ objc_id = "0.1.1" os_info = "3.0.1" uuid = { version = "0.8", features = ["v4"], optional = true } url = "2.1.1" +infer = "0.4" [dev-dependencies] eval = "0.4" diff --git a/examples/webview_custom_protocol.rs b/examples/webview_custom_protocol.rs new file mode 100644 index 0000000..277404b --- /dev/null +++ b/examples/webview_custom_protocol.rs @@ -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 +} + +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> { + let requested_asset_path = path.replace("cacao://", ""); + + let index_html = r#" + + + + + + + + +

Welcome 🍫

+ Link + + "#; + + let link_html = r#" + + + + + + + + +

Hello!

+ Back home + + "#; + + 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 +} + +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(); +} diff --git a/src/webview/class.rs b/src/webview/class.rs index 2a3be29..df67e2e 100644 --- a/src/webview/class.rs +++ b/src/webview/class.rs @@ -4,6 +4,7 @@ use std::ffi::c_void; use std::sync::Once; +use std::ptr::null; use block::Block; @@ -12,7 +13,7 @@ use objc::runtime::{Class, Object, Sel}; use objc::{class, msg_send, sel, sel_impl}; 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::enums::{NavigationPolicy, NavigationResponsePolicy}; use crate::utils::load; @@ -53,6 +54,39 @@ extern fn on_message(this: &Object, _: Sel, _: id, script_me } } +/// Fires when a custom protocol URI is requested from the underlying `WKWebView`. +extern fn start_url_scheme_task(this: &Object, _: Sel, _webview: id, task: id) { + let delegate = load::(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::()]; + 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(_: &Object, _: Sel, _webview: id, _task: id) {} + /// Fires when deciding a navigation policy - i.e, should something be allowed or not. extern fn decide_policy_for_action(this: &Object, _: Sel, _: id, action: id, handler: usize) { let delegate = load::(this, WEBVIEW_DELEGATE_PTR); @@ -159,6 +193,10 @@ pub fn register_webview_delegate_class() -> *const Class { // WKScriptMessageHandler decl.add_method(sel!(userContentController:didReceiveScriptMessage:), on_message:: as extern fn(&Object, _, _, id)); + // Custom protocol handler + decl.add_method(sel!(webView:startURLSchemeTask:), start_url_scheme_task:: as extern fn(&Object, Sel, id, id)); + decl.add_method(sel!(webView:stopURLSchemeTask:), stop_url_scheme_task:: as extern fn(&Object, Sel, id, id)); + // WKUIDelegate decl.add_method(sel!(webView:runJavaScriptAlertPanelWithMessage:initiatedByFrame:completionHandler:), alert:: as extern fn(&Object, _, _, id, _, _)); decl.add_method(sel!(webView:runOpenPanelWithParameters:initiatedByFrame:completionHandler:), run_open_panel:: as extern fn(&Object, _, _, id, _, usize)); diff --git a/src/webview/config.rs b/src/webview/config.rs index 19dfdc6..a9043bb 100644 --- a/src/webview/config.rs +++ b/src/webview/config.rs @@ -13,7 +13,8 @@ use crate::webview::enums::InjectAt; #[derive(Debug)] pub struct WebViewConfig { pub objc: Id, - pub handlers: Vec + pub handlers: Vec, + pub protocols: Vec } impl Default for WebViewConfig { @@ -26,7 +27,8 @@ impl Default for WebViewConfig { WebViewConfig { 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`. pub fn enable_developer_extras(&mut self) { let key = NSString::new("developerExtrasEnabled"); diff --git a/src/webview/mimetype.rs b/src/webview/mimetype.rs new file mode 100644 index 0000000..b42d1b5 --- /dev/null +++ b/src/webview/mimetype.rs @@ -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() + } +} \ No newline at end of file diff --git a/src/webview/mod.rs b/src/webview/mod.rs index 45e5e3c..c79b27b 100644 --- a/src/webview/mod.rs +++ b/src/webview/mod.rs @@ -38,6 +38,7 @@ pub(crate) mod class; use class::{register_webview_class, register_webview_delegate_class}; //pub(crate) mod process_pool; +mod mimetype; mod traits; pub use traits::WebViewDelegate; @@ -50,6 +51,7 @@ fn allocate_webview( unsafe { // Not a fan of this, but we own it anyway, so... meh. let handlers = std::mem::take(&mut config.handlers); + let protocols = std::mem::take(&mut config.protocols); let configuration = config.into_inner(); if let Some(delegate) = &objc_delegate { @@ -66,6 +68,11 @@ fn allocate_webview( let name = NSString::new(&handler); 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(); diff --git a/src/webview/traits.rs b/src/webview/traits.rs index 2944706..048cf19 100644 --- a/src/webview/traits.rs +++ b/src/webview/traits.rs @@ -33,6 +33,11 @@ pub trait WebViewDelegate { /// Note that at the moment, you really should handle bridging JSON/stringification yourself. 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> { + None + } + /// Given a callback handler, you can decide what policy should be taken for a given browser /// action. By default, this is `NavigationPolicy::Allow`. fn policy_for_navigation_action(&self, _action: NavigationAction, handler: F) { From 7827dd6490d99d82180422f6ba86e1481ae32e03 Mon Sep 17 00:00:00 2001 From: David Lemarier Date: Sun, 25 Apr 2021 19:39:27 -0400 Subject: [PATCH 2/2] Make `infer` optional --- Cargo.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 6e6144e..f501cac 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,7 +26,7 @@ objc_id = "0.1.1" os_info = "3.0.1" uuid = { version = "0.8", features = ["v4"], optional = true } url = "2.1.1" -infer = "0.4" +infer = { version = "0.4", optional = true } [dev-dependencies] eval = "0.4" @@ -37,7 +37,7 @@ cloudkit = [] color_fallbacks = [] quicklook = [] user-notifications = ["uuid"] -webview = [] +webview = ["infer"] webview-downloading-macos = [] [package.metadata.bundle.example.ios-beta]