xpages

New XPages Mobile CRM Application Released

Over the last few months I’ve been helping out Vigilus with a new XPages Mobile Web CRM application. ACT! For Notes is a long standing Notes client based application and what we’ve added in VMobile is a fairly significant subset of the features in the full client application designed to work in iPad, iPhone, Android phone and Blackberry. 

Given the size of the application it’s taken a while and there have been some challenges, but overall I’d say it’s a very capable CRM application that will now work on every device you work with day to day.

Well worth a look if your company is wanting a CRM application.

Here is the press release.

Dojo 1.7.2 in XPages

I had need to make use of some of the more advanced Dojo Mobile controls for a project last week which ship with Dojo 1.7.2. For various reasons I needed the Dojo files inside the nsf rather than on the file system, and if you’ve had to do that before you’ll know what a massive pain it is to get the files loaded. So I thought I’d share a simple nsf with the files loaded.

You can either clone the database from Github here: https://github.com/whitemx/Dojo172XPages

Or you can download the nsf here: Dojo172.nsf.zip 

The other changes I’ve made to the database are to disable the standard Dojo libraries (1.6.1 in 8.5.3) in the xsp.properties file and also turned off the default theme settings as well so no CSS will be downloaded unless you manually specify it in the resources.

Sixty not out

Today I uploaded the 60th video to xpages101.net. It’s taken me over 18 months to get this far and there are no plans to stop adding more content. In fact with the announcement of the 8.5.3 release date, there are many more areas which need to be covered. Expect to see a rash of new videos over the next few weeks.

I thought I’d post some of the stats for the site so far.

 

 

In the last couple of months I’ve had the 250th subscriber sign up for the site, and more are joining weekly. Since the site was re-designed earlier this year it’s also easier to submit your own lesson ideas if there’s something which I’ve not covered yet and each of the lessons has a comments section to clarify points or add you’re own thoughts on the lesson.

And finally, to celebrate the 60th lesson being uploaded, here’s an offer for you. If you use the coupon code “60notout” at checkout, you’ll get a £60 discount on your purchase. Just head on over to xpages101.net to get started.

Dojo Charts and IE

I’ve just spent the better part of three days working on a suite of Dojo Charts for an application. They are usually pretty easy to get going and look as good, if not better than a lot of the flash charting tools out there. But, and there’s always a but isn’t there, getting the charts working in IE can be a real challenge with very little in the way of guidance.

So here are three tips which I shall pass on learned through bitter experience over the last few days…

It may well look as though you can initialise your charts using in line JavaScript and, indeed, it will work absolutely fine in Firefox, Safari, Chrome and other “modern” web browser. Not so much with IE. So break your initialisation code into a function and call it from the either the onClientLoad client side event in the XPage or using XSP.addOnLoad(init) depending on your preference.

This was a weird one, and took me ages to find. But one of my charts requires the user to select some options and then I fire a partial refresh of the page to generate the JSON data which populates the chart. But if the data which is returned by the AJAX request (the partial update) begins with JavaScript which needs to be evaluated then you may find in IE that the code is not being evaluated. So the JSON objects I was generating were not available for use in the chart. The solution, and it still grates me that I had to do this, was to add an empty, hidden div at the top of the area which is being refreshed, then the JSON data will be processed correctly.

Finally another thing which I had to do to make my code less efficient was re-write the way that I was generating some of the JSON data. One of the charts has multiple, variable numbers of series of data depending upon what the user selects. So I was generating each series of data to be a JSON object inside an array of JSON objects that I just looped through when building the chart. But no, it seems IE doesn’t like accessing multi dimensional arrays of JavaScript when building charts, so instead I had to create a different variable for each JSON data series and reference them using evaluates.

So the lesson from all of this is that, a) do your initial development in Firefox or Chrome, b) never, ever under estimate the amount of time you’ll need to spend getting the bloody thing working in IE.

LUG Template available on OpenNTF

At UKLUG this year, Warren announced that we would be making the template which we use to run the online elements of the LUG available on OpenNTF. Well I finally got around to making the template and uploading the bits and pieces onto OpenNTF (actually a surprisingly easy process!).

Anyway, if you’re interested then you can download the template from here: LUG Template

As Warren mentioned, we’re looking for help. At the moment there is no documentation at all, so it can be a little painful to get set up, just ask Chris, or Mitch or Stuart or any of the other people who are already using it for their own LUGs! The app itself is simple enough, but it registers people in different DA databases, integrates into other elements on your server, sends email etc etc.

So if you can help then please drop me a line and I will put together a list of elements that we need help with. Of course I’m also looking for help with bug fixing and improving various elements of the application as I’ve not been able to devote as much time as I would like to it this year.

Finally, I’d just like to give a big hand to Warren. I suspect the amount of work that he puts in to UKLUG (and ILUG) goes rather underestimated and it was him who drove the original development of the application and then made the decision for it to be given back to the Lotus community.

XPages Guru Webinar

I’m very honoured to have been asked by Chris Blatnick to be involved in a webinar running on 1st June where several of the “Gurus of XPages” from around the world (plus myself) will be talking about our favourite technology.

You can sign up for the event here.

The rough agenda for the hour long session looks like this:

What is XPages? – Matt White
Case Studies – Discussion database, XTalk, etc. – Bruce Elgort 
Benefits of XPages – Matt White
Why should I transition? – Tim Tripcony
Roadmap to XPages (How do I get from here to there?) – Chris Toohey
What resources are out there? – David Leedy
Q & A

XPages101 LS11 discount

As the annual pilgrimage to Florida for Lotusphere begins, I thought it would be worth mentioning a discount for XPages101 which I’ll be offering for the next 3 weeks.

Use the coupon code “ls11” at checkout and get a 33% discount!

It’s coming up to a year since I first launched the site and in that time I’ve uploaded 37 lessons which run to more than 7 hours of content and well over 2gb of movies. There are many more lessons planned over the coming weeks and months.

So if you feel like it’s time to get into XPages, or you’re just looking for a bit of a helping hand with some of the more complex tasks in Domino Designer then hopefully my videos will be able to help you. Check them out.

How to get SSO for Facebook working with XPages

So, earlier today I posted a short video about a new feature that I’ve written for IQJam (initially at least, it will be coming to IdeaJam at some point). It allows you to authenticate against a Domino server using Facebook Single Sign On (SSO).

Overall this is not for the faint of heart (or the new XPages developer, hence me not going into detail about how what the code is doing etc), there are a lot of moving parts to get things working properly. That being said there is absolutely no reason exactly the same approach couldn’t be taken with a classic Domino application. All of the server Side Javascript would need to be converted to LotusScript and Java (for the network operations).

1) Register your application with Facebook here: http://www.facebook.com/developers/apps.php

2) In the app home page, add a facebook login graphic with some client side onClick code:

var returl = "http://[Your App Host Name][Your App DB Path]/fblogin.xsp";
var url = "https://graph.facebook.com/oauth/authorize?client_id=[YOUR FACEBOOK APP ID]&redirect_uri=" + returl;
window.open (url, "mywindow","width=500,height=250");

3) Create a new XPage called fblogin

4) In the afterPageLoad event put the following code:

try{
    //Get these from Facebook App registration
    var API_KEY = "YOUR FACEBOOK APP ID";
    var SECRET = "YOUR FACEBOOK SECRET KEY";

    //Get the auth code from url param returned by facebook
    var code = context.getUrlParameter("code");
    
    //Now swap the auth code for the access_token key
    var urltoken = "https://graph.facebook.com/oauth/access_token?client_id=" + API_KEY + 
                    "&redirect_uri=http://[yourhostname]" + 
                    "/" + @ReplaceSubstring(database.getFilePath(), "\\", "/") + "/fblogin.xsp" + 
                    "&client_secret=" + SECRET + 
                    "&code=" + code;
    var urltoken:jav.net.URL = new java.net.URL(urltoken);
    var urltokenconn = urltoken.openConnection();
    var tokenreader = new java.io.BufferedReader(
                            new java.io.InputStreamReader(
                            urltokenconn.getInputStream())
                        );
    var inputLine;
    var accesstoken = "";
    while(    (inputLine = tokenreader.readLine()) != null){
        accesstoken += inputLine;
    }
    tokenreader.close();
    accesstoken = @Right(accesstoken, "=");

    //Now get the user info using the access_token
    var url = "https://graph.facebook.com/me?access_token=" + accesstoken + "&client_id=" + API_KEY;
    var url:java.net.URL = new java.net.URL(url);
    var urlconn:java.net.URLConnection = url.openConnection();
    var reader:java.io.BufferedReader = new java.io.BufferedReader(
                                            new java.io.InputStreamReader(
                                            urlconn.getInputStream())
                                        );
    var inputLine;
    var userjson = "";
    while ((inputLine = reader.readLine()) != null){
        userjson += inputLine;
    }
    reader.close();
    
    //Now we've got a JSON object which contains the user data
    userjson = eval("(" + userjson + ")");
    var firstname = userjson.first_name;
    var lastname = userjson.last_name;
    var userId = userjson.id;
    var fbname = getFBName(firstname, lastname, userId);
    var password = getFBPassword(fbname, SECRET);
    print("FBName = " + fbname);
    var fbreg = new facebookReg();
    if (fbreg.validateUser(fbname.getCanonical())){
        //We need to go and register the user
        fbreg.registerNewFBUser(firstname, lastname, fbname, password);
    }
    //Set the username and password fields so the Ajax login can happen    
    viewScope.username = fbname.getCanonical();
    viewScope.password = password;
}catch(e){
    _dump(e);
}

5) In a supporting script library you’ll need the following functions:

/*
An object which handles authentication / registration of Facebook users
Created By: Matt White
Date Created: October 2010
Version: 1.0
*/
var facebookReg = function(){
    var dbNab:NotesDatabase = null;
    var dbMainNab:NotesDatabase = null;
    var registerNewFBUser = function(firstname, lastname, fbname, password){
        getDbs();    
        if(validateUser(fbname.getCanonical())){
            var registerNewUser = false;
            
            if (!addUserToGroup(fbname)){
                print("Couldn't add " + fbname.getAbbreviated() + " to group");
            }else{
                dbNab.DelayUpdates = false
                dbMainNab.DelayUpdates = false
                
                var docPerson = dbNab.createDocument();
                
                docPerson.replaceItemValue("form", "Person");
                docPerson.replaceItemValue("Type", "Person");
                docPerson.replaceItemValue("LastName", lastname);
                docPerson.replaceItemValue("FirstName", firstname);
                var item = docPerson.replaceItemValue("FullName","");
                item.appendToTextList(fbname.getCanonical());
                item.appendToTextList(firstname + " " + lastname);
                docPerson.replaceItemValue("HTTPPassword", password);
                docPerson.replaceItemValue("accountstatus", "Not Verified");
                
                docPerson.computeWithForm( false, false );
                print("Saving new person doc: " + docPerson.getUniversalID() + " in " + dbNab.getTitle());
                docPerson.save();
                
                var addviews = new Array();
                addviews.push(dbNab.getView("($LDAPCN)"));
                addviews.push(dbNab.getView("($Users)"));
                addviews.push(dbNab.getView("($ServerAccess)"));
                addviews.push(dbNab.getView("($VIMPeople)"));
                addviews.push(dbMainNab.getView("($ServerAccess)"));
                addviews.push(dbMainNab.getView("($VIMGroups)"));
                addviews.push(dbMainNab.getView("($Users)"));
                for(var i=0; i<addviews.length; i++)
                    addviews[i].refresh();
                print("Refreshed views");
                
                //Finally create a profile document for the person
                var dbCurrent = sessionAsSigner.getDatabase(database.getServer(), database.getFilePath());
                var profile = dbCurrent.createDocument();
                profile.replaceItemValue("Form", "person");
                profile.replaceItemValue("Name", fbname.getCanonical());
                profile.computeWithForm(false, false);
                profile.save();
                print("Created profile document");
            }
        }
    }
    
    var validateUser = function(thisname){
        getDbs();
        var people = dbNab.getView("($Users)");
        var collection = people.getAllDocumentsByKey(thisname, true);
        print("Found " + collection.getCount() + " matching people for " + thisname);
        if (collection.getCount() > 0)
            return false;
        else
            return true;
    }
    
    var addUserToGroup = function(nname){
        var group = "[Your Group Name Here]";
        var groups = dbMainNab.getView("Groups");
        var docGroup = groups.getDocumentByKey(group, true);
        
        if (docGroup == null){
            docGroup = dbMainNab.createDocument();
            docGroup.replaceItemValue("Form", "Group");
            docGroup.replaceItemValue("ListName", group);
            docGroup.replaceItemValue("Members",  group & " 1");
            docGroup.replaceItemValue("GroupType", "0");
            docGroup.replaceItemValue("ListDescription", "Do NOT edit this group manually, it is updated via an agent!!!");
            docGroup.computeWithForm( false, false );
            docGroup.save();
        }
        
        var groupMainMembers = docGroup.getFirstItem( "Members" );
        var subGroup = "";
        for (var x=groupMainMembers.getValues().length; i>=0; i--){
            if (@Left(groupMainMembers.getValues()[x], @Length( group  )) == group)
                subGroup = groupMainMembers.getValues()[x];
        }
        
        groupNum = 0;
        
        if (subGroup != "")
            groupNum = @TextToNumber( @Right( subGroup, @Length( subGroup ) - @Length( group ) - 1 ) );
        else{
            groupNum = 1
            subGroup = group + " 1";
        }
        
        while(true){
            var groupSubDoc = groups.getDocumentByKey( subGroup, true );
            
            if (groupSubDoc == null){
                groupSubDoc = dbMainNab.createDocument();
                groupSubDoc.replaceItemValue("Form", "Group");
                groupSubDoc.replaceItemValue("ListName", subGroup);
                groupSubDoc.replaceItemValue("GroupType", "0");
                groupSubDoc.computeWithForm( false, false );
                
                if (!groupMainMembers.containsValue( subGroup )){
                    try{
                        groupMainMembers = docGroup.getFirstItem("Members");
                        groupMainMembers.appendToTextList(subGroup);
                        saveGroupMainDoc = true;
                    }catch(e){
                        _dump(e);
                    }
                }
            }
            var groupSubMembers = groupSubDoc.getFirstItem( "Members" );
            
            if (groupSubMembers.getValueLength() < 10000)
                break;
            
            groupNum = groupNum + 1;
            subGroup = group + " " + groupNum;
        }
        
        groupSubMembers.appendToTextList(nname.getCanonical());
        groupSubDoc.save( false, true );
        docGroup.save( false, true );
        return true;
    }
    
    var getDbs = function(){
        if (dbNab == null || dbMainNab == null){
            dbNab = sessionAsSigner.getDatabase(database.getServer(), "[NAB Where Users Are Stored]");
            dbMainNab = sessionAsSigner.getDatabase(database.getServer(), "[Main NAB]");
        }
    }
    
    return {
        // public methods
        registerNewFBUser:        registerNewFBUser,
        validateUser:            validateUser,
        addUserToGroup:            addUserToGroup, 
        getDbs:                    getDbs
    }
}
/*
Creates a new Notes Name using First Name, Last Name, Facebook User ID
*/
function getFBName(firstname, lastname, uid){
    return session.createName(firstname + " " + lastname + "/" + uid + "/Facebook");
}
/*
Generates a password using a Notes Name and a secret key as a salt
*/
function getFBPassword(fbname:NotesName, seed){
    var result = session.evaluate("@Password(\"" + fbname.getCanonical() + seed + "\")");
    return @ReplaceSubstring(result.elementAt(0), ["(",")"], "");
}

6) Finally you’ll need some AJAX code which logs the user in assuming all of the previous code has worked properly

function doLogin(userNameId, passwordId, facebookmode){
    dojo.xhrPost({
        url: "/names.nsf?login",
        content: {
            username: dojo.byId(userNameId).value, 
            password: dojo.byId(passwordId).value, 
            redirectto: dbPath + "/username.txt?open&rnd=" + Math.random()
        },
        load: function(data) {
            try {
                if( data.indexOf("Anonymous") == -1) { 
                    dojo.byId("loginMsg").style.display = "block";
                    dojo.byId("loginMsg").style.color = "green";
                    dojo.byId("loginMsg").style.backgroundColor = "transparent"; 
                    dojo.byId("loginMsg").innerHTML = "Please Wait";
                    if(location.href.indexOf("register.xsp") > -1){
                        location.href = dbPath;
                    }else{
                        if (facebookmode == true){
                            window.opener.location.href = window.opener.location.href;
                            window.close(); 
                        }else{
                            if (window.location.href.indexOf("#") > -1){
                                window.location.replace( strLeft(window.location.href, "#") );
                            }else{
                                window.location.replace( window.location.href );
                            }
                        }
                    }
                } else { 
                    dojo.byId("loginMsg").style.display = "block";
                    dojo.byId("loginMsg").style.color = "red"; 
                    dojo.byId("loginMsg").style.backgroundColor = "transparent";
                    if ( dojo.cookie('DomAuthSessId') != null || dojo.cookie('LtpaToken') != null ) { 
                        dojo.byId("loginMsg").innerHTML = "You do not have access to this database";
                    } else { 
                        dojo.byId("loginMsg").innerHTML = "Wrong username or password"; 
                    } 
                }
            }catch(e){
                alert(e);
                console.error ('Error: ', error);
            }
        },
        error: function(data) {
            alert(e);
            console.error ('Error: ', error); 
        } 
    });
}

7) That code will need to be triggered by some Javascript which runs when the page loads (so this needs to go in the onClientLoad function)

if (gup("code") != ""){
    doLogin("#{id:username}", "#{id:password}", true)
}else{
    window.close();
}

For the gup function go here: http://www.netlobo.com/url_query_string_javascript.html

 

Facebook SSO to XPages

One of the things I’ve been playing with this week is getting Facebook SSO working with XPages. It’s not a true SSO implementation as Domino doesn’t support OAuth, but from the user’s point of view they are not having to enter a username and password to get authenticated against a Domino app. I think it’s pretty cool…

Forcing a file name when downloading a dynamically generated csv file

Over the last couple of weeks I’ve been working on a customisation project using IQJam as a starting point and then making it better fit a particular customer’s requirements. It’s worth mentioning as I finally had the justification to spend a little time investigating a problem which has been bugging me.

One of the features I added to IQJam was the ability to export data to Excel, a common enough feature that you’ve been able to do in Domino for ever. Simply print data out to the browser in simple HTML format and change the content-type of the page to “application/vnd.ms-excel”. That’s not the point of this posting really.

The problem I’ve been trying to work around is that if your user is using Excel 2007 or later (I’m only on the beta of Office 2010 but it still seems to be a problem for me anyway) and you use the printing HTML technique, Excel raises an error for the user when they load the page, something like:

The file you are trying to open, ‘name.ext’, is in a different format than specified by the file extension. Verify that the file is not corrupted and is from a trusted source before opening the file. Do you want to open the file now?

According to various technotes and blog posts there is no work around for this, it is a deliberate security feature of Excel. Fair enough I can live with this, though it’s not acceptable to the end user, so instead of generating an Excel file I reverted to generated CSV formatted data.

The thing I learned from my investigations into the Excel problem was that there is this header which you can add to the data being generated which tells the browser that the page being opened is actually a file attachment and what the name of the file you want to download should be. So in my example, I’m able to create a unique file name every time the page is generated and also specify that it should be saved as a CSV file, not .XSP which is what it would be thanks to the page being loaded being an XPage. See this example:

try{
    var exCon = facesContext.getExternalContext();
    var writer = facesContext.getResponseWriter();
    var response = exCon.getResponse();
    response.setContentType(“text/csv”);
    response.setHeader(“Content-disposition”, “attachment; filename=export_” + DateConverter.dateToString(@Now(), “yyyyMMddhhmm”) + “.csv”);
    response.setHeader(“Cache-Control”, “no-cache”);
    writer.write(getCSVBody());
    writer.endDocument();
    facesContext.responseComplete();
    writer.close();
}catch(e){
    _dump(e);
}

So this server side javascript sits in the afterRenderResponse of my XPage and is using the “web agents XPages style” technique which Stephan first documented to generate non HTML content from an XPage. The key line for this blog post is where the “Content-disposition” header is set, hopefully you can see where the filename is being created (I’m also using Tommy Valand’s DateConverter SSJS code to get the current date / time formatted into a nice string).

Anyway, not a new technique looking at the dates on some of the internet postings out there, but a new one on me and worth passing on I thought.