tauri-apps/tauri

[feat] Expose AddBrowserExtension for Windows

Revxrsal opened this issue · 2 comments

Describe the problem

I can see in #10909 that it is possible now to enable browser extensions on Windows. Unfortunately, we can hardly take advantage of this feature as you currently don't have any way to actually add extensions :P

Describe the solution you'd like

Add a function that invokes ICoreWebviewProfile7#AddBrowserExtension.

A possible approach is to add an extensions function in WebviewWindowBuilder, as shown below:

let window = tauri::WebviewWindowBuilder::new(...)
    .title(...)
    .browser_extensions_enabled(true)
    .extensions(
        /* directory, possibly a Path or PathBuf */, 
        /* list of extension IDs, maybe a Vec<String> */
    );

Since this is only possible on Windows, it could either:

  • Be put behind a feature flag
  • Be callable on all platforms, but only affect Windows

The second approach is likely better, as it is consistent with Tauri's overall design and ensures forward compatibility in case other platforms allow extensions in the future.

Alternatives considered

Modifying the underlying Wry window. Looks like this is not possible.

Additional context

No response

You can use with_webview for now https://docs.rs/tauri/latest/tauri/webview/struct.WebviewWindow.html#method.with_webview

AreBrowserExtensionsEnabled got an actual method because it needs to be set when the inner webview is created.

I've spent some decent three hours trying to get this right 😂 Had to learn a bit about the Windows API and how it's used in Rust. I also got sidetracked by the whole COM thing.

Anyway, here's the final code, in case anybody needs it:

[target.'cfg(target_os = "windows")'.dependencies]
windows-core = "0.58.0"
webview2-com-sys = "0.33.0"

use std::path::Path;
use tauri::webview::PlatformWebview;

/// Represents an Error that occurs when adding a browser extension
#[derive(Debug)]
pub enum Error {
    Unsupported,
    #[cfg(target_os = "windows")]
    WindowsError(windows_core::Error),
}

/// Defines extensions for [PlatformWebview] to easily add extensions
/// to web-views
pub trait BrowserExtensionsExt {
    fn add_extension<P: AsRef<Path>>(&self, extension_folder: P) -> Result<(), Error>;
}

impl BrowserExtensionsExt for PlatformWebview {
    #[cfg(not(target_os = "windows"))]
    fn add_extension<P: AsRef<Path>>(&self, extension_folder: P) -> Result<(), Error> {
        Err(Error::Unsupported)
    }

    #[cfg(target_os = "windows")]
    fn add_extension<P: AsRef<Path>>(&self, extension_folder: P) -> Result<(), Error> {
        windows_webview::add_extension(self, extension_folder.as_ref()).map_err(|e| Error::WindowsError(e))
    }
}

#[cfg(target_os = "windows")]
mod windows_webview {
    use std::ffi::OsStr;
    use std::os::windows::ffi::OsStrExt;
    use std::path::Path;
    use tauri::webview::PlatformWebview;
    use webview2_com_sys::Microsoft::Web::WebView2::Win32::ICoreWebView2BrowserExtension;
    use webview2_com_sys::Microsoft::Web::WebView2::Win32::ICoreWebView2ProfileAddBrowserExtensionCompletedHandler;
    use webview2_com_sys::Microsoft::Web::WebView2::Win32::ICoreWebView2ProfileAddBrowserExtensionCompletedHandler_Impl;
    use webview2_com_sys::Microsoft::Web::WebView2::Win32::{ICoreWebView2Profile7, ICoreWebView2_13};
    use windows_core::{Interface, Result, PCWSTR};
    use windows_core::{implement, HRESULT};

    #[implement(ICoreWebView2ProfileAddBrowserExtensionCompletedHandler)]
    pub struct AddBrowserExtensionCallback;

    impl ICoreWebView2ProfileAddBrowserExtensionCompletedHandler_Impl for AddBrowserExtensionCallback_Impl {
        #[allow(non_snake_case)]
        fn Invoke(&self, error_code: HRESULT, _extension: Option<&ICoreWebView2BrowserExtension>) -> windows_core::Result<()> {
            error_code.ok()
        }
    }

    impl AddBrowserExtensionCallback {
        pub fn new() -> Self {
            Self
        }
    }

    pub(crate) fn add_extension(webview: &PlatformWebview, path: &Path) -> Result<()> {
        unsafe {
            let web_view2 = webview.controller().CoreWebView2()?;
            let profile = web_view2.cast::<ICoreWebView2_13>()?.Profile()?;
            let profile: ICoreWebView2Profile7 = profile.cast()?;
            add_extension_to_profile(profile, path.as_os_str())?;
        }
        Ok(())
    }

    fn add_extension_to_profile(profile: ICoreWebView2Profile7, extension_folder: &OsStr) -> Result<()> {
        unsafe {
            let str: Vec<u16> = extension_folder.encode_wide()
                .chain(std::iter::once(0))
                .collect();
            let path_ptr = PCWSTR(str.as_ptr());

            let handler = AddBrowserExtensionCallback::new();
            let h = ICoreWebView2ProfileAddBrowserExtensionCompletedHandler::from(handler);
            profile.AddBrowserExtension(path_ptr, Some(&h))?;
        }
        Ok(())
    }
}

Which can be used as follows:

    let window = tauri::WebviewWindowBuilder::new(...)
        .title(website)
        .browser_extensions_enabled(true)
        .build().unwrap();

    window.with_webview(|webview| {
        let r = webview.add_extension(r#"<extension path here>"#);
        if let Err(why) = r {
            println!("Failed to add extension: {why:?}");
        }
    }).unwrap();

(Excuse my unwrap() use)