How would go about monkey patching the XMLHTTPRequest's onreadystatechange function. I'm trying to add a function that would be called when every ajax request made from a page come back.
I know this sounds like a terrible idea, but the use case is quite peculiar. I want to use a certain SDK with a console (jqconsole) but show status and results from ajax calls within the console without modifying the external SDK.
I've looked at this post which had great info, but nothing on monkey patching the callback which seem to exceed my JavaScript skills.
P.S Can't use jQuery since it only supports ajax calls made from jQuery not from XMLHTTPRequests directly which is the case here.
To monkey-patch XMLHttpRequests, you need to know how an AJAX request is generally constructed:
Constructor invocation
Preparation the request (setRequestHeader(), open())
Sending the request (.send).
General-purpose patch
(function(xhr) {
function banana(xhrInstance) { // Example
console.log('Monkey RS: ' + xhrInstance.readyState);
}
// Capture request before any network activity occurs:
var send = xhr.send;
xhr.send = function(data) {
var rsc = this.onreadystatechange;
if (rsc) {
// "onreadystatechange" exists. Monkey-patch it
this.onreadystatechange = function() {
banana(this);
return rsc.apply(this, arguments);
};
}
return send.apply(this, arguments);
};
})(XMLHttpRequest.prototype);
The previous assumed that onreadystatechange was assigned to the onreadystatechange handler. For simplicity, I didn't include the code for other events, such as onload. Also, I did not account for events added using addEventListener.
The previous patch runs for all requests. But what if you want to limit the patch to a specific request only? A request with a certain URL or async flag and a specific request body?
Conditional monkey-patch
Example: Intercepting all POST requests whose request body contains "TEST"
(function(xhr) {
function banana(xhrInstance) { // Example
console.log('Monkey RS: ' + xhrInstance.readyState);
}
//
var open = xhr.open;
xhr.open = function(method, url, async) {
// Test if method is POST
if (/^POST$/i.test(method)) {
var send = this.send;
this.send = function(data) {
// Test if request body contains "TEST"
if (typeof data === 'string' && data.indexOf('TEST') >= 0) {
var rsc = this.onreadystatechange;
if (rsc) {
// Apply monkey-patch
this.onreadystatechange = function() {
banana(this);
return rsc.apply(this, arguments);
};
}
}
return send.apply(this, arguments);
};
}
return open.apply(this, arguments);
};
})(XMLHttpRequest.prototype);
The main techniques used is the transparent rewrite using...
var original = xhr.method;
xhr.method = function(){
/*...*/;
return original.apply(this, arguments);
};
My examples are very basic, and can be extended to meet your exact wishes. That's up to you, however.
Assuming you can ignore IE...
//Totally untested code, typed at the SO <textarea>... but the concept *should* work, let me know if it doesn't.
var OldXMLRequest = XMLHttpRequest;
// Create a new instance
function XMLHttpRequest() {
var ajax = new OldXMLRequest();
// save old function
var f = ajax.onreadystatechange;
ajax.onreadystatechange = function() {
console.log("Whatever!");
f(); // Call the old function
}
return ajax;
}
you can learn from Ajax-hook written by chinese!
it is a advanced js to enable Monkey patch XMLHTTPRequest
Related
I'm trying to intercept all AJAX calls in order to check if that AJAX response contains specific error code that I send as JSON from my PHP script (codes: ACCESS_DENIED, SYSTEM_ERROR, NOT_FOUND).
I know one can do something like this:
$('.log').ajaxSuccess(function(e, xhr, settings) {
});
But - does this work only if "ajaxSuccess" event bubble up to .log div? Am I correct? Can I achieve what I want by binding "ajaxSuccess" event to document?
$(document).ajaxSuccess(function(e, xhr, settings) {
});
I can do this in either jQuery or raw JavaScript.
If you're using jQuery, $.ajaxSuccess is a good option, but here's a generic option that will intercept XHR calls from all frameworks (I've tested it with ExtJS and jQuery - it should work even if multiple frameworks are loaded concurrently). It's been tested to work with IE8, Chrome and Firefox.
(function(XHR) {
"use strict";
var open = XHR.prototype.open;
var send = XHR.prototype.send;
XHR.prototype.open = function(method, url, async, user, pass) {
this._url = url;
open.call(this, method, url, async, user, pass);
};
XHR.prototype.send = function(data) {
var self = this;
var oldOnReadyStateChange;
var url = this._url;
function onReadyStateChange() {
if(self.readyState == 4 /* complete */) {
/* This is where you can put code that you want to execute post-complete*/
/* URL is kept in this._url */
}
if(oldOnReadyStateChange) {
oldOnReadyStateChange();
}
}
/* Set xhr.noIntercept to true to disable the interceptor for a particular call */
if(!this.noIntercept) {
if(this.addEventListener) {
this.addEventListener("readystatechange", onReadyStateChange, false);
} else {
oldOnReadyStateChange = this.onreadystatechange;
this.onreadystatechange = onReadyStateChange;
}
}
send.call(this, data);
}
})(XMLHttpRequest);
I've posted a more specific example on github which intercepts AJAX calls and posts the AJAX call durations back to the server for statistical analysis.
From http://api.jquery.com/ajaxSuccess/ :
Whenever an Ajax request completes successfully, jQuery triggers the ajaxSuccess event. Any and all handlers that have been registered with the .ajaxSuccess() method are executed at this time.
So the selector doesn't define the position where you are "catching" the event (because, honestly, ajax event by its nature doesn't start from a DOM element), but rather defines a scope to which the handling will be defaulted (i.e. this will poitn to that/those element(s)).
In summary - it should be exactly what you wish for
The best way, which I found https://lowrey.me/intercept-2/
const intercept = (urlmatch, callback) => {
let send = XMLHttpRequest.prototype.send;
XMLHttpRequest.prototype.send = function() {
this.addEventListener('readystatechange', function() {
if (this.responseURL.includes(urlmatch) && this.readyState === 4) {
callback(this);
}
}, false);
send.apply(this, arguments);
};
};
Try using Mockjax.js http://code.appendto.com/plugins/jquery-mockjax
It lets you hijack AJAX calls to the server and mock the location.
I have a page and I would like to disable all AJAX requests with jQuery.
Do you have any ideas? And if it is possible?
if (false) {
//disable all ajax requests
}
If all of your ajax requests are being sent through jQuery ajax methods (including helper methods), you can do this with beforeSend.
window.ajaxEnabled = true;
$.ajaxSetup({
beforeSend: function(){
return window.ajaxEnabled;
}
});
$.post("http://www.google.com"); // throws an error
window.ajaxEnabled = false;
$.post("http://www.google.com"); // doesn't throw an error
http://jsfiddle.net/k2T95/3
And here's one that will block all, regardless of what javascript library is sending it, also based on a global flag. Doesn't affect XDomainRequest obj though
(function (xhr) {
var nativeSend = xhr.prototype.send;
window.ajaxEnabled = true;
xhr.prototype.send = function () {
if (window.ajaxEnabled) {
nativeSend.apply(this, arguments);
}
};
}(window.XMLHttpRequest || window.ActiveXObject));
http://jsfiddle.net/k2T95/4/
EDIT: There are better ways, depending your specific case which i don't know
Try: {test it for cross browser support, i didn't do it}
XMLHttpRequest.prototype.send = function(){};
Not only for requests done in jQuery
If you want to re-enable it:
var oSend = XMLHttpRequest.prototype.send; // keep reference
XMLHttpRequest.prototype.send = function(){};
Then call:
XMLHttpRequest.prototype.send = oSend; // get back reference to prototype method
Been trying to create a basic Ajax library using JavaScript based upon a tutorial in the book "Build your own AJAX Web Applications" by Matthew Eernise (see here) as I want to get more in-depth knowledge of AJAX XML-RPC and REST. Based on the book I have created a JavaScript constructor function to get AJAX or an XMLHttpRequest going, but somehow I seem to suffer from an out-of-scope issue and the Ajax class is not defined in the following script:
function Ajax() {
// properties
this.req = null;
this.url = null;
this.method = 'GET';
this.asynch = true;
this.status = null;
this.statusText = '';
this.postData = null;
this.readyState = null;
this.responseText = null;
this.responseXML = null;
this.handleResp = null;
this.responseFormat = 'text',
// 'text', 'html', 'xml' or 'object'
this.mimeType = null;
} // End Constructor
//Create XMLHttpRequest method with XMLHttpRequest object
this.init = function() {
if (!this.req) {
try {
//Try to create objects for Firefox, Safari, IE7, etc.
this.req = newXMLHttpRequest();
}
catch(e) {
try {
//Try to create object for later versions of IE.
this.req = new ActiveXObject('MSXML2.XMLHTTP');
}
catch(e) {
try {
//Try to create for early versions of IE.
this.req = new ActiveXObject('Microsoft.XMLHTTP');
}
catch(e) {
//Could not create XMLHttpRequest object.
return false;
}
}
}
}
return this.req;
};
//Sending a Request method
this.doReq = function() {
if (!this.init()) {
alert('Could not create XMLHttpRequest object.');
return;
}
//Setting up a request
//open methods with method, url and asycn yes or no
this.req.open(this.method, this.url, this.asynch);
//Make sure mimetype is OK
if (this.mimeType) {
try {
req.overrideMimeType(this.mimeType);
}
catch(e) {
//couldn't override MIME type ... IE6 or Opera?
}
}
var self = this; // fix loss-of-scope in inner function
this.req.onreadystatechange = function() {
var resp = null;
if (self.req.readyState == 4) {
//do stuff to handle response
switch (self.reponseFormat) {
case 'text':
resp = self.req.responseText;
break;
case 'xml':
resp = self.req.responseXML;
break;
case 'object':
resp = req;
break;
}
if (self.req.status >= 200 && self.req.status <= 299) {
self.handleResp(resp);
}
else {
self.handleErr(resp);
}
}
};
this.req.send(this.postData);
};
this.handleErr = function() {
var errorWin;
try {
errorWin = window.open('', 'errorWin');
errorWin.document.body.innerHTML = this.responseText;
}
catch(e) {
alert('An error occured, but this error message cannot be '
+ 'displayed. This is probably because of your browser\'s '
+ 'pop-up blocker. \n'
+ 'Please allow pop-ups from this website if you want to '
+ 'see the full error messages. \n'
+ '\n'
+ 'Status Code: ' + this.req.status + '\n'
+ 'Status description: ' + this.req.statusText);
}
};
this.abort = function() {
if (this.req) {
this.req.onreadystatechange = function() {};
this.req.abort();
this.req = null;
}
};
this.doGet = function (url, hand, format) {
this.url = url;
this.handleResp = hand;
this.responseFormat = format || 'text' ;
this.doReq();
};
The error I get on the page that loads this script with
var hand = function (str) {
alert(str);
}
var Ajax = new Ajax(); // new instance as can ben done with PHP5 constructor classes
ajax.doGet ('/fakeserverpage.php', hand);
and starts up a new instance of Ajax get the error ajax is not defined even though I did add var self = this; // fix loss-of-scope in inner function
to fix the scope issue. What am I missing?
Update 1
Thanks to a tip here Gave new instance a different name so they don't clash:
var hand = function (str) {
alert(str);
}
var ajax = new Ajax(); // new instance as can ben done with PHP5 constructor classes
ajax.doGet ('/fakeserverpage.php', hand);
Now I am a little further. Now I get a new error: Uncaught TypeError: Object #<Ajax> has no method 'doGet'
Update 2
I tried using Ajax.prototype.init instead of this.init as recommended by a co dev here, but I still have the same error..
Update 3
Thanks to #Soufiana Hassou I improved the code by adding Ajax.prototype to all methods. Did not know it was necessary for all to work with the constructor, but it is. Code is here http://pastebin.com/g86k0z8d . I now get this pop-up saying Could not create XMLHttpRequest object. This error message is built into the method so it is working, but it cannot create the object somehow. This means there must be an error in my request for an XMLHttpRequest as I covered all cases and tested this in Firefox 11 for Mac using code on my local MacPorts MAMP. Either that or there is something else I do not know about..
Update 4
Fixed a typo. Then I got a 404 loading the fake server page. Corrected path ajax.doGet ('/ajax/fakeserverpage.php', hand); so now OK. Only I need to get the PHP to generate the code so I get an OK. The header response is OK, but I do not see the AJAX alert yet. Then I checked the console and found this error:
self.req is undefined
http://localhost/ajax/ajax.js
Line 78
See latest code: http://pastebin.com/g86k0z8d . I added some more Ajax.prototype where I thought they were still needed. Now I get:
this.req is null
http://localhost/ajax/ajax.js
Line 100
Update 5
Made some more changes removing some selfs used initially for the out-of-scope issue using var self = this. Code is still the same pastebin, but I have updated it. Now I have:
Ajax.prototype.handleResp is not a function
http://localhost/ajax/ajax.js
Line 92
Update 6
I cleaned up some of the mistakes I made in the req.onreadystatechange = function() function and now I does run. I turned of Firefox pop-up blocker for localhost and on reload it opened another tab and showed the text undefined. So almost there. No errors, just no pop-up with OK. Chrome showed a pop-up with the undefined in the body. Updated code here: http://pastebin.com/g86k0z8d as usual
You are using the same name for your instance and the class itself.
Also, you are declaring Ajax and using ajax, Javascript is case-sensitive.
First, you have var Ajax = new Ajax(); You should have var ajax = new Ajax(); instead.
Secondly, using this outside of the constructor isn't referring to the Ajax object. Try using its prototype instead:
function Ajax() {
// Set properties here
}
Ajax.prototype.init = function() {
// ...
}
See this article on Javascript classes for more information.
I have a javascript code on my website, there is a variable:
var remoteJsonVar;
On the other hand there is a json file on a remote website
https://graph.facebook.com/?ids=http://www.stackoverflow.com
I need to set the variable remoteJsonVar to this remote jason data.
I am sure that it is very simple, but I can't find the solution.
A small working example would be nice.
Because you're trying to get the data from a different origin, if you want to do this entirely client-side, you'd use JSON-P rather than just JSON because of the Same Origin Policy. Facebook supports this if you just add a callback parameter to your query string, e.g.:
https://graph.facebook.com/?ids=http://www.stackoverflow.com?callback=foo
Then you define a function in your script (at global scope) which has the name you give in that callback parameter, like this:
function foo(data) {
remoteJsonVar = data;
}
You trigger it by creating a script element and setting the src to the desired URL, e.g.:
var script = document.createElement('script');
script.src = "https://graph.facebook.com/?ids=http://www.stackoverflow.com?callback=foo";
document.documentElement.appendChild(script);
Note that the call to your function will be asynchronous.
Now, since you may want to have more than one outstanding request, and you probably don't want to leave that callback lying around when you're done, you may want to be a bit more sophisticated and create a random callback name, etc. Here's a complete example:
Live copy | Live source
(function() {
// Your variable; if you prefer, it could be a global,
// but I try to avoid globals where I can
var responseJsonVar;
// Hook up the button
hookEvent(document.getElementById("theButton"),
"click",
function() {
var callbackName, script;
// Get a random name for our callback
callbackName = "foo" + new Date().getTime() + Math.floor(Math.random() * 10000);
// Create it
window[callbackName] = function(data) {
responseJsonVar = data;
display("Got the data, <code>shares = " +
data["http://www.stackoverflow.com"].shares +
"</code>");
// Remove our callback (`delete` with `window` properties
// fails on some versions of IE, so we fall back to setting
// the property to `undefined` if that happens)
try {
delete window[callbackName];
}
catch (e) {
window[callbackName] = undefined;
}
}
// Do the JSONP request
script = document.createElement('script');
script.src = "https://graph.facebook.com/?ids=http://www.stackoverflow.com&callback=" + callbackName;
document.documentElement.appendChild(script);
display("Request started");
});
// === Basic utility functions
function display(msg) {
var p = document.createElement('p');
p.innerHTML = msg;
document.body.appendChild(p);
}
function hookEvent(element, eventName, handler) {
// Very quick-and-dirty, recommend using a proper library,
// this is just for the purposes of the example.
if (typeof element.addEventListener !== "undefined") {
element.addEventListener(eventName, handler, false);
}
else if (typeof element.attachEvent !== "undefined") {
element.attachEvent("on" + eventName, function(event) {
return handler(event || window.event);
});
}
else {
throw "Browser not supported.";
}
}
})();
Note that when you use JSONP, you're putting a lot of trust in the site at the other end. Technically, JSONP isn't JSON at all, it's giving the remote site the opportunity to run code on your page. If you trust the other end, great, but just remember the potential for abuse.
You haven't mentioned using any libraries, so I haven't used any above, but I would recommend looking at a good JavaScript library like jQuery, Prototype, YUI, Closure, or any of several others. A lot of the code above has already been written for you with a good library. For instance, here's the above using jQuery:
Live copy | Live source
jQuery(function($) {
// Your variable
var responseJsonVar;
$("#theButton").click(function() {
display("Sending request");
$.get("https://graph.facebook.com/?ids=http://www.stackoverflow.com&callback=?",
function(data) {
responseJsonVar = data;
display("Got the data, <code>shares = " +
data["http://www.stackoverflow.com"].shares +
"</code>");
},
"jsonp");
});
function display(msg) {
$("<p>").html(msg).appendTo(document.body);
}
});
I'm trying to intercept all AJAX calls in order to check if that AJAX response contains specific error code that I send as JSON from my PHP script (codes: ACCESS_DENIED, SYSTEM_ERROR, NOT_FOUND).
I know one can do something like this:
$('.log').ajaxSuccess(function(e, xhr, settings) {
});
But - does this work only if "ajaxSuccess" event bubble up to .log div? Am I correct? Can I achieve what I want by binding "ajaxSuccess" event to document?
$(document).ajaxSuccess(function(e, xhr, settings) {
});
I can do this in either jQuery or raw JavaScript.
If you're using jQuery, $.ajaxSuccess is a good option, but here's a generic option that will intercept XHR calls from all frameworks (I've tested it with ExtJS and jQuery - it should work even if multiple frameworks are loaded concurrently). It's been tested to work with IE8, Chrome and Firefox.
(function(XHR) {
"use strict";
var open = XHR.prototype.open;
var send = XHR.prototype.send;
XHR.prototype.open = function(method, url, async, user, pass) {
this._url = url;
open.call(this, method, url, async, user, pass);
};
XHR.prototype.send = function(data) {
var self = this;
var oldOnReadyStateChange;
var url = this._url;
function onReadyStateChange() {
if(self.readyState == 4 /* complete */) {
/* This is where you can put code that you want to execute post-complete*/
/* URL is kept in this._url */
}
if(oldOnReadyStateChange) {
oldOnReadyStateChange();
}
}
/* Set xhr.noIntercept to true to disable the interceptor for a particular call */
if(!this.noIntercept) {
if(this.addEventListener) {
this.addEventListener("readystatechange", onReadyStateChange, false);
} else {
oldOnReadyStateChange = this.onreadystatechange;
this.onreadystatechange = onReadyStateChange;
}
}
send.call(this, data);
}
})(XMLHttpRequest);
I've posted a more specific example on github which intercepts AJAX calls and posts the AJAX call durations back to the server for statistical analysis.
From http://api.jquery.com/ajaxSuccess/ :
Whenever an Ajax request completes successfully, jQuery triggers the ajaxSuccess event. Any and all handlers that have been registered with the .ajaxSuccess() method are executed at this time.
So the selector doesn't define the position where you are "catching" the event (because, honestly, ajax event by its nature doesn't start from a DOM element), but rather defines a scope to which the handling will be defaulted (i.e. this will poitn to that/those element(s)).
In summary - it should be exactly what you wish for
The best way, which I found https://lowrey.me/intercept-2/
const intercept = (urlmatch, callback) => {
let send = XMLHttpRequest.prototype.send;
XMLHttpRequest.prototype.send = function() {
this.addEventListener('readystatechange', function() {
if (this.responseURL.includes(urlmatch) && this.readyState === 4) {
callback(this);
}
}, false);
send.apply(this, arguments);
};
};
Try using Mockjax.js http://code.appendto.com/plugins/jquery-mockjax
It lets you hijack AJAX calls to the server and mock the location.