I have a WebBrowser control in my C# application. The web browser is under the user's control, that is, he can load any web page his computer can access on the web (of course limited by proxy, hosts file and so on).
I need to know and to be notified when there is a Javascript call inside the page loaded in the web browser component.
First example: given a link like this
test
When the user clicks the link I need to know that the function "jsFunct" has been called.
Second example: given a call like
<script type="text/javascript">
window.setTimeout("jsFunct()", 1000);
</script>
I need to know that, 1 second after the execution of the script, the function jsFunct has been called.
The best thing would be to have an event fired when the function is called. It would also be great if the event could get the Javascript code executed, or at least the function name in the arguments.
EDIT:
Even if the question is related to the webbrowser component, anything that allows the user to detect javascript activation (even via js) would be fine, being able to inject a js that handles the javascript event and passes it to the wb control triggering some event that it can handle.
You can use window.external to call a C# method when a global function is fired in JavaScript. See WebBrowser Control Overview for details on window.external.
You'll need to set ObjectForScripting: Webbrowser control's window.external is ALWAYS null. for this to work.
Take #Krishna's answer to add the JavaScript (but drop jQuery because it won't be needed):
private void addScript(HtmlElement head, string scriptSource)
{
HtmlElement lhe_script = head.Document.CreateElement("script");
IHTMLScriptElement script = (IHTMLScriptElement)lhe_script.DomElement;
script.src = scriptSource;
head.AppendChild(lhe_script);
}
addScript(WebBrowser.Head, #"InjectMonitor.js");
The JavaScript below (InjectMonitor.js) will find all global functions and attach your specified handler:
function augment(withFn) {
var name, fn;
for (name in window) {
fn = window[name];
if (typeof fn === 'function') {
window[name] = (function(name, fn) {
var args = arguments;
return function() {
withFn.apply(this, args);
fn.apply(this, arguments);
};
})(name, fn);
}
}
}
augment(function(name, fn) {
console.log("calling " + name, fn);
// window.external.yourC#method
});
In this example, taken from Adding Console Log to Every Function, it just logs the call to console; but using window.external you could send some message back to your C# application with details of what function was called from the client.
Finally, here's a JS Bin example (run it and don't forget the console): JS Bin Example
On the webbrowser load event,
Inject Jquery
Inject Monitor scripts
,
private void addScript(HtmlElement head, string scriptSource)
{
HtmlElement lhe_script = head.Document.CreateElement("script");
IHTMLScriptElement script = (IHTMLScriptElement)lhe_script.DomElement;
script.src = scriptSource;
head.AppendChild(lhe_script);
}
addScript(Webbrowser.Head, #"<Change File Path here>jquery.min.js");
addScript(WebBrowser.Head, #"InjectMonitor.js");
your file InjectMonitor.js should be something like this
$(document).ready(function () {
//Add click event for every anchor on the page loaded- note this merely alerts text on click. you can however add your own function
$("a").click(function (e) { alert($(this).text()); return false;})
});
Well what krishna has answered is interms of pure javascript attaching to events, however i see that you might need to attach it to all the tags(a,p,div,input) etc and to all the events attached to each tag.
i believe the another way is to play around with the BHO(browser helper object) available to your in .net, and if not and you are good at VC++ and MFC you can also play around with Windows Hooks.
Related
I am using a WKWebView inside a UIViewController's view to display a webpage hosted on a server using a url endpoint. The webpage uses Reactjs. That is all the information I have about the webpage. The code creates a webview and inserts the webview as subview of the controllers view.
let requestObj = URL(string:urlString)!
let preferences = WKPreferences()
preferences.javaScriptEnabled = true
let configuration = WKWebViewConfiguration()
configuration.preferences = preferences
webViewWK = WKWebView(frame: .zero, configuration: configuration)
webViewWK.navigationDelegate = self
_ = webViewWK.load(requestObj)
webViewwrapper = WKWebViewWrapper(forWebView: webViewWK)
The webpage loads fine and also the controller acts as the delegate of the webview and receives the messages for the same. Now I also implement a WKWebViewWrapper class which conforms to WKScriptMessageHandler. This class can then receive messages from webkit object which is created by the WKWebView behing the scenes. The implementation for the same is as below
class WKWebViewWrapper : NSObject, WKScriptMessageHandler{
var wkWebView : WKWebView
let eventNames = ["buttonClick"]
var eventFunctions: Dictionary<String, (String) -> Void> = [:]
let controller: WKUserContentController
init(forWebView webView : WKWebView){
wkWebView = webView
controller = WKUserContentController()
super.init()
}
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
if let contentBody = message.body as? String {
if let eventFunction = eventFunctions[message.name]{
print("Detected javascript event")
}
}
}
func setUpPlayerAndEventDelegation(){
wkWebView.configuration.userContentController = controller
for eventname in eventNames {
controller.add(self, name: eventname)
eventFunctions[eventname] = { _ in }
wkWebView.evaluateJavaScript("var elements = document.getElementsByClassName('btn button_btn button_primary button_md button_block'); for (var i = 0 ; i < elements.length; i++) { elements[i].addEventListener('onClick', function(){ window.webkit.messageHandlers.\(eventname).postMessage(JSON.stringify(isSuccess)) }); }") { any, error in
if let error = error {
print("EvaluateJavaScript Error:",error)
}
if let any = any {
print("EvaluateJavaScript anything:", any)
}
}
}
}
}
The setUpPlayerAndEventDelegation() method is the most important part. Here for the controller object which is of type WKUserContentcontroller adds message handlers using its add(: , name:) method. According to documentation this method adds a messageHandler of the name parameter to the webkit object. Whenver the messsage handler is triggered, the WKScriptMessageHandler's userContentController( userContentController: WKUserContentController, didReceive message: WKScriptMessage) method is called with useful parameters. Then I inject javascript into the webpage using evaluateJavaScript method of webview which is as below
var elements = document.getElementsByClassName('btn button_btn button_primary button_md button_block');
for (var i = 0 ; i < elements.length; i++) {
elements[i].addEventListener('onClick', function(){ window.webkit.messageHandlers.\(eventname).postMessage(JSON.stringify(true)) });
}
It fetches elements with the given class. Then I iterate over the array to add event listener for HTML event 'onClick' for each element. For events listener I add an anonymous function to trigger the previously registered message handler on the webkit. This script is executed properly as I don't get error in the completion block of the evaluateJavaScript method. So I can be sure now that when a button onClick HTML event occurs the annonymous function will execute, which in turn will postMessage for the messageHandler on the webkit object.
Now I call the WKWebViewWrapper's setUpPlayerAndEventDelegation() method from WKWebViewDelegate method webView(_ webView: WKWebView, didFinish navigation: WKNavigation!), where I can be sure that all the HTML elements are loaded by comapring WKNavigation objects.
The flow executes and after the Page loads and I click any buttons the events are not observed by my script message handler i.e the WKWebViewWrapper class. The method userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) is not fired at all.
Is there something that I am missing here?. I am not good at Javascript. Please do let me know if Reactjs needs some different script to and event listener to button elements. I have reffered this tutorial.
PS: If we add the similar script to output console messages on a webbrowser which has loaded the page, it works fine.
Notice an important behavior (but less known) about WKWebViewConfiguration in Apple Docs,
WKWebViewConfiguration is only used when a web view is first initialized. You cannot use this class to change the web view's configuration after it has been created.
So, this is typically you should setup your WKUserContentController fully prior to web view creation.
// First, create custom configuration with user script
let userController = WKUserContentController()
let scalingScriptString = "var meta = document.createElement('meta'); meta.setAttribute('name', 'viewport'); meta.setAttribute('content', 'width=device-width'); document.getElementsByTagName('head')[0].appendChild(meta);";
let scalingScript = WKUserScript(source: scalingScriptString, injectionTime: .atDocumentStart, forMainFrameOnly: true)
userController.addUserScript(scalingScript)
let configurations = WKWebViewConfiguration()
configurations.userContentController = userController // MUST set controller in configurations before creating webview
// Now, use that configuration to create the webview
webView = WKWebView(frame: .zero, configuration: configurations)
So I actually ended up getting it to work. There were a number of issues but they were all due to errors in my JavaScript. The JavaScript simply failed to execute rather than producing any errors however which made it seem like it was an iOS problem. It took a really long time and lots of debugging via Safari.
Essentially what I discovered is probably a mistake that is rampant because of how little documentation/articles there are online for sending messages via WKMessagingScript. All of the samples show something like this:
window.webkit.messageHandlers.test.postMessage(“message to post”)
Some of them go on to say you can send anything even a json dictionary. What they fail to say is that 1) you can NOT pass an object to this function, and 2) passing a literal dictionary is illegal in JavaScript. They also don’t give any more applicable examples. You may want just a string message to let you know something has happened but if you need to pass data you’ll be getting it from the elements and are likely to make mistake number 1 in your implementation (what both you and I did).
1) is a biggie. In your example you are calling a function. In JavaScript functions are first class citizens so you are passing an object. You also can not, for example, pass element.id which is what I was doing because you have to pass the element. What you need to do is pass the value only which is a foundation type.
*** Note you can pass an object within JavaScript such as console.log(element); which is what makes debugging this issue so hard. If you had commented out the WebKit call but passed your function to a console log it would have worked, implying the problem was with iOS, rather than highlighting the problem was actually with passing an object.
2) will usually work in console logging because the browsers we use will recognize it. Enough devs do it that even though it’s not right the browsers will interpret it. iOS may also one day too but it’s better practice to not do it.
This would have worked (assuming no other issues in your code):
var elements = document.getElementsByClassName('btn button_btn button_primary button_md button_block');
for (var i = 0 ; i < elements.length; i++) {
var message = String(JSON.stringify(true));
elements[i].addEventListener('click', function(){
window.webkit.messageHandlers.testEvent.postMessage(message)
});
}
Now I'm not totally sure if sending a string alone will still work or if it needs to be a dictionary as I was sending dictionaries but if you need to send a dictionary you would do it like this:
var elements = document.getElementsByClassName('btn button_btn button_primary button_md button_block');
for (var i = 0 ; i < elements.length; i++) {
var eventName = String(eventName); // if this variable is a string then you probably don't need this step
var stringified = String(JSON.stringify(true));
var message = {};
message[String(eventName)] = stringified;
elements[i].addEventListener('click', function(){
window.webkit.messageHandlers.testEvent.postMessage(message)
});
}
Rather than window.webkit.messageHandlers.testEvent.postMessage({"foo": "bar"}) for example. Note that I didn't test that this did not work, I just read this online and asked a JS dev I know and they confirmed it so who knows it may work. I think it's safer to break it up though just in case. There is some shorthand that has been added to JS using square brackets that would allow you to pass a literal however it is only recently added so I don't imagine all versions of iOS support it and I would not recommend using it.
I do see two additional problems with your code though. First is that you are using 'onClick' when you should be using 'click'.
So onclick creates an attribute within the binded HTML tag, using a string which is linked to a function. Whereas .click binds the function itself to the property element.
https://teamtreehouse.com/community/whats-the-difference-between-click-and-onclick
If you were a web dev you would handle both but for iOS you should only use click.
The other thing I noticed in your code is you are passing the eventName into the webkit function window.webkit.messageHandlers.\(eventName).postMessage...... I'm not totally sure if that will work or not. I suspect it will not because that is not a string, that is a function call. Though I don't know anything about JavaScript at all (this was literally the first JS I've ever written) so I may be wrong about that. In objc or swift though you could not do that when making a function call. Even if it would work I think it adds too much complexity and is not scalable if the iOS WKMessagingScript were updated to no longer allow it. I would suggest using the correct name. If you want to encapsulate your code then switch on eventName.
After 4 years I got a chance to work on the similar codebase and found out the exact reason of the failure. The exact reason are as below:
Unnecessary and erroneous assignment of userContentController. This didn't replace the actual WKUserContentController created at the time of instantiation of WKWebView. We added the script handler erroneously like so controller.add(self, name: eventname). Instead we should have added the script handler directly to the webviews usercontentcontroller instance like so
//WRONG: To set userContentController and add script handler
wkWebView.configuration.userContentController = controller
controller.add(self, name: eventname)
//CORRECT: To add script handler
wkWebView.configuration.userContentController.add(self, name: eventname)
Thanks to #Ashok for pointing this out.
The timing of evaluating the javascript to add Event Listener to each element was not perfect. By the time our javascript got evaluated the elements had not been created and thus no event listeners were added. There are two ways to perfect the time of evaluation of the javascript. We can introduce a delay which obviously is not a clean solution. The cleaner solution is to use WKUserScript with the injectionTime set to .atDocumentEnd and add the script to the userContentController
let eventName = "SOME_EVENT"
let js = "var elements = document.getElementsByClassName('btn button_btn button_primary button_md button_block'); for (var i = 0 ; i < elements.length; i++) { elements[i].addEventListener('click', function(){ window.webkit.messageHandlers.\(eventname).postMessage("MESSAGE") }); }"
let userScript = WKUserScript(source: js, injectionTime: .atDocumentEnd, forMainFrameOnly: false)
wkWebView.configuration.userContentController.addUserScript(userScript)
I would like to thanks #Abbey Jackson for providing hints to the solution for my case. Her answer pointed me to the fact that onClick, onclick should not be used for adding event listeners to the elements. Instead use click for the same.
// Relevant while coding for WKWebView on iOS
// WRONG: Using onClick or onclick does not register the event listener
elements[i].addEventListener("click", function(){ YOUR_HANDLER_CODE } }
// CORRECT: Use click instead of onClick or onclick for adding event listener
elements[i].addEventListener("click", function(){ YOUR_HANDLER_CODE } }
NOTE: I could pefectly pass a literal dictionary like {"foo": "bar"} in place of "YOUR_MESSAGE". It didnt cause me a problem. Although it could cause a problem if it contained items which couldn't be easily ported to Swift.
I'm trying to write an IE BHO (ported from Chrome) where I need to use XSS with jQuery. I am hoping that I can enable XSS for my own addon/JavaScript by replacing the jQuery function, $.ajaxSettings.xhr with,
$.ajaxSettings.xhr = function() { return window.myExtension.Xhr() }
Where myExtension.Xhr is a COM function defined in my extension (and myExtension is added via ((IExpando)window).addProperty).
I tried defining my Xhr() function as (C# implementing COM),
public object Xhr()
{
return new MSXML.XMLHTTPRequest();
}
But this fails when jQuery tries to set the onload property of the XHR (at jquery-2.1.3.js:8617). So it looks like I'm using the wrong COM XmlHttpRequest (MSXML2.XMLHttp also has a missing onload). So where is the "real" XmlHttpRequest defined?
// Listen to events
xhr.onload = callback();
xhr.onerror = callback("error");
// Create the abort callback
callback = xhrCallbacks[ id ] = callback("abort");
I can of course just use one of the several methods of submitting GET/POST requests in .NET, but then I'd have to dirty my code with workarounds for IE when doing XSS ajax calls (since Firefox and Chrome extensions can do XSS with appropriate permissions).
I did not find a real solution to my question, but a sort of workaround for what I ultimately want to do is to use jQuery 1.x instead, which is able to (and does, for IE6-8) work with the old MSXML.XMLHTTPRequest.
I am posting this for anyone would like to run cross-domain Ajax requests in scripts they inject via a BHO.
First, define this interface:
[ComVisible(true)]
[Guid("XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX")]
[InterfaceType(ComInterfaceType.InterfaceIsDual)]
public interface IExtension
{
[DispId(1)]
object Xhr();
[DispId(2)]
string OptionalFooBar();
}
And implement it somewhere, e.g. in this case my BHO itself:
//Exposed to JS
public object Xhr()
{
return new MSXML2.XMLHTTP();
}
And once your document completes, say in an OnDocumentComplete handler, add a property to the window through IDispatchEx to expose your methods:
void OnDocumentComplete(object pDisp, ref object URL)
{
dynamic window = null;
IExpando windowEx = null;
try
{
if (pDisp != this.site)
return;
var document2 = browser.Document as IHTMLDocument2;
var document3 = browser.Document as IHTMLDocument3;
window = document2.parentWindow;
windowEx = window as IExpando;
windowEx.AddProperty("myExtension")
window.myExtension = this;
}
And then in your JS, after jQuery has been loaded:
jQuery.ajaxSettings.xhr = function () { return window.myExtension.Xhr(); }
My node-webkit application consists of a control window and a presentation window.
The control window gathers data and eventually launches the presentation window via the window.open function.
The presentation window now has access to some information in the global variable.
Now I want to render a graphical representation of that information by creating SVG elements and so forth.
I already have a javascript function to do exactly that, but I need some way of starting that function from the control window.
I can't call it directly, since then the function has to access the other window's DOM.
I have tried using the eval function on the other window's object, but that crashes node-webkit with the message
[18719:0522/205047:ERROR:breakpad_linux.cc(1225)] crash dump file written to
/tmp/chromium-renderer-minidump-788bf8d0c68301d5.dmp
What would be the best way to do this?
Use setInterval to regularly check a global variable?
One solution I've found to be pretty effective is attaching pub/sub functionality to the globalvariable. The setup I've used so far is jQuery-based, though it could also be constructed without. First it is initialized using a variant of this code:
var messages = messages || {};
global.Message = global.Message || function( id ) {
var callbacks, method,
message = id && messages[ id ];
if ( !message ) {
callbacks = jQuery.Callbacks();
message = {
publish: callbacks.fire,
subscribe: callbacks.add,
unsubscribe: callbacks.remove
};
if ( id ) {
messages[ id ] = message;
}
}
return message;
};
Then anywhere between the windows events can be published and subscribed to using the following pattern. One window can publish the data:
global.Message("someButtonClicked").publish(data);
Then the other can listen for it.
global.Message("someButtonClicked").subscribe(onButtonClicked);
function onButtonClicked(data) {
console.log(data);
};
You can inject and eval a piece of JS by using Window.eval():
https://github.com/rogerwang/node-webkit/wiki/Window#windowevalframe-script
I need to handle globally ajax responses. Everything works ok, out of the box, when I only want to call normal javascript action without any arguments. Than I can use p:ajaxStatus controll, and application behaves in a correct way.
What I actually now need to do, is to handle situation, when during ajax request there was externalContext.redirect() call. It happens only in one place in application, but it is called from many places.
I was thinking, that I can for instance add callback param in RequestContext. But how can I access this param in javascript?
While watching in firebug, I can see that callbackParam is returned in json response, but how can I access this value in javascript?
It's been added as a property of default args object which is available in oncomplete context.
So, for example
RequestContext.getCurrentInstance().addCallbackParam("foo", "bar");
is available as
oncomplete="console.log(args.foo)"
See also:
PrimeFaces RequestContext showcase
Update: as per the comments, that turns out to fail in <p:ajaxStatus>. I sugges to report it as a bug to PF guys, that the arguments are not available in <p:ajaxStatus oncomplete>. In the meanwhile, you can workaround it with the following script which is loaded by <h:outputScript target="head"> inside the <h:body> (to guarantee that it's loaded after PrimeFaces script):
var originalPrimeFacesAjaxUtilsSend = PrimeFaces.ajax.AjaxUtils.send;
PrimeFaces.ajax.AjaxUtils.send = function(cfg) {
var originalOncomplete = cfg.oncomplete;
cfg.oncomplete = function() {
ajaxStatusOncomplete.apply(this, arguments);
if (originalOncomplete) {
originalOncomplete.apply(this, arguments);
}
};
originalPrimeFacesAjaxUtilsSend.apply(this, arguments);
};
function ajaxStatusOncomplete(xhr, status, args) {
// Do your thing here.
}
In p:ajaxStatus params available via PrimeFaces.ajax.Queue.xhrs
For example:
oncomplete="console.log(PrimeFaces.ajax.Queue.xhrs[0].pArgs.foo)"
I'm not using eval, and I'm not sure what the problem is that Crockford has with the following. Is there a better approach to solve the following problem or is this just something I need to ignore (I prefer to perfect/improve my solutions if there is areas for improvement).
I'm using some pixel tracking stuff and in this case a client has bound a JS function to the onclick property of an HTML image tag which redirects off the site. I need to track the clicks reliably without running into race conditions with multiples of event listeners on the image. The strategy is to override the event at run time, copying and running it in my own function. Note this is being applied to a site I do not control and cannot change. So the solution looks something like:
...
func = Function(img.attr('onclick'));
...
img.attr('onclick', '');
... //some custom tracking code
func.call(this);
and the JSLint checker throws the eval is evil error.
Is there a better way to avoid race conditions for multiple events around href actions?
You're implicitly using eval because you're asking for the callback function as it was specified as an attribute in the HTML as a string and then constructing a Function with it.
Just use the img.onclick property instead, and you will directly obtain the function that the browser built from the attribute that you can then .call:
var func = img.onclick; // access already compiled function
img.onclick = null; // property change updates the attribute too
... // some custom tracking code
func.call(img, ev); // call the original function
or better yet:
(function(el) {
var old = el.onclick;
el.onclick = function() {
// do my stuff
..
// invoke the old handler with the same parameters
old.apply(this, arguments);
}
})(img);
The advantage of this latter method are two fold:
it creates no new global variables - everything is hidden inside the anonymous closure
It ensures that the original handler is called with the exact same parameters as are supplied to your replacement function
var oldClick = myImg.onclick;
myImg.onclick = function(evt){
// Put you own code here
return oldClick.call( this, evt );
};