Showing posts with label greasemonkey. Show all posts
Showing posts with label greasemonkey. Show all posts

Monday, October 12, 2009

Callback Functions - Scoping

According to Wikipedia, a callback function is executable code that is passed as an argument to other code. It allows a lower-level software layer to call a subroutine (or function) defined in a higher-level layer.

Callback functions are used very frequently in event driven software systems. Whenever an event happens, a function is called.

The idea is simple, but many people tend to become confused when using these things. The problem that they encounter is that people tend to run into problems with the scope of variables that they are using.

Part of the problem is remembering that the callback function may form a closure. When the callback function is executed, the scope consists of the callback function's code and references to any variables of the function containing the callback function reference. The converse is not true; the function containing the callback function reference cannot use the arguments and variables of the callback function.

Another thing that can confuse people using callback functions is the use of global variables. Since callbacks are usually called in response to events, it may not be clear what the current state of the system is and using any predefined global variable may result in inconsistent results.

In addition to global variables, variables using the same name in the code containing the reference to the callback function and with in the callback can sometimes confusing to people. In this case, the variable within the callback takes precedence.

# example function with closure

function makePowerTo(x) {
return function(y) {
var result = y;
for (var i = 1; i < x;i++)
result *= y;
return result;
};
}
var square = makePowerTo(2);
var cube = makePowerTo(3);
print(square(2)); // 4
print(cube(2)); // 8

square
and cube are both closures. They share the same function body definition, but store different environments. In square's environment, x is 2. As far as cube is concerned, x is 3.

Friday, August 24, 2007

Greasemonkey & Auto Updating Userscripts With Subversion

Some time ago, I wanted to create a way of auto updating my Greasemonkey userscripts so that I wouldn't have to somehow manually notify everyone when an update occured.


My scripts are kept in a subversion repository with WebDav support. So one of the first things that I did was examine the HTTP headers of my web server when I made a subversion request from my browser:



Date Fri, 24 Aug 2007 16:00:53 GMT
Server Apache/2.0.54 (Unix) DAV/2 SVN/1.2.0 mod_perl/1.999.22 Perl/v5.8.5
Etag "1390//some_script.user.js"
Accept-Ranges bytes
Content-Length 638
Keep-Alive timeout=15, max=99
Connection Keep-Alive
Content-Type text/html; charset=UTF-8

The one thing to notice is the line with the Etag text. It pretty much displays the subversion version of my script, 'some_script.user.js'. So if I could get this information when my script loads and compare it to any value that I have stored previously, then I can cause my script to update itself quite easily!


So I created a function called check4Update(options) that did just that. In addition to my function, I also used cookie management code that I slighltly modified from webreference.com (included below).


The function that I created has a json object called options with the following parameters:




    • url - the subversion URL for your userscript

    • scriptname - the name of your script [defaults to the domain that the script acts upon; this may not be desirable, so please set it!]

    • delay - the number of page refreshes to wait before nagging the user again [ defaults to 7]


    Items in bold are required and items in italics are optional



 
window.check4Update = function(options) {
var options = options || {};
if (!url) {
// we need this at least ...
return;
}
var url = options.url;
var scriptname = options.scriptname || location.host + "_script";
var delay = options.delay || 7;

// cookie_name is the name of the cookie that we will set with the
// value of version
var cookie_name = scriptname

// the version of the script. Initially, this will be null.
var version = getCookie(cookie_name);

// the name of the cookie that indicates when we should nag
// about upgrading in case the user didnt want to upgrade their script
var upgrade_cookie = scriptname+ '_DELAY_UPGRADE'

try {
GM_xmlhttpRequest({
// head command because we are only interested
// in the Headers and not the script!
method: 'HEAD',
url: url,
headers: {
'User-agent': 'Mozilla/4.0 (compatible) Greasemonkey '+scriptname+'/1.0'
},
onload: function(responseDetails) {
if (responseDetails.status == 200 || responseDetails.status == 202) {
var headers = (responseDetails.responseHeaders).split('\n');
for (var i in headers) {

// go through and look for 'Etag'
if (headers[i].indexOf('Etag') != -1) {
var s = headers[i];

// create a regular expression to parse the version
var regx = /^.*Etag: \"(\d+)\/\/.*\"$/;
regx = regx.exec(s);

// here s will be our version obtained from the svn repository
s = regx[1]

// here version would be a previously set version or null
// if this is the first time running the script
if (version) {
// here we get the nag count
var count = getCookie(upgrade_cookie)

// if the nag count exists and is greater than 0, then decrement it
// and dont prompt to upgrade
if (count && count-- > 0) {
// user doesnt want to be notified
var now = new Date();
window.fixDate(now);
now.setTime(now.getTime() + 31*24*60*60*1000); // expire in a month
deleteCookie(upgrade_cookie);
setCookie(upgrade_cookie, count, now,"/", scriptname + ".com");
GM_log("Wont nag for " + count +" more reloads ....");
return;
}

// so we have found a newer version of the script prompt to upgrade
if (s > version) {
// ask the user if they wish to upgrade
// if they choose not to, then set a cookie to wait 'delay' page
// refreshes and then notify them to upgrade again
if (confirm("A newer version of this script exists!\n\tRemote Version: " +s+"\n\tYour Version: "+version+"\nWould you like to upgrade now?")) {
setCookie(upgrade_cookie, -1, null, "/", scriptname + ".com");
var now = new Date();
window.fixDate(now);
now.setTime(now.getTime() + 31*24*60*60*1000); // expire in a month
deleteCookie(cookie_name,"/", scriptname + ".com");
setCookie(cookie_name, s, now,"/", scriptname + ".com");
alert("Please reload the page once the script has been updated!")
// here we cause the page to refresh with the location of our script
// this will cause greasemonkey to prompt the user to install the script
location.replace(url)
} else {
// set a cookie to upgrade later
var now = new Date();
window.fixDate(now);
now.setTime(now.getTime() + 31*24*60*60*1000); // expire in a month
deleteCookie(upgrade_cookie,"/", scriptname + ".com");
setCookie(upgrade_cookie, delay, now,"/", scriptname + ".com");
GM_log("Won't notify of upgrades for a "+delay+" page refreshes!");
}
}
} else {
// version wasnt previously set so we set the version in the cookie to
// be the current version of the script in svn repository
var now = new Date();
window.fixDate(now);
now.setTime(now.getTime() + 31*24*60*60*1000); // expire in a month
deleteCookie(cookie_name,"/", scriptname + ".com");
setCookie(cookie_name, s, now,"/", scriptname + ".com");
}
}
}
}
},
onerror: function(responseDetails) {
console.log('GM_xmlhttpRequest error: %s', responseDetails.responseText)
},
onreadystatechange: function(responseDetails) {

}
});
} catch (exception){
console.log('Error occurred while updating the script %s . The error was: %s', scriptname, exception);
}

}

The code above is commented to indicate the logic behind it. If you need more information, please add a comment and I will explain any discrepancies. Also, if you can think of a better way of doing things, please let me know!


Below are the cookie management functions:



window.setCookie = function (name, value, expires, path, domain, secure) {
var curCookie = name + "=" + escape(value) +
((expires) ? "; expires=" + expires.toGMTString() : "") +
((path) ? "; path=" + path : "") +
((domain) ? "; domain=" + domain : "") +
((secure) ? "; secure" : "");

document.cookie = curCookie;
}


window.getCookie = function (name) {
var dc = document.cookie;
var prefix = name + "=";
var begin = dc.indexOf("; " + prefix);
if (begin == -1) {
begin = dc.indexOf(prefix);
if (begin != 0) return null;
} else
begin += 2;
var end = document.cookie.indexOf(";", begin);
if (end == -1)
end = dc.length;

return unescape(dc.substring(begin + prefix.length, end));
}


window.deleteCookie = function (name, path, domain) {
if (getCookie(name)) {
document.cookie = name + "=" +
((path) ? "; path=" + path : "") +
((domain) ? "; domain=" + domain : "") +
"; expires=Thu, 01-Jan-70 00:00:01 GMT";
}
}


window.fixDate = function (date) {
var base = new Date(0);
var skew = base.getTime();
if (skew > 0)
date.setTime(date.getTime() - skew);
}


This is how one could call the this function from within their userscript.



window.check4Update({'url':'http://someurl.com/my_script.user.js', 'scriptname':'my_script_name', 'delay':7});

Finally, all of the code mentioned above was placed in my Greasemonkey userscript at the end of the onload event handler:



window.addEventListener('load',
function () {
//... your user script code here ... //

// ... the auto update code here ... //
}
,true);


Good luck!