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!