Related
I'm trying to develop a library that try to detect auto-click on a page.
The library will be imported on several different pages, some will have jquery, some other will not, or will have other different libraries, so my solution should be vanilla javascript.
the goal is to have several security layers, and the first one will be in javascript, this library will not be the only counter measure against auto-click, but should provide as much informations as possible.
The idea is to intercept all click and touch events that occur on the page, and if those events are script generated, something will happen (should be a ajax call, or setting a value on a form, or setting a cookie or something else, this is not important at this stage).
I've write a very simple script that checks for computer generated clicks:
(function(){
document.onreadystatechange = function () {
if (document.readyState === "interactive") {
try{
document.querySelector('body').addEventListener('click', function(evt) {
console.log("which", evt.which);
console.log("isTrusted", evt.isTrusted);
}, true); // Use Capturing
}catch(e){
console.log("error on addeventlistener",e);
}
}
}
}());
I saw this working on a html page without any other js in it, but since I added this javascript to test the auto-click detection simply "nothing" happens, and with nothing I mean both autoclick and detection.
The same code as follow, if used in the console, is working fine, and events are intercepted and evaulated.
this is the script used:
document.onreadystatechange = function () {
if (document.readyState === "interactive") {
//1 try
el = document.getElementById('target');
if (el.onclick) {
el.onclick();
} else if (el.click) {
el.click();
}
console.log("clicked")
}
//2 try
var d = document.createElement('div'); d.style.position = 'absolute'; d.style.top = '0'; d.style.left = '0'; d.style.width = '200px'; d.style.height = '200px'; d.style.backgroundColor = '#fff'; d.style.border = '1px solid black'; d.onclick = function() {console.log('hello');}; document.body.appendChild(d);
}
the html page is very simple:
<body>
<h1>Hello, world!</h1>
<div id="target"> aaaaa </div>
</body>
and for test purposes I added the detection library in head, while the "autoclick" code is just behind the </body> tag.
I guess the problem is in "how I attach the event handler", or "when", so what I'm asking is what can I do to intercept clicks events "for sure", the idea is to intercept clicks on every element, present and future, I don't want to prevent them, just be sure to intercept them somehow.
Of course I cannot intercept those events that has been prevented and do not bubble, but I'd like to "try" to have my js "before" any other.
Do you have some idea about this?
jsfiddle of example
Using document.onreadystatechange will only work as expected in simple scenerios when no other third party libraries are included. Wrap you code inside the native DOMContentLoaded event.
document.addEventListener("DOMContentLoaded",function(){
document.body.addEventListener('click', function(evt) {
if (evt.target.classList.contains('some-class')) {} // not used
console.log("which", evt.which);
console.log("isTrusted", evt.isTrusted);
}, true);
//this is the autoclick code!
el = document.getElementById('target');
if (el.onclick) {
el.onclick();
} else if (el.click) {
el.click();
}
console.log("clicked")
});
<html>
<head>
<title>Hello, world!</title>
</head>
<body>
<h1>Hello, world!</h1>
<div id="target"> aaaaa </div>
</body>
</html>
If you look at the event param passed to the function on a click, or whatever other event, you can look for the following which is a telltale sign that the clicker ain't human...
event.originalEvent === undefined
From what you've said I'd use the following to track clicks...
$(document).on("click", function(event){
if(event.originalEvent.isTrusted){
//not human
}else{
//human
}
});
Can you check if both a click event and either a mouseup or touchend event happen within 100 ms of each other? If they don't it's likely an automated event.
let mouseuportouchend = false;
let click = false;
let timer = null;
const regMouseupOrTouchend = function(){
mouseuportouchend = true;
if (!timer) timer = setTimer();
}
const regClick = function(){
click = true;
if (!timer) timer = setTimer();
}
const setTimer = function(){
timer = setTimeout(()=>{
if (click && mouseuportouchend) console.log("Manual");
else console.log ("Auto");
click=false;
mouseuportouchend=false;
timer=null;
}, 100)
}
let el = document.getElementById('target');
el.addEventListener("click",regClick);
el.addEventListener("mouseup", regMouseupOrTouchend);
el.addEventListener("touchend", regMouseupOrTouchend);
In IE, we can listen to onreadystatechange event to know when document.write changes iframe's content. But in Chrome, it doesn't work.
<html>
<script>
function loadFrame() {
var ifr = document.getElementById("iframeResult");
var ifrw = (ifr.contentWindow) ? ifr.contentWindow : (ifr.contentDocument.document) ? ifr.contentDocument.document : ifr.contentDocument;
ifrw.document.open();
ifrw.document.write("<input type='submit' />");
ifrw.document.close();
}
</script>
<body onload="loadFrame();">
<div><input type="submit" value="Reload Frame" onclick="loadFrame()" /></div>
<div>
<iframe frameborder="0" id="iframeResult" style="background-color:red;" onreadystatechange="console.log('ready state changed');">
</iframe>
</div>
</body>
<html>
In above code, when click Reload Frame button on IE, console outputs ready state changed, but in Chrome, it doesn't output anything.
How should we do in Chrome to know when document.write changes iframe's content?
EDIT:
Gideon is right, we can listen to onload event in Chrome. But if I comment document.open and document.close two lines, onload doesn't work any more. Does anybody have a solution to this?
You can add the load event listener, which will be triggered when the iFrame is modified by the page.
document.getElementById("iframeResult").addEventListener("load", function(){
console.log("iFrame has been loaded.");
});
I think I have come up with a solution to your problem. It involves using the MutationObserver API, in order to detect changes to the iFrame's DOM.
MutationObserver provides developers a way to react to changes in a
DOM. It is designed as a replacement for Mutation Events defined in
the DOM3 Events specification.
I also used the window.postMessage API to notify the parent page when the MutationObserver has detected DOM events, so as to allow the parent to respond.
I have created a simple example below. Please note that I have used * for origin, but it is recommended that you do origin checks for security reasons. Also note that Chrome doesn't allow frames to access other frames in the local file system, but it will work on a web server or you can test locally using FireFox, which doesn't have that restriction.
iframe.html
<head>
<meta charset="utf-8">
</head>
<body>
<script>
var observer = new MutationObserver(function(mutations) {
mutations.forEach(function(mutation) {
if (mutation.type == 'childList') {
if (mutation.addedNodes.length >= 1) {
if (mutation.addedNodes[0].nodeName != '#text') {
window.parent.postMessage("DOMChanged", "*");
}
} else if (mutation.removedNodes.length >= 1) {
window.parent.postMessage("DOMChanged", "*");
}
} else if (mutation.type == 'attributes') {
window.parent.postMessage("DOMChanged", "*");
}
});
});
var observerConfig = {
attributes: true,
childList: true,
characterData: true
};
// listen to all changes to body and child nodes
var targetNode = document.body;
observer.observe(targetNode, observerConfig);
</script>
</body>
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
</head>
<body>
<div>
<input type="submit" value="Update iFrame" onclick="updateiFrameDOM()" />
</div>
<iframe src="iframe.html" id="iframeResult"></iframe>
<script>
function updateiFrameDOM() {
var ifr = document.getElementById("iframeResult");
var ifrw = (ifr.contentWindow) ? ifr.contentWindow : (ifr.contentDocument.document) ? ifr.contentDocument.document : ifr.contentDocument;
var div = document.createElement("div");
var text = document.createTextNode("Hello");
div.appendChild(text);
var body = ifrw.document.getElementsByTagName("body")[0];
body.appendChild(div);
}
// Create IE + others compatible event handler
var eventMethod = window.addEventListener ? "addEventListener" : "attachEvent";
var eventer = window[eventMethod];
var messageEvent = eventMethod == "attachEvent" ? "onmessage" : "message";
// Listen to message from child window
eventer(messageEvent, function(e) {
console.log(e.data);
}, false);
</script>
</body>
</html>
Some additional sources I used:
Respond to DOM Changes with Mutation Observers
window.postMessage Tip: Child-To-Parent Communication
I hope this helps you.
This code does the trick:
<script>
var observeDOM = (function(){
var MutationObserver = window.MutationObserver || window.WebKitMutationObserver,
eventListenerSupported = window.addEventListener;
return function(obj, callback){
if( MutationObserver ){
// define a new observer
var obs = new MutationObserver(function(mutations, observer){
if( mutations[0].addedNodes.length || mutations[0].removedNodes.length )
callback();
});
// have the observer observe foo for changes in children
obs.observe( obj, { childList:true, subtree:true });
}
else if( eventListenerSupported ){
obj.addEventListener('DOMNodeInserted', callback, false);
obj.addEventListener('DOMNodeRemoved', callback, false);
}
}
})();
window.onload = function() {
// Observe a specific DOM element:
observeDOM( document.getElementById("iframeResult").contentDocument ,function(){
console.log('dom changed');
});
}
function reload() {
document.getElementById("iframeResult").contentDocument.write("<div>abc</div>");
}
</script>
<body>
<input type="submit" onclick="reload();" value="Reload" />
<iframe id="iframeResult"></iframe>
</body>
Be aware of that, observer must be added to document, not document.body. Because first call to document.write() will auto call document.open(), it will cover old document.body with a new one.
I'm trying to check connect with javascript, but I have trying a lot of tutorials but any works for me.
For example I've this code but not works
<!DOCTYPE HTML>
<html>
<head>
<title>Online status</title>
<script>
function updateIndicator()
{
document.getElementById('indicator').textContent = navigator.onLine ? 'online' : 'offline';
}
</script>
</head>
<body onload="updateIndicator()" ononline="updateIndicator()" onoffline="updateIndicator()"> <p>The network is: <span
id="indicator">(state unknown)</span>
</body>
</html>
Some help? thanks.
EDIT: Seems like it was a setup problem, will leave this anyway as it could be useful in cases where ononline and onoffline are not supported.
I am not sure ononline and onoffline are going to be well supported, but you can just set an interval to check for the status and update it accordingly. This little fiddle works (just stop your network connection and the status will switch to offline)
http://jsfiddle.net/vpG5b/1/
You can adjust the interval as needed but 500 ms should be good enough and not be too demanding and I added check to only update the status if it has changed (less DOM manipulations), that way you can even have offline/online handlers.
var handler = {
online: function() {
alert('online');
},
offline: function() {
alert('offline');
}
};
function isOnline() {
var status = navigator.onLine ? 'online' : 'offline',
indicator = document.getElementById('indicator'),
current = indicator.textContent;
// only update if it has change
if (current != status) {
// update DOM
indicator.textContent = status;
// trigger handler
handler[status]();
};
};
setInterval(isOnline, 500);
isOnline();
I am writing an internal framework that can have no dependencies (i.e. jQuery, etc.) and am trying to implement my own DOM ready-style functionality. It seems that when a callback in the ready queue (array of callbacks to complete on DOM ready), if an exception is thrown inside that function, execution stops and continues onto the next callback (which is what I want), but Firefox will not report the error (log to the console, trigger onerror, anything). Am I doing something wrong?
I have implemented this using a combination of a pattern by Dean Edwards (http://dean.edwards.name/weblog/2009/03/callbacks-vs-events/) and the jQuery source. I do not wish to implement just like jQuery because if one callback fails, the subsequent callbacks won't execute.
var readyCallbacks = [];
(function () {
var currentHandler,
fireReady,
removeEvents,
loopCallbacks = function () {
for (var i = 0, len = readyCallbacks.length; i < len; i += 1) {
currentHandler = readyCallbacks[i];
fireReady();
}
};
if (document.addEventListener) {
document.addEventListener('readyEvents', function () { currentHandler(); }, false);
fireReady = function () {
var readyEvent = document.createEvent('UIEvents');
readyEvent.initEvent('readyEvents', false, false);
document.dispatchEvent(readyEvent);
};
removeEvents = function () {
window.removeEventListener('load', loopCallbacks, false);
loopCallbacks();
};
document.addEventListener('DOMContentLoaded', removeEvents, false);
window.addEventListener('load', loopCallbacks, false);
} else {
// if < IE 9
document.documentElement.readyEvents = 0;
document.documentElement.attachEvent('onpropertychange', function (e) {
if (e.propertyName === 'readyEvents')
currentHandler();
});
fireReady = function () {
document.documentElement.readyEvents += 1;
};
removeEvents = function () {
window.detachEvent('onload', loopCallbacks);
loopCallbacks();
};
document.attachEvent('onreadystatechange', removeEvents);
window.attachEvent('onload', loopCallbacks);
}
})();
Client.ready = function (callback) {
readyCallbacks.push(callback);
};
Here is my test implementation. The console is written to and the DOM element has been manipulated. In IE error with the UNDEFINED_VARIABLE++; is shown in the console, but not in Firefox.
<!DOCTYPE html>
<html>
<head>
<script src="Client.js"></script>
<script>
Client.ready(function () {
console.log('logging before error');
UNDEFINED_VARIABLE++; // This does not error in Firefox
console.log('logging after error, not logged in Firefox');
});
Client.ready(function () {
console.log('before DOM access');
document.getElementById('cheese').innerHTML = 'cheese';
console.log('after DOM access');
});
</script>
</head>
<body>
<div id="cheese">test</div>
</body>
</html>
After some searching, this appears to be a known Mozilla bug: Errors thrown by handlers for custom events dispatched via dispatchEvent() are not logged properly in some versions of Firefox 3.x. This isn't a problem in your code, but in the browser implementation. If it's a real problem for you, you can wrap the handler in a try/catch block to identify and deal with errors:
document.addEventListener('readyEvents', function() {
try { currentHandler() } catch (e) { console.log(e) }
}, false);
I have a jsFiddle here that demonstrates the problem with a simplified set of tests. Firefox correctly logs errors on handlers for built-in events like DOMContentLoaded and load, but misses the error when firing a handler for a custom event.
It's hard to get it right. See the jQuery source code and search for ready to see how it is implemented in a portable way. Keep in mind that you have to make sure that when you bind ready handlers after the dom is already ready then you have to call them straight away or else they won't be called at all. See also this question.
I'm writing a Greasemonkey script for a site which at some point modifies location.href.
How can I get an event (via window.addEventListener or something similar) when window.location.href changes on a page? I also need access to the DOM of the document pointing to the new/modified url.
I've seen other solutions which involve timeouts and polling, but I'd like to avoid that if possible.
I use this script in my extension "Grab Any Media" and work fine ( like youtube case )
var oldHref = document.location.href;
window.onload = function() {
var bodyList = document.querySelector("body")
var observer = new MutationObserver(function(mutations) {
mutations.forEach(function(mutation) {
if (oldHref != document.location.href) {
oldHref = document.location.href;
/* Changed ! your code here */
}
});
});
var config = {
childList: true,
subtree: true
};
observer.observe(bodyList, config);
};
With the latest javascript specification
const observeUrlChange = () => {
const oldHref = document.location.href;
const body = document.querySelector("body");
const observer = new MutationObserver(mutations => {
mutations.forEach(() => {
if (oldHref !== document.location.href) {
oldHref = document.location.href;
/* Changed ! your code here */
}
});
});
observer.observe(body, { childList: true, subtree: true });
};
window.onload = observeUrlChange;
Compressed with OpenAI
window.onload = () => new MutationObserver(mutations => mutations.forEach(() => oldHref !== document.location.href && (oldHref = document.location.href, /* Changed ! your code here */))).observe(document.querySelector("body"), { childList: true, subtree: true });
popstate event:
The popstate event is fired when the active history entry changes. [...] The popstate event is only triggered by doing a browser action such as a click on the back button (or calling history.back() in JavaScript)
So, listening to popstate event and sending a popstate event when using history.pushState() should be enough to take action on href change:
window.addEventListener('popstate', listener);
const pushUrl = (href) => {
history.pushState({}, '', href);
window.dispatchEvent(new Event('popstate'));
};
You can't avoid polling, there isn't any event for href change.
Using intervals is quite light anyways if you don't go overboard. Checking the href every 50ms or so will not have any significant effect on performance if you're worried about that.
There is a default onhashchange event that you can use.
Documented HERE
And can be used like this:
function locationHashChanged( e ) {
console.log( location.hash );
console.log( e.oldURL, e.newURL );
if ( location.hash === "#pageX" ) {
pageX();
}
}
window.onhashchange = locationHashChanged;
If the browser doesn't support oldURL and newURL you can bind it like this:
//let this snippet run before your hashChange event binding code
if( !window.HashChangeEvent )( function() {
let lastURL = document.URL;
window.addEventListener( "hashchange", function( event ) {
Object.defineProperty( event, "oldURL", { enumerable: true, configurable: true, value: lastURL } );
Object.defineProperty( event, "newURL", { enumerable: true, configurable: true, value: document.URL } );
lastURL = document.URL;
} );
} () );
Through Jquery, just try
$(window).on('beforeunload', function () {
//your code goes here on location change
});
By using javascript:
window.addEventListener("beforeunload", function (event) {
//your code goes here on location change
});
Refer Document : https://developer.mozilla.org/en-US/docs/Web/Events/beforeunload
Have you tried beforeUnload?
This event fires immediately before the page responds to a navigation request, and this should include the modification of the href.
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
<HTML>
<HEAD>
<TITLE></TITLE>
<META NAME="Generator" CONTENT="TextPad 4.6">
<META NAME="Author" CONTENT="?">
<META NAME="Keywords" CONTENT="?">
<META NAME="Description" CONTENT="?">
</HEAD>
<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.3/jquery.min.js" type="text/javascript"></script>
<script type="text/javascript">
$(document).ready(function(){
$(window).unload(
function(event) {
alert("navigating");
}
);
$("#theButton").click(
function(event){
alert("Starting navigation");
window.location.href = "http://www.bbc.co.uk";
}
);
});
</script>
<BODY BGCOLOR="#FFFFFF" TEXT="#000000" LINK="#FF0000" VLINK="#800000" ALINK="#FF00FF" BACKGROUND="?">
<button id="theButton">Click to navigate</button>
Google
</BODY>
</HTML>
Beware, however, that your event will fire whenever you navigate away from the page, whether this is because of the script, or somebody clicking on a link.
Your real challenge, is detecting the different reasons for the event being fired. (If this is important to your logic)
Try this script which will let you run code whenever the URL changes (without a pageload, like an Single Page Application):
var previousUrl = '';
var observer = new MutationObserver(function(mutations) {
if (location.href !== previousUrl) {
previousUrl = location.href;
console.log(`URL changed to ${location.href}`);
}
});
based on the answer from "Leonardo Ciaccio", modified code is here:
i.e. removed for loop and reassign the Body Element if it is removed
window.addEventListener("load", function () {
let oldHref = document.location.href,
bodyDOM = document.querySelector("body");
function checkModifiedBody() {
let tmp = document.querySelector("body");
if (tmp != bodyDOM) {
bodyDOM = tmp;
observer.observe(bodyDOM, config);
}
}
const observer = new MutationObserver(function (mutations) {
if (oldHref != document.location.href) {
oldHref = document.location.href;
console.log("the location href is changed!");
window.requestAnimationFrame(checkModifiedBody)
}
});
const config = {
childList: true,
subtree: true
};
observer.observe(bodyDOM, config);
}, false);
Well there is 2 ways to change the location.href. Either you can write location.href = "y.html", which reloads the page or can use the history API which does not reload the page. I experimented with the first a lot recently.
If you open a child window and capture the load of the child page from the parent window, then different browsers behave very differently. The only thing that is common, that they remove the old document and add a new one, so for example adding readystatechange or load event handlers to the old document does not have any effect. Most of the browsers remove the event handlers from the window object too, the only exception is Firefox. In Chrome with Karma runner and in Firefox you can capture the new document in the loading readyState if you use unload + next tick. So you can add for example a load event handler or a readystatechange event handler or just log that the browser is loading a page with a new URI. In Chrome with manual testing (probably GreaseMonkey too) and in Opera, PhantomJS, IE10, IE11 you cannot capture the new document in the loading state. In those browsers the unload + next tick calls the callback a few hundred msecs later than the load event of the page fires. The delay is typically 100 to 300 msecs, but opera simetime makes a 750 msec delay for next tick, which is scary. So if you want a consistent result in all browsers, then you do what you want to after the load event, but there is no guarantee the location won't be overridden before that.
var uuid = "win." + Math.random();
var timeOrigin = new Date();
var win = window.open("about:blank", uuid, "menubar=yes,location=yes,resizable=yes,scrollbars=yes,status=yes");
var callBacks = [];
var uglyHax = function (){
var done = function (){
uglyHax();
callBacks.forEach(function (cb){
cb();
});
};
win.addEventListener("unload", function unloadListener(){
win.removeEventListener("unload", unloadListener); // Firefox remembers, other browsers don't
setTimeout(function (){
// IE10, IE11, Opera, PhantomJS, Chrome has a complete new document at this point
// Chrome on Karma, Firefox has a loading new document at this point
win.document.readyState; // IE10 and IE11 sometimes fails if I don't access it twice, idk. how or why
if (win.document.readyState === "complete")
done();
else
win.addEventListener("load", function (){
setTimeout(done, 0);
});
}, 0);
});
};
uglyHax();
callBacks.push(function (){
console.log("cb", win.location.href, win.document.readyState);
if (win.location.href !== "http://localhost:4444/y.html")
win.location.href = "http://localhost:4444/y.html";
else
console.log("done");
});
win.location.href = "http://localhost:4444/x.html";
If you run your script only in Firefox, then you can use a simplified version and capture the document in a loading state, so for example a script on the loaded page cannot navigate away before you log the URI change:
var uuid = "win." + Math.random();
var timeOrigin = new Date();
var win = window.open("about:blank", uuid, "menubar=yes,location=yes,resizable=yes,scrollbars=yes,status=yes");
var callBacks = [];
win.addEventListener("unload", function unloadListener(){
setTimeout(function (){
callBacks.forEach(function (cb){
cb();
});
}, 0);
});
callBacks.push(function (){
console.log("cb", win.location.href, win.document.readyState);
// be aware that the page is in loading readyState,
// so if you rewrite the location here, the actual page will be never loaded, just the new one
if (win.location.href !== "http://localhost:4444/y.html")
win.location.href = "http://localhost:4444/y.html";
else
console.log("done");
});
win.location.href = "http://localhost:4444/x.html";
If we are talking about single page applications which change the hash part of the URI, or use the history API, then you can use the hashchange and the popstate events of the window respectively. Those can capture even if you move in history back and forward until you stay on the same page. The document does not changes by those and the page is not really reloaded.
ReactJS and other SPA applications use the history object
You can listen to window.history updating with the following code:
function watchHistoryEvents() {
const { pushState, replaceState } = window.history;
window.history.pushState = function (...args) {
pushState.apply(window.history, args);
window.dispatchEvent(new Event('pushState'));
};
window.history.replaceState = function (...args) {
replaceState.apply(window.history, args);
window.dispatchEvent(new Event('replaceState'));
};
window.addEventListener('popstate', () => console.log('popstate event'));
window.addEventListener('replaceState', () => console.log('replaceState event'));
window.addEventListener('pushState', () => console.log('pushState event'));
}
watchHistoryEvents();