XPages

It’s Learn XPages Month (and I have a discount to offer)

You may have seen that June has been declared Learn XPages Month. XPages.info has all of the details but I thought I would (shamelessly) highlight that XPages101 will be offering a 33% discount for the whole of June. If you use the coupon code “learnxpages” at checkout the discount will be applied for access to a whole year’s worth of XPages training content.

So head on over to XPages101 to get your subscription.

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…

What’s new with XPages in 8.5.2

Over on XPages101.net I’ve just posted (yet another) video. This time we’re looking at what’s new in 8.5.2 for the XPages developer.

What’s New With XPages in 8.5.2

As a bit of a teaser, I thought I’d post the audio from that recording here as well so that you can hear what you’re missing. As ever, feel free to subscribe if you want to get a jumpstart on your XPages development. There are 30 different videos up on the site covering all the elements you’ll need to get started and then dig a little deeper into XPages development. The full video listing can be found here.

XPages101 Online is here

One of my big takeaways from Lotusphere this year was that there is a huge appetite for XPages training material. I had already got my classroom based XPages101 course lined up, but now that that’s done, it’s time to turn attention to the rest of the world and the online format.

So what I’ve come up with is a new website (same URL as before though) at xpages101.net. From here you can see the details of the classroom based course but there’s a whole other section of the site devoted to online material. Now to make this work for me (I run a small business on my own and this stuff takes a *lot* of time to put together) there is a charge for the content. But I’ve tried to make it as appealing as possible.

You have two choices, either a single user account or a five user account. In both cases you’ll get full access to the site, all of the videos and support content for a full 12 months. The aim from my side is to keep on adding new videos every week or two for as long as there are interesting things to talk about. I just worked out what I have to create lessons on and it will take me well into May just from my initial list!

Also, to thank you for your early interest in the site, you can use the coupon code “earlybird” at check out and get a 33% discount until February 26th.

So please, go and check out the site, let me know what you think and if you have any areas that you’d like covered the please either comment here or use the contact us form on the site.

XPages101 done. (for now)

Yesterday, Ben Poole and myself presented the XPages101 course which I have designed over the last couple of months to be an introduction to developing with XPages.

Overall it seemed to go pretty well, the timings need some work, I went a little fast at some points which may have left a few people overwhelmed at the fire hose of information we got through in a short period of time! That will not happen again now that I understand how it all hangs together a bit more.

The feedback from the attendees was really gratifying…

Question Score
Overall, how would you rate the course? 95% (between Excellent and Good)
How was the course material? 86.67% (between Excellent and Good)
How was the presentation style? 86.67% (between Excellent and Good)
Would you recommend the course to your colleagues / nerdy friends? Yes: 100%

 

Obviously there’s always room for improvement and there were some great feedback comments as well…

“A real eye opener for me as I’ve not looked XPages prior to the course.”

“Excellent value for money. Pitched just right for the first steps into XPages.”

“It gave me a very good understanding of XPages under the bonnet.”

“It was great being here, great course, the XPages start I was looking for.”

One of the things which I had consciously not done was provide lots of handout materials. The aim was to reduce the costs of the course as much as possible. From the feedback it seems people would have been happy to pay extra to get more paper based materials. So I think in future iterations of the course we’ll change that around.

The other area where a couple of people expressed an interest was for an “intermediate” course. This leaves me in a bit of a quandary, as I’m not sure I know what intermediate is. So if you have any thoughts I’d love to hear them.

Overall though, as I said earlier, it was a great day. Good fun for me, and hopefully useful to everyone else.

Announcing XPages101 – An Introduction to XPages course

A few weeks ago I asked the twittersphere what people thought would be a reasonable cost for a one day long, introduction to XPages course. This was before I had investigated the cost of anything such as room hire etc, and also before I had thought about what the content for the course might be. But I liked the idea of running a single day bootcamp that will help you get started with XPages so, taking people’s advice and sourcing all of the elements for the course, I’m happy to be able to open up xpages101.net today.

I’ve managed to find a very reasonably priced training room, and more importantly, written a sample application which includes as much of the elements of a “real world” XPages app as I could squeeze into an eight hour course. The plan for the day is that you bring along your own laptop with Domino Designer 8.5.1 installed, I give you a starting point template and then together we go through the steps to build an XPages application. As we progress we’ll cover all of the major elements of XPages including the new design elements, server side Javascript, Dojo, OneUI and Themes, searching and so on. Then at the end of the day you’ll have a working application (in theory at least!) and be able to go back to your office and use it as a reference point for your first real world development when you deploy Domino 8.5.1.

So please do head on over to xpages101.net, have a look at the site and if you’ve got any questions drop me a line, otherwise I hope to see you in London in February 2010.

Catch and Cancel Return Key Press for Safari in XPages

The solution to one of the small but annoying bugs in IQJam had eluded me for quite a while, so this morning I resolved to have another go at it after a lazy weekend of feeling ill. And, lo and behold, I managed to fix it.

So a little bit of background to the problem, when you login to IQJam we provide a login button for you to press after filling in your username and password, but most people are used to just hitting return after finishing typing their password. Unfortunately, this is a case where Safari actually implements internet standards just a little too well. What browsers are meant to do (but none of them except Safari actually do) is submit the form that a text field is in when the user presses the return key.

Now in most development environments this is pretty easy to fix, you just add a

return false;

to the onSubmit action of the HTML form that the field is located in. But with XPages we don’t have access to the action, onSubmit or anything else to do with the form, as pretty much everything you click on in an XPage will want to interact with the server using code that has been generated for us rather than written by us.

In this case, I wanted to do my own AJAX post request to the server and deal with the entire interaction myself and stop anything else being submitted to the server. The problem was how? I spent ages trying to stop event bubbling to try and cancel the keypress event when the user presses return to no avail. However, I did learn, at least, that the keypress event fires before the form is submitted back to the server.

So my solution is to put this code in the keypress event of the password field:

if (thisEvent.keyCode == 13){

dojo.connect(

document.forms[0], 

‘onsubmit’

null

function(e)

e.preventDefault();

return false;

}

);

doLogin(“#{id:userName}”, “#{id:password}”)

}

Basically when the user presses the return key, using Dojo, I can re-write the form onsubmit method to prevent it posting back to the server, and instead run my own “doLogin” function.

It’s important to be aware that this will effectively break the XPage for all other actions unless I re-write the original onSubmit function back to what it was after I have finished, but here, I am going to be re-loading the page anyway so it’s not necessary.

Once again, we have more evidence that if you’re beginning to get into XPages, it’s worth spending as much time learning Dojo as it is to learn the XPages themselves.

On synchronization in XPages

The biggest issue that we had with XPages in 8.5.0 was performance, every piece of code you wrote had to be optimised to get the best performance, something which us Notes developers have spent all too little time on in the past.

With 8.5.1 everything is faster from DDE onwards, but there are some code changes that we can take advantage of to make things even better. Most important of these is synchronization. For those of you who know Java this will be a familiar concept that has just been implemented into Server Side JavaScript (SSJS). But for the LotusScript developer, here is my quick attempt at explaining it…

If you have a commonly used piece of code, it will be called by multiple different areas of the system at the same time (or at least very close together) and return the same result. If you’re doing some expensive piece of work (such as a DbLookup) then all of the calls to that code will run and there will be an inevitable performance hit.

By adding the synchronize wrapper around the “expensive” code what will happen is that all of the calls to that code will queue up behind each other, so that code can only run once at a time. We can then cache the results of the code so that all of the queued up calls can just get that result from memory (the applicationScope for example) rather than having to go off and calculate it again and again. A very simple concept but one which saves a huge amount of processing time.

But what will our code look like? Well here is a sample function from the upcoming IQJam application that will be launching later this week.

function getControlPanelFieldString(fieldname){

synchronized(applicationScope){

if(isCacheInvalid(“controlpanel_” + fieldname, 600)){

var controlPanels = database.getView(“lookupControlPanel”);

var controlPanel = controlPanels.getFirstDocument();

applicationScope.put(“controlpanel_” + fieldname, controlPanel.getItemValueString(fieldname));

controlPanel = null;

controlPanels = null;

}

  return applicationScope.get(“controlpanel_” + fieldname);

}

/**

A generic caching mechanism for each key will check to see if it is ‘n’ seconds

since it was last updated. Use for things that change relatively infrequently  

*/

function isCacheInvalid(key, cacheInterval){

var currentTime = new Date().getTime();

if (!applicationScope.containsKey(key + “_time”)){

applicationScope.put(key + “_time”, currentTime);

  return true;

}

var diffInSecs = Math.ceil((currentTime – applicationScope.get(key + “_time”)) / 1000);

if (diffInSecs < cacheInterval) {

return false;

} else {

applicationScope.put(key + “_time”, currentTime);

return true;

}

}

We store lots of tiny variables about the application in a “Control Panel” document and then read them as needed into the applicationScope. The nature of the variables is that they don’t change much so we can cache them for long periods of time (10 minutes in this case).

As with lots of XPages code, the idea for this came from the Discussion template (which is quite dramatically different under the covers in 8.5.1), so I’d highly recommend digging through the code in there to get an idea of what you can do with XPages. And of course we have to offer thanks to Thomas Gumz and the other XPages developers in IBM who write the code that the rest of us can then re-use for our own dastardly ends.

Disclaimer: Notes/Domino 8.5.1 is beta software and no features are guaranteed until release.

Sessions, sessions, sessions

When is a session not a session?

Most Domino web sites these days will be running some form of session authentication which stores a cookie on the user’s browser and a tiny piece of memory with details about the user on the server. This has always worked fine, and of course these sessions can time out (by default after 30 minutes).

But with the advent of XPages we now also have the sessionScope variable container which allows us to store information about a user’s session (whether they are authenticated or not). This offers us huge opportunities for performance improvements as it means we don’t need to keep on doing @DbLookups to get user specific information for every page load. But it does introduce some potential issues if the authentication session timeouts are different to the sessionScope timeouts.

Basically the server needs to be able to clear out sessionScope variables after an amount of time, otherwise the server would run out of memory very quickly indeed. But in a recent project we had users who would open a page and then come back to it 2 or 3 hours later and then wonder why everything had stopped working. This is because the default timeout for sessionScope variables is something around 30 minutes and our session timeouts were set to 2 hours.  So once the page had been left inactive for half an hour it effectively lost all of the background information that drove how it should act when the user pressed the save button for example.

The solution is very simple, but very important. In the application properties for your database (opened in Domino Designer these days remember), go to the XPages tab and make sure to set the Session timeout field to be more than the authentication Session Timeout (which you set in the server or website document). The rule of thumb that we have come to (in the absence of any official guidelines from IBM) is that if you have a 2 hour authentication timeout for your website, then set the XPages Session Timeout to 3 hours.

Generally the application timeout is less important for this sort of thing, but we took the opportunity to also set that to 3 hours in this case as I knew it wouldn’t be too large.

There are, of course, caveats here. If your server has memory issues then this is only going to exacerbate them as for each user (remember a user is just a visitor to the website, not necessarily someone who logs in) is going to take up some memory from the server for a minimum of 3 hours, but for us it solves more problems than it causes.

Of course, if you are not using the sessionScope to store any page sensitive data then you can just ignore all of this, but once you start using the sessionScope, it does become rather addictive, so I suspect you will get some benefit from setting this variable. Either way if you start to see completely bizarre errors, this may be a good first port of call.

Hopefully this article will save someone the pain of trying to debug seemingly un-reproducible errors in a dev environment where you don’t have the time to leave pages inactive for hours on end!