Finally! After much pain, have managed to get an XUL app, lauched from a web server, to talk to an XML-RPC server. Here’s how...
We have contact!
When I first read that there’s support for XML-RPC in Mozilla (and SOAP / WSDL but don’t even go there...) got really excited. Things went down hill from there while stuggling through issues which this should document how to solve.
Using XML-RPC provides an alternative approach to the “REST” approach Mike was discussing in Connecting XUL Applications with PHP.
It’s probably easier to define a clear server side API with XML-RPC but the security requirements (see: Pre-requisites) mean it’s only going to be useful for special cases (e.g. the back end admin interface to your web site) as opposed to public use.
XML-RPC support in Mozilla is provided as a JavaScript component, the project (and limited documentation) being found at http://www.mozilla.org/projects/xmlrpc/.
There are locally installed apps out there, like Mozblog which using Mozilla’s XML-RPC but, to date, I’ve failed to find examples of anyone using it in a “web launched” XUL app, where the security shennanigans come into play.
Pre-requisites 1. Get yourself Firebird 0.7 (shame on you if you haven’t already got it).
2. Edit the file [firebird_install_dir]/defaults/pref/all.js
3. Set the value “signed.applets.codebase_principal_support” to true (this relaxes security just enough to use XML-RPC from a “web launched” XUL app)
4. Get the latest version of the XML-RPC Javascript component, as updated by the Mozblog team: http://mozblog.mozdev.org/nsXmlRpcClient.js.
5. Stored it under [firebird_install_dir]/components/
6. Quit Firebird completely (if you’re using Mozilla, you’ll need to stop the quick launch as well)
7. Restart Firebird.
8. From the Firebird menu > Tool > Options > Privacy > Cache - clear the cache (in Mozilla it’s Edit > Preferences > Advanced > Cache )
The basics are now ready. You’ll get a dialog pop up whenever a script does anything needing special security permission.
Also, as always, on your web server (assuming Apache) put a .htaccess file in the directory you want to run the XUL app from, containing;
AddType application/vnd.mozilla.xul+xml .xul
To tell Apache to send the right Content-Type header.
The Script To demonstrate, I’ve put together a client script (JavaScript) which calls the XML-RPC system.listMethods and system.methodHelp methods commonly supported by XML-RPC servers using the XML-RPC Introspection extension ( see here.
Here’s the script (called system.js);
/** * Creates and returns and instance of the XML-RPC client */ function getClient() { // Enable correct security netscape.security.PrivilegeManager.enablePrivilege( 'UniversalXPConnect UniversalBrowserAccess'); return Components.classes['@mozilla.org/xml-rpc/client;1'].createInstance( Components.interfaces.nsIXmlRpcClient); } /** * Global instance stored here */ var xmlRpcClient; /** * Makes sure we only have the current instance */ function getXmlRpc() { if (!xmlRpcClient) { xmlRpcClient = getClient(); } // Fetch the server URL from the XUL document - this is specific to my app serverUrl = document.getElementById('server').value; // Initialize the client with the URL xmlRpcClient.init(serverUrl); return xmlRpcClient; }
These first two functions deal with setting up the XML-RPC client. It could be done in different ways but this seems to have become a convention amongst those who’ve been using XUL for locally installed apps. A source of examples in Mozblog can be found here.
Note that in the getXmlRpc() function, I’m fetching a value from the XUL document (the URL of the XML-RPC server entered by the user) and passing it to the init method, to “point” the client at the server. In your own app, this might be hard coded in the script.
In the getClient() method, this is Mozilla security voodoo you have to do (results in the pop up asking the user to permit the action).
/**
* Called on a button click from the XUL app
*/
function listMethods() {
// Enable correct security
netscape.security.PrivilegeManager.enablePrivilege(
'UniversalXPConnect UniversalBrowserAccess');
// Get the instance of the XML-RPC client
var xmlRpcClient = getXmlRpc();
// Get the list from the XUL doc, just to show how arg1(below) works
methodList = document.getElementById('listMethodsListBox');
// Call the listMethodsHandler
// arg0: the handler to call
// arg1: some object - whatever you like - passed to listener - use NULL if you dont want it
// arg2: XML-RPC method to call
// arg3: comma seperated array of arguments to pass to XML-RPC method
// arg4: number of arguments specified in arg3
xmlRpcClient.asyncCall(listMethodsHandler, methodList, 'system.listMethods', [], 0);
}
The listMethods function above is used to “respond” to a button click in the XUL app. The asyncCall method makes the XML-RPC call and passes the response ot the listMethodsHandler below;
/**
* Handler for the listMethods function
*/
var listMethodsHandler = {
// Called if there's a valid XML-RPC result
// Param0: the XML-RPC client
// Param1: the "context" object (see xmlRpcClient.asyncCall arg1)
// Param2: contains the XML-RPC result
onResult: function(client, methodList, result) {
// Enable correct security
netscape.security.PrivilegeManager.enablePrivilege(
'UniversalXPConnect UniversalBrowserAccess');
// "Implement" the result with the XPCom Array interface
listMethodsResult = result.QueryInterface(
Components.interfaces.nsISupportsArray);
// Set the number of results
var count = listMethodsResult.Count();
// Loop through the results, adding items to the list
for (i = 0; i < count; i++) {
methodName = listMethodsResult.QueryElementAt(
i, Components.interfaces.nsISupportsCString);
listItem = methodList.appendItem(methodName,'');
}
},
// Called if we got back an XML-RPC fault (but doesn't seem to work?)
// Param0: the XML-RPC client
// Param1: the "context" object (see xmlRpcClient.asyncCall arg1)
// Param2: the fault string
onFault: function (client, ctxt, fault) {
alert('XML-RPC Fault: '+fault);
},
// Called if there's any other error (e.g. the server URL was wrong)
// Param0: the XML-RPC client
// Param1: the "context" object (see xmlRpcClient.asyncCall arg1)
// Param2: some XPCom status code
// Param3: an error message
onError: function (client, ctxt, status, errorMsg) {
alert('Error: '+errorMsg);
}
};
The above is a handled. Basically you’ll need one for each seperate XML-RPC method you application is able to call. Because the response from system.listMethods is an array, I need to convert the result into something which implements the XPCom array interface, providing methods to iterate over the result. Basically you should be able to copy and paste what’s happening there, to deal with an XML-RPC array. See that Mozblog example I linked to above for how to deal with structs and arrays of structs.
Also see the XPCom Core Data Types.
With the loop I’m adding items to an XUL listbox element (the XUL is coming up shortly).
One other example is system.listMethods, which shows you how to pass and argument and handle a response which is a string;
/**
* Called by a button click
*/
function methodHelp(methodName) {
// Enable correct security
netscape.security.PrivilegeManager.enablePrivilege(
'UniversalXPConnect UniversalBrowserAccess');
// Get the instance of the XML-RPC client
var xmlRpcClient = getXmlRpc();
// Create an empty XML-RPC string value object to use as a parameter
var methodName = xmlRpcClient.createType(xmlRpcClient.STRING,{});
// Store in in a value from the XUL form
methodName.data = document.getElementById('methodNameTextBox').value;
// Make the XML-RPC call, passing the parameter
xmlRpcClient.asyncCall(methodHelpHandler, null, 'system.methodHelp', [methodName], 1);
}
/**
* Handler for the methodSignature function
*/
var methodHelpHandler = {
onResult: function(client, ctxt, result) {
netscape.security.PrivilegeManager.enablePrivilege(
'UniversalXPConnect UniversalBrowserAccess');
methodSignatureResult = result.QueryInterface(
Components.interfaces.nsISupportsCString);
alert(methodSignatureResult.toString());
},
onFault: function (client, ctxt, fault) {
alert('XML-RPC Fault: '+fault);
},
onError: function (client, ctxt, status, errorMsg) {
alert('Error: '+errorMsg);
}
};
/**
* Handler for the methodSignature function
*/
var methodHelpHandler = {
onResult: function(client, ctxt, result) {
netscape.security.PrivilegeManager.enablePrivilege(
'UniversalXPConnect UniversalBrowserAccess');
methodSignatureResult = result.QueryInterface(
Components.interfaces.nsISupportsCString);
alert(methodSignatureResult.toString());
},
onFault: function (client, ctxt, fault) {
alert('XML-RPC Fault: '+fault);
},
onError: function (client, ctxt, status, errorMsg) {
alert('Error: '+errorMsg);
}
};
Finally, here’s the XUL document;
<?xml version="1.0"?>
<window title="XML-RPC Introspection" orient="vertical"
xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
<script src="system.js"/>
<grid>
<columns>
<column flex="1" />
<column flex="2" />
</columns>
<rows>
<row flex="1">
<text value="XML-RPC Server URL" />
<textbox id="server"
value="http://www.oreillynet.com/meerkat/xml-rpc/server.php"/>
</row>
<row flex="1">
<text value="Available Methods" />
<listbox id="listMethodsListBox" flex="1">
<listhead>
<listheader label="Available Methods"/>
</listhead>
<listcols>
<listcol flex="1"/>
</listcols>
</listbox>
</row>
</rows>
</grid>
<hbox align="right">
<button label="system.listMethods"
id="listMethodsButton" onclick="listMethods();" />
</hbox>
<grid>
<columns>
<column flex="1" />
<column flex="2" />
</columns>
<rows>
<row flex="1">
<text value="Enter a Method Name" />
<textbox id="methodNameTextBox" value="system.listMethods" />
</row>
<row flex="1">
<text id="methodSignatureText" value=" "/>
</row>
</rows>
</grid>
<hbox align="right">
<button label="system.methodSignature"
id="methodSignatureButton" onclick="methodHelp();" />
</hbox>
</window>
Have fun!