Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

How to inject custom javascript to new windows requested by window.open? #2491

Closed
ztanczos opened this issue Jun 1, 2022 · 86 comments
Closed
Assignees
Labels
bug Something isn't working tracked We are tracking this work internally.

Comments

@ztanczos
Copy link

ztanczos commented Jun 1, 2022

Hi,

I'm trying to ensure that custom JavaScript is always injected to new windows which are requested by e.g.: window.open. For this I'm subscribing to NewWindowRequested event handler the following way:

        public async Task InitializeAsync()
        {
            await webView.EnsureCoreWebView2Async();
            webView.CoreWebView2.NewWindowRequested += CoreWebView2_NewWindowRequested;
        }

        private async void CoreWebView2_NewWindowRequested(object sender, Microsoft.Web.WebView2.Core.CoreWebView2NewWindowRequestedEventArgs e)
        {
            var deferral = e.GetDeferral();
            Window window = new Window();
            var newWebView = new WebView2();
            window.Content = newWebView;
            window.Show();
            await newWebView.EnsureCoreWebView2Async();
            await newWebView.CoreWebView2.AddScriptToExecuteOnDocumentCreatedAsync("alert('hello')");

            e.NewWindow = newWebView.CoreWebView2;
            e.Handled = true;
            deferral.Complete();
        }

What I'm experiencing is that the new window pops up fine but the custom script is never executed.

How can I ensure that custom JavaScript is always executed when a new window is requested by window.open before the window object is returned to the JavaScript caller?

Thank you,
Zoltan

AB#43367417

@vbryh-msft
Copy link
Contributor

Hi @ztanczos , in order for JS injection to take effect in a new window, it should be called after e.NewWindow = newWebView.CoreWebView2;. Here is some doc.

@psmulovics
Copy link

psmulovics commented Jun 1, 2022

Maybe it's worth revising the doc, as it currently says:

Changes to settings should be made before put_NewWindow is called to ensure that those settings take effect for the newly setup WebView.

Which would imply that the setting for AddScriptToExecuteOnDocumentCreatedAsync should be made before you set NewWindow.

@vbryh-msft
Copy link
Contributor

@psmulovics, sorry for confusion. We have further The methods which should affect the new web contents like AddScriptToExecuteOnDocumentCreated and add_WebResourceRequested have to be called after setting NewWindow. - we will see how to make it more clear.

@psmulovics
Copy link

Not related to the documentation, rather to make it more foolproof - could it give a warning in debug if you set it at the wrong time?

@pontusn
Copy link

pontusn commented Jun 6, 2022

Most interesting information, this enabled me to remove an ugly workaround where our integration actually had to reload ininitially loaded content for window.open to make all hooks and injected scripts work.

@ztanczos
Copy link
Author

ztanczos commented Jun 8, 2022

Hi @vbryh-msft,
Thank you for your answer but unfortunately it still doesn't work for me, at least not reliably. This is the full source code for the NewWindowRequested event handler:

        private async void CoreWebView2_NewWindowRequested(object sender, Microsoft.Web.WebView2.Core.CoreWebView2NewWindowRequestedEventArgs e)
        {
            try
            {
                var deferral = e.GetDeferral();
                Window window = new Window();
                var newWebView = new WebView2();

                window.Content = newWebView;
                window.Show();

                await newWebView.EnsureCoreWebView2Async();
                e.NewWindow = newWebView.CoreWebView2;
                
                await newWebView.CoreWebView2.AddScriptToExecuteOnDocumentCreatedAsync("alert('hello')");
                e.Handled = true;
                deferral.Complete();
            }
            catch (Exception ex)
            {

            }
        }

and this is the code which triggers the new window request:

<button onclick="window.open('https://www.google.com')">click me</button>

With this event handler most of the time the popup window is just blank (i.e.: not even google.com is displayed), but sometimes - mostly when I put a breakpoint at AddScriptToExecuteOnDocumentCreatedAsync I see the page properly displayed and my 'hello' alert message as well.

Also if I put a Thread.Sleep(3000) before AddScriptToExecuteOnDocumentCreatedAsync it starts to work more often.

Am I doing something wrong or there's a synchronization issue in WV2?

@psmulovics
Copy link

tagging @liminzhu for tracking

@pontusn
Copy link

pontusn commented Jun 8, 2022

For our Win32 integration I avoid extensive processing in the event handler. Instead I get the deferral and use PostMessage to create window and integrate with new WebView2 instance from the message pump.

@ztanczos
Copy link
Author

Hi @pontusn,
I'm not sure how would that be possible with the C# .NET interface: my understanding is that I'd need to set eventArgs.NewWindow to the new CoreWebView2 instance in the event handler and creating that instance can only be done asynchronously and it is already a relatively heavy operation.

Hi @vbryh-msft,
Just wondering if you had a chance to check my last message.

@pontusn
Copy link

pontusn commented Jun 16, 2022

Use the deferral and avoid lengthly processing in the event handler.

@vbryh-msft vbryh-msft added bug Something isn't working tracked We are tracking this work internally. labels Jun 16, 2022
@vbryh-msft
Copy link
Contributor

Hi @ztanczos - I was able to repro the issue in .Net, but was unable to quickly solve it - created a bug for somebody to have more time to look into it.

@vbryh-msft
Copy link
Contributor

@ztanczos - I have tried your sample one more time - do you need to have alert in AddScriptToExecuteOnDocumentCreatedAsync? Could you try console.log instead and see how it behaves for you?

@ztanczos
Copy link
Author

@vbryh-msft - I don't need alert, let me try to explain what I'm looking for: we have custom Javascript libraries which provide all sorts of functionalities for the hosted web applications. These libraries should be already present when loading a page. This works fine for the 'main' window but if a web application tries to open a new window by using window.open we'd like to intercept that call and inject the same set of JS libs before the window object is available to the caller.

Consider this sample HTML:

<html>
<head>
<title>test</title>
<script type="text/javascript">
	var openedWindow;
	
	console.log(window['TESTDATA'])
	
	function openWindow() {
	  openedWindow = window.open('https://www.google.com')
	  console.log('window opened')
	  console.log(openedWindow['TESTDATA'])
	}
</script>
</head>
<body>
<h1>hello world</h1>
<button onclick="openWindow()">click me</button>
</body>
</html>

And in .NET I'm trying to inject the following JS:

        public async Task InitializeAsync()
        {
            await webView.EnsureCoreWebView2Async();
            webView.CoreWebView2.NewWindowRequested += CoreWebView2_NewWindowRequested;
            await webView.CoreWebView2.AddScriptToExecuteOnDocumentCreatedAsync("window['TESTDATA'] = 'this works'");
        }
        private async void CoreWebView2_NewWindowRequested(object sender, Microsoft.Web.WebView2.Core.CoreWebView2NewWindowRequestedEventArgs e)
        {
            try
            {
                _deferral = e.GetDeferral();
                Window window = new Window();
                var newWebView = new WebView2();

                window.Content = newWebView;
                window.Show();

                await newWebView.EnsureCoreWebView2Async();
                e.NewWindow = newWebView.CoreWebView2;

                await newWebView.CoreWebView2.AddScriptToExecuteOnDocumentCreatedAsync("window['TESTDATA'] = 'this doesn't work'");
                e.Handled = true;
                _deferral.Complete();
            }
            catch (Exception ex)
            {
                Debug.Fail(ex.ToString());
            }
        }

When I click on the button undefined is logged to the console instead of this doesn't work.

Not sure if this makes sense, let me know if you have further questions.
Thank you

@vbryh-msft
Copy link
Contributor

vbryh-msft commented Jun 21, 2022

@ztanczos There are could be two issues with your example:

  1. Apostrophe in doesn't causes Uncaught SyntaxError: Unexpected identifier - could you try without it, like just does
  2. Parent window from where you are trying to access console.log(openedWindow['TESTDATA']) matters - could you try to open google.com from google.com? Hope it makes sense. Or just check window['TESTDATA'] in DevTools(right-click -> Inspect) of the new window?
    Screenshot_doeswork

@ztanczos
Copy link
Author

@vbryh-msft thanks, I didn't notice the apostrophe.. 🤦
I changed the test URLs so there's no cross-origin issue and now I see the same behavior, i.e.: undefined is logged when the window is opened but later I see TESTDATA populated.
I'll check whether this behavior would work for us.

@ztanczos
Copy link
Author

What exactly is the purpose of calling deferral.Complete()? I commented out that line so that I never call .Complete() and I don't see any change in behavior.

@vbryh-msft
Copy link
Contributor

vbryh-msft commented Jun 29, 2022

Hi @ztanczos - you should use GetDeferral if you want to defer event completion at a later time. Here is an example how it can be used - you can search for more examples in that file. We suggest to always complete deferral if it was taken in order for event to be in proper state and be able to complete successfully.

This is how you would use deferral particularly in new window requested(c++ code)

Is injecting JS script works in new window as expected?

@ztanczos
Copy link
Author

What I observe when using the Deferral in the NewWindowRequested event handler is that once I set .NewWindow then completing or not-completing the deferral doesn't matter: once .NewWindow is set the Javascript window.open call returns with a navigated window object.

Injecting JS works although not as expected. What we're trying to achieve is to inject custom Javascript into the new window before the window object is returned to the caller. The reason for this is that we provide a large set of custom functionalities for hosted web applications via JS bindings which should be available to child windows as well. With the current behavior there's a timing issue: JS bindings will be available at some point but we have no control over when exactly, so code which relies on JS objects being available in child windows as soon as the window object is ready would break.
Originally I thought we should use the Deferral to signal when .NewWindow is ready to be used but apparently it is not the case.

@plantree1995
Copy link

Hi @ztanczos, I have tried some tests based on WPF_GettingStarted and it seems fine. My code is as follows:

public async Task InitializeAsync()
{
	await webView.EnsureCoreWebView2Async();
	webView.CoreWebView2.NewWindowRequested += CoreWebView2_NewWindowRequested;
	//await webView.CoreWebView2.AddScriptToExecuteOnDocumentCreatedAsync("window['TESTDATA'] = 'this works'");
}

private async void CoreWebView2_NewWindowRequested(object sender, Microsoft.Web.WebView2.Core.CoreWebView2NewWindowRequestedEventArgs e)
{
	var _deferral = e.GetDeferral();
	Window window = new Window();
	var newWebView = new WebView2();

	window.Content = newWebView;
	window.Show();

	await newWebView.EnsureCoreWebView2Async();
	e.NewWindow = newWebView.CoreWebView2;

	await newWebView.CoreWebView2.AddScriptToExecuteOnDocumentCreatedAsync("window['TESTDATA'] = 'this does work'; alert('hello')");
	await newWebView.CoreWebView2.AddScriptToExecuteOnDocumentCreatedAsync("function square(x) { return x * x; }");

	e.Handled = true;
	_deferral.Complete();
}

You will get the alert and the function works.

@ztanczos
Copy link
Author

Hi @plantree1995 ,
Your code sample doesn't work for me: I don't see any alert, in fact the new window opens as completely blank.

@psmulovics
Copy link

Hi @plantree1995 , Your code sample doesn't work for me: I don't see any alert, in fact the new window opens as completely blank.

Same for me.

@plantree1995
Copy link

Hi @ztanczos @psmulovics, I followed this project, and all source code is posted below:

namespace WPFSample
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
            webView.NavigationStarting += EnsureHttps;
            InitializeAsync();

        }

        async void InitializeAsync()
        {
            await webView.EnsureCoreWebView2Async(null);
            webView.CoreWebView2.WebMessageReceived += UpdateAddressBar;
            webView.CoreWebView2.NewWindowRequested += CoreWebView2_NewWindowRequested;

            await webView.CoreWebView2.AddScriptToExecuteOnDocumentCreatedAsync("window.chrome.webview.postMessage(window.document.URL);");
            await webView.CoreWebView2.AddScriptToExecuteOnDocumentCreatedAsync("window.chrome.webview.addEventListener(\'message\', event => alert(event.data));");

        }

        void UpdateAddressBar(object sender, CoreWebView2WebMessageReceivedEventArgs args)
        {
            String uri = args.TryGetWebMessageAsString();
            addressBar.Text = uri;
            webView.CoreWebView2.PostWebMessageAsString(uri);
        }

        private async void CoreWebView2_NewWindowRequested(object sender, Microsoft.Web.WebView2.Core.CoreWebView2NewWindowRequestedEventArgs e)
        {
            var _deferral = e.GetDeferral();
            Window window = new Window();
            var newWebView = new WebView2();

            window.Content = newWebView;
            window.Show();

            await newWebView.EnsureCoreWebView2Async();
            e.NewWindow = newWebView.CoreWebView2;

            await newWebView.CoreWebView2.AddScriptToExecuteOnDocumentCreatedAsync("window['TESTDATA'] = 'this does work'; alert('hello')");
            await newWebView.CoreWebView2.AddScriptToExecuteOnDocumentCreatedAsync("function square(x) { return x * x; }");

            e.Handled = true;
            _deferral.Complete();
        }

        void EnsureHttps(object sender, CoreWebView2NavigationStartingEventArgs args)
        {
            String uri = args.Uri;
            if (!uri.StartsWith("https://"))
            {
                webView.CoreWebView2.ExecuteScriptAsync($"alert('{uri} is not safe, try an https link')");
                args.Cancel = true;
            }
        }

        private void ButtonGo_Click(object sender, RoutedEventArgs e)
        {
            if (webView != null && webView.CoreWebView2 != null)
            {
                webView.CoreWebView2.Navigate(addressBar.Text);
            }
        }

    }

}

Compile and run it in debug mode, open any links in new window, I would get the alert and function square(). If you have any other questions, please let me know.

@ztanczos
Copy link
Author

Hi @plantree1995,
This sample is slightly different from what we're trying to achieve. We're trying to open a new window using Javascript's window.open() this way:

<html>
<head>
<title>test</title>
<script type="text/javascript">
	var openedWindow;
	
	console.log(window['TESTDATA'])
	
	function openWindow() {
	  openedWindow = window.open('http://mytesturl/wv2test.html')
	  console.log(openedWindow['TESTDATA']);
	}
</script>
</head>
<body>
<h1>hello world</h1>
<button onclick="openWindow()">click me</button>
</body>
</html>

When I open this HTML with the sample code from WPF_GettingStarted I see the main page rendered correctly but when I click on the click me button I only get a blank window and no alert pop-up + undefined is logged to the console indicating that TESTDATA is not available on openedWindow.

@plantree1995
Copy link

Hi @ztanczos, I tried this way.

      private async void CoreWebView2_NewWindowRequested(object sender, Microsoft.Web.WebView2.Core.CoreWebView2NewWindowRequestedEventArgs e)
      {
          try
          {
              var _deferral = e.GetDeferral();
              Window window = new Window();
              var newWebView = new WebView2();

              window.Content = newWebView;
              window.Show();

              await newWebView.EnsureCoreWebView2Async();
              e.NewWindow = newWebView.CoreWebView2;

              await newWebView.CoreWebView2.AddScriptToExecuteOnDocumentCreatedAsync("function square(x) { return x * x; }; function cube(x) { return x * x * x; }");


              e.Handled = true;
              _deferral.Complete();
          }
          catch (Exception )
          {

          }
      }

and the html just like this:

<html>

<head>
  <title>test</title>
  <script type="text/javascript">
    var openedWindow;

    console.log(window['TESTDATA'])

    function openWindow() {
      openedWindow = window.open('http://bing.com', 'hello')
    }
  </script>
</head>

<body>
  <h1>hello world</h1>
  <button onclick="openWindow()">click me</button>
</body>

</html>

When you click on the button to open a new window, you could use function square() and function cube() in the Console.

@ztanczos
Copy link
Author

Hi @plantree1995,
Thanks for looking into this but I know that eventually the injected Javascript code will be there but I have no control over when and I'd need to ensure that the returned Window object already has the injected JS functionality right after the window.open() call.

Our use-case: we're hosting web applications in our container which provides rich functionality for these web applications via JS + interop objects. This functionality is required in windows opened by these web applications too without timing issues. In the past we could achieve the same with CEF + CefSharp by using V8Extensions but now we're struggling with WebView2.

@plantree1995
Copy link

Hi @ztanczos,
Do you mean that you want to get the attribute of child window' JS functionality in the parent window?

@plantree
Copy link
Contributor

Hi @gkerenyi,

Thanks for your professional reply!

According to your description, the result should be as expected. According to the MDN, using <a> without given target means default behavior, which is _self, and the current window will be reused. No new window will be opened, so in my story, rel="noopener" actually does nothing.

To help better understand your need, could you provide the scenario where you need to inject scripts to all opened window?

@gkerenyi
Copy link

Hi @plantree,

To help better understand your need, could you provide the scenario where you need to inject scripts to all opened window?

We are building a generic browser where we inject a custom script to each page for various purposes (e.g. analyzing content, showing overlays on some elements, etc.). Script injection should work in all pages regardless of the way they were opened, otherwise the script's functionalities would depend on whether the page was opened directly or in a new tab/window.

Unfortunately, if we don't set rel="opener" to <a> or <form>, perhaps we could not inject our custom scripts successfully.

With all due respect, this sounds like an assumption. I don't understand why script injection would not be allowed with rel="noopener". The linked documentation states that rel=noopener means the scripts of the new window do not have access to the opening window. From this point of view, why would an injected script be different from a script that is loaded by the new page itself (which is allowed with rel=noopener)?

@plantree
Copy link
Contributor

Hi @gkerenyi,

Thanks for your professional and patient analysis! As for your concern, it might be a problem, and I'll keep track of this and try to fix if it is possible.

Any progress will be synchronized with you in time. If you have any other questions, please feel free to let me know.

@plantree
Copy link
Contributor

Hi @gkerenyi,

After some investigations, I have found an easy way to make it work. If you're available, please have a try.

Do a navigate at the end of handling NewWindowRequestEvent, like this:

private async void CoreWebView2_NewWindowRequested(object sender, Microsoft.Web.WebView2.Core.CoreWebView2NewWindowRequestedEventArgs e)
{
    var deferral = e.GetDeferral();
    Window window = new Window();
    var newWebView = new WebView2();
    window.Content = newWebView;
    window.Show();
    await newWebView.EnsureCoreWebView2Async();
    await newWebView.CoreWebView2.AddScriptToExecuteOnDocumentCreatedAsync("alert('hello')");
    newWebView.CoreWebView2.NavigationStarting += (s, ev) =>
    {
        Debug.WriteLine("-------------New WebView2 NavigationStarting: " + ev.Uri);
    };
    e.NewWindow = newWebView.CoreWebView2;
    e.Handled = true;
    deferral.Complete();
    // Will work in `noopener` mode
    newWebView.CoreWebView2.Navigate(e.Uri);
}

I will still keep focus on this and try to find an easier and more elegant way. Any feedbacks will be welcome. Thanks!

@gkerenyi
Copy link

Hi @plantree,

Thank you for your suggestion, but unfortunately it is not satisfactory, because it doesn't work with POST requests (the POST data is lost after re-navigating). In fact, this kind of re-navigation is the current workaround we are using to mitigate the original timing problem of script injection in the stable version of WebView2, and it is a major issue for us that POST requests in new windows are failing with this workaround.

To give you background, many sites (e.g., banking sites) open new windows using POST navigation. We need script injection to work in such a situation and we need the navigation to get the POST data through.

To summarize:

  • Script injection should work in all pages regardless of the way they were opened.
  • We can't use Navigate() but need to rely on the parent WebView to handle the navigation for the child WebView, otherwise POST navigation will lose the POST data.

Thank you for your continued focus on this problem!

@plantree
Copy link
Contributor

Hi @gkerenyi,

Thanks for your patience and meticulous analysis!

If you're available, could you provide a simple test demo, which has a complete coverage of the scenes you need. That might be very helpful while we are investigating this issue :) .

@gkerenyi
Copy link

Hi @plantree,

I forked a demo project previously used in this thread and simplified it to demonstrate the relevant test cases:
https://github.com/gkerenyi/ScriptVsNewWindow

Please see the README file for details on the actual and expected behaviors.

@plantree
Copy link
Contributor

Hi @gkerenyi,

Thanks for your well-prepared demo project!

I'll check that and any progress will be synchronized to you ASAP.

@MarkIngramUK
Copy link

@plantree , what's the status of this? Is there an ETA for a fix?

CC @nishitha-burman

@plantree
Copy link
Contributor

plantree commented Aug 1, 2023

Hi @MarkIngramUK,

Thanks for you attention! We have already prepared a fix solution, which is under review internally. I will try to get back with the ETA a few days later.

Thanks for your patience.

@plantree
Copy link
Contributor

plantree commented Aug 7, 2023

Hi @MarkIngramUK,

Apologies for my delayed response. I needed to ensure that I was up-to-date with the latest developments.

The fix patch has been included in the most recent Canary release, such as version 117.0.2024.0 or later. Feel free to give it a try, and your feedbacks would be greatly appreciated. Thanks!

cc. @novac42

@psmulovics
Copy link

@plantree and which version of the nuget would be needed for it? Or does that not matter?

notify @ztanczos

@plantree
Copy link
Contributor

plantree commented Aug 8, 2023

Hi @psmulovics and @ztanczos,

Thanks for your interest. Fortunately, this alteration takes place during runtime, eliminating the need for a NuGet update. Simply utilize the latest Canary version and verify its effects. Your feedbacks will be greatly appreciated. Thanks!

@gkerenyi
Copy link

gkerenyi commented Aug 8, 2023

Hi @plantree,

Thanks for the notification about the fix. I have tested the solution and so far it is looking good.
We will continue our testing and get back to you in case we find anything.

@plantree
Copy link
Contributor

plantree commented Aug 9, 2023

Hi @gkerenyi,

Thank you for sharing your feedback! Feel free to reach out whenever you have questions. Have a nice day.

@pontusn
Copy link

pontusn commented Aug 22, 2023

I've tried latest Canary 118.* and there are still something strange happening related to first navigation via window.open.

When we actually get correct script injection the mechanism behind WebMessage fails, breaking both our custom integrations and general support for COM.

@plantree
Copy link
Contributor

Hi @pontusn,

Thanks for your feedback! To help better understand your issue, could you provide a simple demo for reproduction? Thanks.

@gkerenyi
Copy link

gkerenyi commented Oct 24, 2023

Hi @plantree & @victorhuangwq,

We found a timing problem similar to script injection, but this time with web resource request handling, and it can be reproduced both with the WebResourceRequested event and the Chromium DevTools Protocol Fetch.requestPaused event.

The essence of the issue is the following:

  • Assigning a handler to these events before setting CoreWebView2NewWindowRequestedEventArgs.NewWindow has no effect (handlers are not called).
  • Assigning a handler to these events after setting CoreWebView2NewWindowRequestedEventArgs.NewWindow results in a race condition, because navigation is already started in the background when setting NewWindow, so nothing guarantees that we already have a handler in place when the first web resource requests need to be processed.

I created a small repro app, please find it here with additional information in the readme: https://github.com/gkerenyi/CdpVsNewWindow

Either it should be possible to set up both types of event handlers before setting NewWindow, or navigation should not start until the deferral for NewWindowRequested is completed.

Since the WebResourceRequested event is lacking in functionality compared to CDP Fetch, we especially need the Fetch method to be guaranteed to be ready to process all web requests when starting navigation in a new window.

@pontusn
Copy link

pontusn commented Oct 24, 2023

We have seen same problem. Our workaround is to detect failed initial navigation and retry.

@victorhuangwq
Copy link
Collaborator

@yildirimcagri-msft looks like there's regression on something that was previously addressed - could you take a look?

@gkerenyi
Copy link

@pontusn As discussed before in relation to script injection, re-navigation is not an option, because it loses POST data.
The same is true for web request processing as for script injection: it should be possible to hook everything up before navigation is started in the new window by the opener window.

@pontusn
Copy link

pontusn commented Oct 26, 2023

Agree, but for our usage retry works.

@RendijsSmukulis
Copy link

@nishitha-burman as discussed, I've created a new issue (#4181) as the discussion in this bug report has deviated from the original issue (script injection).

@champnic
Copy link
Member

champnic commented Apr 3, 2024

Apparently this original issue was fixed back in runtime versions 114+. Closing as completed.

@champnic champnic closed this as completed Apr 3, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working tracked We are tracking this work internally.
Projects
None yet
Development

No branches or pull requests