CefSharp Inject Javascript prior to any document load/processing
Asked Answered
B

2

11

For a project I am working on I need to inject javascript prior to any of the webpage document processing begins. This can easily be achieved via the WebBrowser component, but I am encountering difficulty using CefSharp.

Here is a simplification of the problem, a webpage needs an "InjectedObject" to be present to function. Calling the webpage without injection occurring at the very top of the document, or being evaluated/executed before the document is processed would result in:

=====html example output on failure=====

isObjectPresent?

false

=====

Where as I need the webpage to display:

=====html example output on success=====

isObjectPresent?

true

=====

<html>
  <head>
    <script>
      isObjectPresent = typeof InjectedObject == "object";
    </script>
  </head>
  <body>
    <p>isObjectPresent?</p>
    <div id="result"></div>
    <script>
      document.getElementById("result").innerHTML = isObjectPresent;
    </script>
  </body>
</html>

Looking at all the available suggestions would indicate I should use LoadingStateChanged() or FrameLoadEnd() to inject the script, ie:

public void OnFrameLoadEnd(object sender, FrameLoadEndEventArgs args) {
  if (args.Frame.IsMain) {
    args.Frame.ExecuteJavascriptAsync("window.InjectedObject = {};");
  }
}

However all iterations I have tried of this, and even using FrameLoadStart, has resulted in the inserted javascript occurring after the document has begun processing. Is there any example of a true javascript injection insuring it occurs BEFORE document processing begins. (making sure to avoid a race condition/timing issue).

As an example of the WebBrowser component behavior that I am looking to imitate is:

private void uiWebBrowser_Navigated(object sender, System.Windows.Navigation.NavigationEventArgs e) 
{
  var browser = (WebBrowser)sender;
  var document = browser.Document as HTMLDocument;
  var head = document.getElementsByTagName("head").Cast<HTMLHeadElement>().First();
  if (head != null)
  {
    var script = document.createElement("script") as IHTMLScriptElement;
    script.text = "window.InjectedObject = {};"
    if (head.firstChild != null)
    {
      head.insertBefore((IHTMLDOMNode)script, head.firstChild);
    }
    else
    {
      head.appendChild((IHTMLDOMNode)script;
    }
  }
}

Any help or suggestion is welcome, ideally I'd like to avoid downloading the page via an internet request parsing and inserting, and then using loadhtml, since I expect I would have to do that potentially for All navigation actions that impacted the main frame, which sounds like a hack job.

Following up from the comments it was suggested that the javascript V8 engine context was sufficient for the above use case. Attempting to implement the OnContextCreated method from the IRenderProcessMessageHandler interface has the same results.

==MainWindow.xaml==

<Window x:Class="ExampleCefSharp001.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:cefSharp="clr-namespace:CefSharp.Wpf;assembly=CefSharp.Wpf"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    xmlns:local="clr-namespace:ExampleCefSharp001"
    mc:Ignorable="d"
    Title="MainWindow" Height="1000" Width="1100">
  <Grid>
    <cefSharp:ChromiumWebBrowser x:Name="uiWebView"></cefSharp:ChromiumWebBrowser>
  </Grid>
</Window>

==MainWindow.xaml.cs==

public partial class MainWindow : Window
{
  JavascriptManager jsmanager;

  public MainWindow()
  {
    InitializeComponent();

    jsmanager = new JavascriptManager(uiWebView);
  }
}

public class JavascriptManager : ILoadHandler, IRenderProcessMessageHandler
{
  string injection = "window.InjectedObject = {};";

  public JavascriptManager(ChromiumWebBrowser browser)
  {
    browser.LoadHandler = this;
    browser.RenderProcessMessageHandler = this;

    //  Lets just pretend this is a real url with the example html above.
    browser.Address = "https://www.example.com/timingtest.htm"
  }

  public void OnContextCreated(IWebBrowser browserControl, IBrowser browser, IFrame frame)
  {
    frame.ExecuteJavaScriptAsync(injection);
  }
}

I do appreciate the comments and suggestions. If there is something I am missing please let me know!

Bisectrix answered 19/1, 2017 at 15:36 Comment(8)
Read the FAQ, the topic is covered there.Presocratic
Hi Amaitland, I assume you are speaking of [github.com/cefsharp/CefSharp/wiki/Frequently-asked-questions], which indicates numerous ways to inject javascript at run time, but makes no mention of preprocessing (that I can see). If there is a reference to preprocessing I would be very interested to know where it is.Bisectrix
V8 context creation is very likely sufficient for your purposes.Presocratic
Updated with code using OnContextCreated, unfortunately had the same results.Bisectrix
hi amaitland, I did update the question specifically based on your suggestion to use the V8 context creation, which I assume you mean was to use OnContextCreated. I even added the MainWindow.xaml (not that I was doing much with it) as well as the MainWindow.xaml.cs code for a more complete view. the html page itself is the test. if there is not an object assigned to window.InjectedObject it simply displays on the body that it was not present. The problem is preprocessing, in other browsers I can inject javascript ahead of all other javascript on the html page.Bisectrix
There is another option, very complicated. If there is another browser that meets your requirements go with that.Presocratic
Forgive me if I'm misinterpreting, but I'm not sure why you are being so combative, the purpose of this question was to identify if it was possible to do preprocessing with cefsharp. I actually do appreciate your suggestion. It simply didn't work for the test case I presented, and I know by virtue of looking at the documentation/forums that you are active in that community, so you are much better versed in what cefsharp can an cannot do than I am. As it stands I have not been able to use cefsharp to address the timing problem I presented, the html document always presents "false".Bisectrix
I'm very busy, so responding on my phone, responses are short, no time for pleasantries. For clarity, I've maintained the project for over two years, and I'm the first to tell you it's got plenty of limitations, if you find another browser that better suits your needs, then you should switch. See github.com/cefsharp/CefSharp/blob/cefsharp/53/CefSharp.Example/… if you can understand what's going on then you can pre-process the response data. Dig through the source to see how it's usedPresocratic
B
7

Finally got back to this. Heavily based on example found in: CefSharp.Example/Filters/FindReplaceResponseFilter.cs

implementing the IRequestHandler and IResponseFilter interfaces:

==MainWindow.xaml==

<Window x:Class="ExampleCefSharp001.MainWindow"
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:cefSharp="clr-namespace:CefSharp.Wpf;assembly=CefSharp.Wpf"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
  xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
  xmlns:local="clr-namespace:ExampleCefSharp001"
  mc:Ignorable="d"
  Title="MainWindow" Height="1000" Width="1100">
  <Grid>
    <cefSharp:ChromiumWebBrowser x:Name="uiWebView"></cefSharp:ChromiumWebBrowser>
  </Grid>
</Window>

==MainWindow.xaml.cs==

public partial class MainWindow : Window
{
  JavascriptManager jsmanager;

  public MainWindow()
  {
    InitializeComponent();

    jsmanager = new JavascriptManager(uiWebView);
  }
}

public class JavascriptManager : IRequestHandler
{
  string injection = "window.InjectedObject = {};";

  public JavascriptManager(ChromiumWebBrowser browser)
  {
    browser.RequestHandler = this;

    //  Lets just pretend this is a real url with the example html above.
    browser.Address = "https://www.example.com/timingtest.htm"
  }

  public IResponseFilter GetResourceResponseFilter(IWebBrowser browserControl, IBrowser browser, IFrame frame, IRequest request, IResponse response)
    {
        if (frame.IsMain && request.ResourceType == ResourceType.MainFrame) 
        {
            return new JavascriptInjectionFilter(injection);
        }
        return null;
    }
}

public class JavascriptInjectionFilter : IResponseFilter
{
    /// <summary>
    /// Location to insert the javascript
    /// </summary>
    public enum Locations
    {
        /// <summary>
        /// Insert Javascript at the top of the header element
        /// </summary>
        head,
        /// <summary>
        /// Insert Javascript at the top of the body element
        /// </summary>
        body
    }

    string injection;
    string location;
    int offset = 0;
    List<byte> overflow = new List<byte>();

    /// <summary>
    /// Constructor
    /// </summary>
    /// <param name="injection"></param>
    /// <param name="location"></param>
    public JavascriptInjectionFilter(string injection, Locations location = Locations.head)
    {
        this.injection = "<script>" + injection + "</script>";
        switch (location)
        {
            case Locations.head:
                this.location = "<head>";
                break;

            case Locations.body:
                this.location = "<body>";
                break;

            default:
                this.location = "<head>";
                break;
        }
    }

    /// <summary>
    /// Disposal
    /// </summary>
    public void Dispose()
    {
        //
    }

    /// <summary>
    /// Filter Processing...  handles the injection
    /// </summary>
    /// <param name="dataIn"></param>
    /// <param name="dataInRead"></param>
    /// <param name="dataOut"></param>
    /// <param name="dataOutWritten"></param>
    /// <returns></returns>
    public FilterStatus Filter(Stream dataIn, out long dataInRead, Stream dataOut, out long dataOutWritten)
    {
        dataInRead = dataIn == null ? 0 : dataIn.Length;
        dataOutWritten = 0;

        if (overflow.Count > 0)
        {
            var buffersize = Math.Min(overflow.Count, (int)dataOut.Length);
            dataOut.Write(overflow.ToArray(), 0, buffersize);
            dataOutWritten += buffersize;

            if (buffersize < overflow.Count)
            {
                overflow.RemoveRange(0, buffersize - 1);
            }
            else
            {
                overflow.Clear();
            }
        }


        for (var i = 0; i < dataInRead; ++i)
        {
            var readbyte = (byte)dataIn.ReadByte();
            var readchar = Convert.ToChar(readbyte);
            var buffersize = dataOut.Length - dataOutWritten;

            if (buffersize > 0)
            {
                dataOut.WriteByte(readbyte);
                dataOutWritten++;
            }
            else
            {
                overflow.Add(readbyte);
            }

            if (char.ToLower(readchar) == location[offset])
            {
                offset++;
                if (offset >= location.Length)
                {
                    offset = 0;
                    buffersize = Math.Min(injection.Length, dataOut.Length - dataOutWritten);

                    if (buffersize > 0)
                    {
                        var data = Encoding.UTF8.GetBytes(injection);
                        dataOut.Write(data, 0, (int)buffersize);
                        dataOutWritten += buffersize;
                    }

                    if (buffersize < injection.Length)
                    {
                        var remaining = injection.Substring((int)buffersize, (int)(injection.Length - buffersize));
                        overflow.AddRange(Encoding.UTF8.GetBytes(remaining));
                    }

                }
            }
            else
            {
                offset = 0;
            }

        }

        if (overflow.Count > 0 || offset > 0)
        {
            return FilterStatus.NeedMoreData;
        }

        return FilterStatus.Done;
    }

    /// <summary>
    /// Initialization
    /// </summary>
    /// <returns></returns>
    public bool InitFilter()
    {
        return true;
    }

}

Thanks to amaitland for pointing me in the right direction, and for the sample program that the vast majority of the above code was based on. End result:

<html><head></head><body><script>window.InjectedObject = {}</script>
  <script>
    isObjectPresent = typeof InjectedObject == "object";
  </script>
  <p>isObjectPresent?</p>
  <div id="result"></div>
  <script>
    document.getElementById("result").innerHTML = isObjectPresent;
  </script>
</body></html>

Which meets my needs of pre-processing the document with some text at the top of the header ensuring no timing issues where existing code might be run before the injected code.

edit couple small fixes. added control logic to only insert when a mainframe is loaded.

Bisectrix answered 25/1, 2017 at 19:47 Comment(1)
Still some debugging code left in the example, I'm sure if you looked at the code for 5mins you'd be able to resolve the problem.Presocratic
B
2

Well your answer is correct that you should override the implementation of GetResourceResponseFilter but in case you didn't implement the interface in a correct way you will end up with browser not rendering content, you can instead inherit the DefaultRequestHandler and override the GetResourceResponseFilter() and provide the custom filter as mentioned in the accepted answer, this will be easier in case you need only to ovveride this specific functionality :

public class CustomRequestHandler : DefaultRequestHandler
{
    string script = "alert('hello');";

    public override IResponseFilter GetResourceResponseFilter(CefSharp.IWebBrowser browserControl, IBrowser browser, IFrame frame, IRequest request, IResponse response)
    {
        if (frame.IsMain && request.ResourceType == ResourceType.MainFrame)
        {
            return new JavascriptInjectionFilter(script);
        }
        return null;
    }
}

Then assign it to the chromium browser:

CustomRequestHandler customRequestHandler = new CustomRequestHandler();
chromeBrowser.RequestHandler = customRequestHandler ;
Bobbi answered 20/3, 2019 at 13:32 Comment(2)
Probably better if you edit the accepted answer with your code changes. ThanksPresocratic
DefaultRequestHandler Has been renamed: github.com/cefsharp/CefSharp/issues/3124Emaciated

© 2022 - 2024 — McMap. All rights reserved.