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

 

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.