$dt->blog();

Messenger Plus Script update checking tutorial

Category: code | Posted By: dt | Posted On: 11/11/2007
Gravatar
This is a tutorial on Script version checking and updating
tutorial by -dt- (Matt Labrum)


So you've made a script, and you want to make sure your users have the most current version of your script.
Well you've come to the right place. This tutorial is about exactly that!

Now I'm going to assume a few things
1) you're not a complete noob
2) you're not segosa
3) you have a script (If you don't just create a new one)
4) the script must have a ScriptInfo.xml file
5) somewhere online where you can upload files

Now that thats out of the way, we can get to the fun part :d

A few reference documents for later:
XMLHttpRequest - http://msdn2.microsoft.com/en-us/library/ms535874.aspx
XMLDomDocument - http://msdn2.microsoft.com/en-us/library/ms757878.aspx


Now open up your ScriptInfo.xml file it should look something like the one shown below


Code:
<ScriptInfo xmlns="urn:msgplus:scripts" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 xsi:schemaLocation="urn:msgplus:scripts PlusScripts.xsd">
	<Information>
		<Name>updateExample</Name>
		<Description>This script is an example script to test update checking</Description>
		<AboutUrl>http://blog.thedt.net</AboutUrl>
		<Version>1.00</Version>
	</Information>
</ScriptInfo>



You should notice the Version tag, this is one of the core tags needed in our update checker. You should also notice that there aren't any tags to specify an update location.
This is a problem since our update checker needs to find the new version information somewhere, so lets add a <Update> section in our ScriptInfo.xml and add some updater tags



Code:
<ScriptInfo xmlns="urn:msgplus:scripts" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 xsi:schemaLocation="urn:msgplus:scripts PlusScripts.xsd">
	<Information>
		<Name>updateExample</Name>
		<Description>This script is an example script to test update checking</Description>
		<AboutUrl>http://blog.thedt.net</AboutUrl>
		<Version>1.00</Version>
		<Update>
			<ScriptInfoUrl>http://yoursite.com/updateExample/ScriptInfo.xml</ScriptInfoUrl>
			<PlscLocation>http://yoursite.com/updateExample/updateExample.plsc</PlscLocation>
			<ReleaseNotes>
			- First release
			</ReleaseNotes>
			<DateReleased>10/11/2007</DateReleased>
		</Update>
	</Information>
</ScriptInfo>



Ok so now we have alot more information for our script to use, but I bet you're looking at me like "dt what the hell are all those tags" well :( I'll explain each one

ScriptInfoUrl - the location of the updated scriptInfo.xml (our checker will download this file and compare it to the current one)
PlscLocation - Plsc to download if our update checker detects that we need updating
ReleaseNotes - optional, the changelog to display to the user
DateReleased - optional, the date the new version was released


Now i bet you're wondering how the update checker will work. Well, inside your script's OnEvent_Initialize you'll trigger UpdateChecker.Check(), which will then download the update xml (gets this value from Update/ScriptInfoUrl).
Once the updateXML is downloaded it will trigger UpdateChecker.parseUpdate() which then compares the local ScriptInfo.xml <version> tag with the downloaded ScriptInfo.xml <version> tag.
If it's a newer version then the script will display a dialog to the user asking them if they want to update, if they say yes then your script will download the plsc file and run it.

Diagram of how all this fits together

(click for a bigger image)

Now to make the javascript for the above process

Create a new file called UpdateChecker.js
Inside that file we'll create an Object called UpdateChecker, with the functions we need


Code:
var UpdateChecker = function(){
	this.Check();
};

UpdateChecker.prototype = {
	"Check" : function(){}, //This function will parse the local XML file and trigger the download the update ScriptInfo.xml
	"ParseScriptInfo" : function(xml){}, //returns an object with the parsed ScriptInfo information 
	"AskUser" : function(){},
	"GetNewPlsc" : function(){}
}


Now we need to start filling the functions and making it do something :d

We will start with the checking code

Code:

	"Check" : function(){ //This function will parse the local XML file and trigger the download the update ScriptInfo.xml
		var scriptInfo = this.ParseScriptInfo(); //will return an object in the form of {"Version" : 1.0, "UpdateUrl" : "http://.../scriptInfo.xml", "ReleaseNotes" : "", "DateReleased" : ""}
		this.details = scriptInfo; //add the details to the object, incase the script wants to use this for something
		
		Interop.Call("Wininet.dll", "DeleteUrlCacheEntryW", scriptInfo.UpdateUrl); //make sure theres no cached version of the update
		
		var xmlhttp = new ActiveXObject("Microsoft.XMLHTTP"); // Create a new XMLHttp request object to download our update ScriptInfo.xml
		
		// see http://msdn2.microsoft.com/en-us/library/ms536648.aspx for details on the open method
		xmlhttp.open("GET", scriptInfo.UpdateUrl, true) //open a new xmlhttp request to the updateUrl and set it to asynchronous operation
		
		
		var thisd = this; //create a reference to the current scope to use in the callback
		
		//see http://msdn2.microsoft.com/en-us/library/ms534308.aspx for more information on the onreadystatechange callback
		xmlhttp.onreadystatechange = function(){
			if(xmlhttp.readyState == 4){
				var updateInfo = thisd.ParseScriptInfo(xmlhttp.responseXML); //parses the update scriptInfo and returns an object

				if(updateInfo.Version > scriptInfo.Version){ //if version is newer
					Debug.Trace("Update is newer");
				}else{
					Debug.Trace("Update Is older");
				}
			}
		}
		xmlhttp.send();
	}


Now this can't do anything on its own because it requires the object from ParseScriptInfo, so we will implement that and then test the script


Code:

	"ParseScriptInfo" : function(xml){ //returns an object with the parsed ScriptInfo information 
		if(typeof(xml) == "undefined"){ //if no xml is passed, we will load the local script info
			var xml =  new ActiveXObject("Microsoft.XMLDOM");
			xml.async = false;
			xml.load(MsgPlus.ScriptFilesPath + "\\" + "ScriptInfo.xml");
		}
	
	
		//object to return
		var ob = {
			"Version" : 0,
			"UpdateUrl" : "",
			"PlscLocation" : "",
			"ReleaseNotes" : "",
			"DateReleased" : ""
		};
	
		//get the Update node values
		ob.UpdateUrl = xml.selectSingleNode("//Update/ScriptInfoUrl").text;
		ob.PlscLocation = xml.selectSingleNode("//Update/PlscLocation").text;
		ob.ReleaseNotes = xml.selectSingleNode("//Update/ReleaseNotes").text;
		ob.DateReleased = xml.selectSingleNode("//Update/DateReleased").text;
			
		var version = xml.selectSingleNode("//Version").text;
		//convert the version to a real number, parseInt chops off the decimal points so we cheat and * it by 1
		ob.Version = version * 1;
		
		return ob; //return our object
	}



The full code so far


Code:
var UpdateChecker = function(){
	this.Check();
};

UpdateChecker.prototype = {
	"Check" : function(){ //This function will parse the local XML file and trigger the download the update ScriptInfo.xml
		var scriptInfo = this.ParseScriptInfo(); //will return an object in the form of {"Version" : 1.0, "UpdateUrl" : "http://.../scriptInfo.xml", "ReleaseNotes" : "", "DateReleased" : ""}
		this.details = scriptInfo; //add the details to the object, incase the script wants to use this for something
		
		Interop.Call("Wininet.dll", "DeleteUrlCacheEntryW", scriptInfo.UpdateUrl); //make sure theres no cached version of the update
		
		var xmlhttp = new ActiveXObject("Microsoft.XMLHTTP"); // Create a new XMLHttp request object to download our update ScriptInfo.xml
		
		// see http://msdn2.microsoft.com/en-us/library/ms536648.aspx for details on the open method
		xmlhttp.open("GET", scriptInfo.UpdateUrl, true) //open a new xmlhttp request to the updateUrl and set it to asynchronous operation
		
		
		
		
		var thisd = this; //create a reference to the current scope to use in the callback
		
		//see http://msdn2.microsoft.com/en-us/library/ms534308.aspx for more information on the onreadystatechange callback
		xmlhttp.onreadystatechange = function(){
			//because this refers the function we have to use thisd when refering to our class
			if(xmlhttp.readyState == 4){
				var updateInfo = thisd.ParseScriptInfo(xmlhttp.responseXML); //parses the update scriptInfo and returns an object

				if(updateInfo.Version > scriptInfo.Version){ //if version is newer
					Debug.Trace("Update is newer");
				}else{
					Debug.Trace("Update Is older");
				}
			}
		}
		xmlhttp.send();
	},
	
	
	"ParseScriptInfo" : function(xml){ //returns an object with the parsed ScriptInfo information 
		if(typeof(xml) == "undefined"){ //if no xml is passed, we will load the local script info
			var xml =  new ActiveXObject("Microsoft.XMLDOM");
			xml.async = false;
			xml.load(MsgPlus.ScriptFilesPath + "\\" + "ScriptInfo.xml");
		}
	
	
		//object to return
		var ob = {
			"Version" : 0,
			"UpdateUrl" : "",
			"PlscLocation" : "",
			"ReleaseNotes" : "",
			"DateReleased" : "",
			"Name" : ""
		};
	
		//get the Update node values
		ob.UpdateUrl = xml.selectSingleNode("//Update/ScriptInfoUrl").text;
		ob.PlscLocation = xml.selectSingleNode("//Update/PlscLocation").text;
		ob.ReleaseNotes = xml.selectSingleNode("//Update/ReleaseNotes").text;
		ob.DateReleased = xml.selectSingleNode("//Update/DateReleased").text;
		ob.Name = xml.selectSingleNode("//Name").text;
		
		var version = xml.selectSingleNode("//Version").text;
		//convert the version to a real number, parseInt chops off the decimal points so we cheat and * it by 1
		ob.Version = version * 1;
		
		
		return ob; //return our object
	}, 
	"AskUser" : function(){},
	"GetNewPlsc" : function(){}
}



Now if you want to test the above code, add the following to your OnEvent_Initialize


Code:
function OnEvent_Initialize(){
	var uc = new UpdateChecker();
}


Make sure you have a copy of your ScriptInfo.xml uploaded at the location specified at ScriptInfoUrl, otherwise the code will fail.

Now run the script, if your uploaded ScriptInfo.xml file has a greater version than the local ScriptInfo.xml it should display "Update is newer"


Ok, so now we need to do something if the update is newer, we will call the UpdateChecker.AskUser() method to ask the user if they want to update.
We will just display a messagebox with yes or no buttons, in your own script I recommend that you create a fancy GUI :)



Code:
	"AskUser" : function(updateInfo){
		//we will just display a simple yes/no messagebox, in your own version you could take this a step futher and display a pretty plus GUI for it, but thats outside the scope of this tutorial
		
		var message = "A new Script Update has been detected for " + updateInfo.Name + " Do you want to update?";
		var result = Interop.Call("User32", "MessageBoxW", 0, message, "Script Update", 4 /* MB_YESNO */);
		
		
		if(result == 6){ //a result of 6 means that the user pressed "yes"
			this.GetNewPlsc(updateInfo.PlscLocation); //call the GetNewPlsc function to download the file
		}
	}



And finally we need to create the GetNewPlsc function to open the Plsc file.
Now in your own scripts you should use MsgPlus.DownloadFile to download the plsc and then use

Code:
	new ActiveXObject("WScript.Shell").Run('"downloadlocation"');

to run it
but in this example we will simply execute the url


Code:
	"GetNewPlsc" : function(url){ //function to download the plsc
		//execute the url
		new ActiveXObject("WScript.Shell").Run('"' + url + '"');
	}


Ok so lets look at the code so far.
It should download the new ScriptInfo.xml and compare the versions, if theres an update then it will open the PlscLocation


Code:
var UpdateChecker = function(){
	this.Check();
};

UpdateChecker.prototype = {
	"Check" : function(){ //This function will parse the local XML file and trigger the download the update ScriptInfo.xml
		var scriptInfo = this.ParseScriptInfo(); //will return an object in the form of {"Version" : 1.0, "UpdateUrl" : "http://.../scriptInfo.xml", "ReleaseNotes" : "", "DateReleased" : ""}
		this.details = scriptInfo; //add the details to the object, incase the script wants to use this for something
		
		Interop.Call("Wininet.dll", "DeleteUrlCacheEntryW", scriptInfo.UpdateUrl); //make sure theres no cached version of the update
		
		var xmlhttp = new ActiveXObject("Microsoft.XMLHTTP"); // Create a new XMLHttp request object to download our update ScriptInfo.xml
		
		// see http://msdn2.microsoft.com/en-us/library/ms536648.aspx for details on the open method
		xmlhttp.open("GET", scriptInfo.UpdateUrl, true) //open a new xmlhttp request to the updateUrl and set it to asynchronous operation
		
		
		
		
		var thisd = this; //create a reference to the current scope to use in the callback
		
		//see http://msdn2.microsoft.com/en-us/library/ms534308.aspx for more information on the onreadystatechange callback
		xmlhttp.onreadystatechange = function(){
			//because this refers the function we have to use thisd when refering to our class
			if(xmlhttp.readyState == 4){
				var updateInfo = thisd.ParseScriptInfo(xmlhttp.responseXML); //parses the update scriptInfo and returns an object

				if(updateInfo.Version > scriptInfo.Version){ //if version is newer
					Debug.Trace("Update is newer");
					
					thisd.AskUser(updateInfo);
				}else{
					Debug.Trace("Update Is older");
				}
			}
		}
		xmlhttp.send();
	},
	
	
	"ParseScriptInfo" : function(xml){ //returns an object with the parsed ScriptInfo information 
		if(typeof(xml) == "undefined"){ //if no xml is passed, we will load the local script info
			var xml =  new ActiveXObject("Microsoft.XMLDOM");
			xml.async = false;
			xml.load(MsgPlus.ScriptFilesPath + "\\" + "ScriptInfo.xml");
		}
	
	
		//object to return
		var ob = {
			"Version" : 0,
			"UpdateUrl" : "",
			"PlscLocation" : "",
			"ReleaseNotes" : "",
			"DateReleased" : "",
			"Name" : ""
		};
	
		//get the Update node values