/*global define */ define(['jquery', 'underscore', 'backbone', 'jws', 'models/Search', "collections/SolrResults"], function($, _, Backbone, JWS, SearchModel, SearchResults) { 'use strict'; // User Model // ------------------ var UserModel = Backbone.Model.extend({ defaults: function(){ return{ type: "person", //assume this is a person unless we are told otherwise - other possible type is a "group" checked: false, //Set to true when we have checked the status of this user basicUser: false, //Set to true to only query for basic info about this user - prevents sending queries for info that will never be displayed in the UI lastName: null, firstName: null, fullName: null, email: null, logo: null, description: null, verified: null, username: null, usernameReadable: null, orcid: null, searchModel: null, searchResults: null, loggedIn: false, ldapError: false, //Was there an error logging in to LDAP registered: false, isMemberOf: [], isOwnerOf: [], identities: [], identitiesUsernames: [], pending: [], token: null, expires: null, rawData: null } }, initialize: function(options){ if(typeof options !== "undefined"){ if(options.username) this.set("username", options.username); if(options.rawData) this.set(this.parseXML(options.rawData)); } this.on("change:identities", this.pluckIdentityUsernames); this.on("change:username change:identities change:type", this.updateSearchModel); this.createSearchModel(); this.on("change:username", this.createReadableUsername()); //Create a search results model for this person var searchResults = new SearchResults([], { rows: 5, start: 0 }); this.set("searchResults", searchResults); }, createSearchModel: function(){ //Create a search model that will retrieve data created by this person this.set("searchModel", new SearchModel()); this.updateSearchModel(); }, updateSearchModel: function(){ if(this.get("type") == "node"){ this.get("searchModel").set("dataSource", [this.get("node").identifier]); this.get("searchModel").set("username", []); } else{ //Get all the identities for this person var ids = [this.get("username")]; _.each(this.get("identities"), function(equivalentUser){ ids.push(equivalentUser.get("username")); }); this.get("searchModel").set("username", ids); } this.trigger("change:searchModel"); }, parseXML: function(data){ var model = this, username = this.get("username"); //Reset the group list so we don't just add it to it with push() this.set("isMemberOf", this.defaults().isMemberOf, {silent: true}); this.set("isOwnerOf", this.defaults().isOwnerOf, {silent: true}); //Reset the equivalent id list so we don't just add it to it with push() this.set("identities", this.defaults().identities, {silent: true}); //Find this person's node in the XML var userNode = null; if(!username) var username = $(data).children("subject").text(); if(username){ var subjects = $(data).find("subject"); for(var i=0; i<subjects.length; i++){ if($(subjects[i]).text().toLowerCase() == username.toLowerCase()){ userNode = $(subjects[i]).parent(); break; } } } if(!userNode) userNode = $(data).first(); //Get the type of user - either a person or group var type = $(userNode).prop("tagName"); if(type) type = type.toLowerCase(); if(type == "group"){ var fullName = $(userNode).find("groupName").first().text(); } else if(type){ //Find the person's info var firstName = $(userNode).find("givenName").first().text(), lastName = $(userNode).find("familyName").first().text(), email = $(userNode).find("email").first().text(), verified = $(userNode).find("verified").first().text(), memberOf = this.get("isMemberOf"), ownerOf = this.get("isOwnerOf"), identities = this.get("identities"); //Sometimes names are saved as "NA" when they are not available - translate these to false values if(firstName == "NA") firstName = null; if(lastName == "NA") lastName = null; //Construct the fullname from the first and last names, but watch out for falsely values var fullName = ""; fullName += firstName? firstName : ""; fullName += lastName? (" " + lastName) : ""; if(!fullName) fullName = this.getNameFromSubject(username); //Don't get this detailed info about basic users if(!this.get("basicUser")){ //Get all the equivalent identities for this user var equivalentIds = $(userNode).find("equivalentIdentity"); if(equivalentIds.length > 0) var allPersons = $(data).find("person subject"); _.each(equivalentIds, function(identity, i){ //push onto the list var username = $(identity).text(), equivUserNode; //Find the matching person node in the response _.each(allPersons, function(person){ if($(person).text().toLowerCase() == username.toLowerCase()){ equivUserNode = $(person).parent().first(); allPersons = _.without(allPersons, person); } }); var equivalentUser = new UserModel({ username: username, basicUser: true, rawData: equivUserNode }); identities.push(equivalentUser); }); } //Get each group and save _.each($(data).find("group"), function(group, i){ //Save group ID var groupId = $(group).find("subject").first().text(), groupName = $(group).find("groupName").text(); memberOf.push({ groupId: groupId, name: groupName }); //Check if this person is a rightsholder var allRightsHolders = []; _.each($(group).children("rightsHolder"), function(rightsHolder){ allRightsHolders.push($(rightsHolder).text().toLowerCase()); }); if(_.contains(allRightsHolders, username.toLowerCase())) ownerOf.push(groupId); }); } return { isMemberOf: memberOf, isOwnerOf: ownerOf, identities: identities, verified: verified, username: username, firstName: firstName, lastName: lastName, fullName: fullName, email: email, registered: true, type: type, rawData: data } }, getInfo: function(){ var model = this; //If the accounts service is not on, flag this user as checked/completed if(!appModel.get("accountsUrl")){ this.set("fullName", this.getNameFromSubject()); this.set("checked", true); return; } //Only proceed if there is a username if(!this.get("username")) return; /* //Check if this is an ORCID if(this.isOrcid()){ //Get the person's info from their ORCID bio appLookupModel.orcidGetBio({ userModel: this, success: function(){ model.set("checked", true); }, error: function(){ model.set("checked", true); } }); return; }*/ //Get the user info using the DataONE API var url = appModel.get("accountsUrl") + encodeURIComponent(this.get("username")); var requestSettings = { type: "GET", url: url, success: function(data, textStatus, xhr) { //Parse the XML response to get user info var userProperties = model.parseXML(data); //Filter out all the falsey values _.each(userProperties, function(v, k) { if(!v) { delete userProperties[k]; } }); model.set(userProperties); //Trigger the change events model.trigger("change:isMemberOf"); model.trigger("change:isOwnerOf"); model.trigger("change:identities"); model.set("checked", true); }, error: function(xhr, textStatus, errorThrown){ // Sometimes the node info has not been received before this getInfo() is called. // If the node info was received while this getInfo request was pending, and this user was determined // to be a node, then we can skip any further action here. if(model.get("type") == "node") return; if((xhr.status == 404) && nodeModel.get("checked")){ model.set("fullName", model.getNameFromSubject()); model.set("checked", true); } else if((xhr.status == 404) && !nodeModel.get("checked")){ model.listenToOnce(nodeModel, "change:checked", function(){ if(!model.isNode()){ model.set("fullName", model.getNameFromSubject()); model.set("checked", true); } }); } else{ //As a backup, search for this user instead var requestSettings = { type: "GET", url: appModel.get("accountsUrl") + "?query=" + encodeURIComponent(model.get("username")), success: function(data, textStatus, xhr) { //Parse the XML response to get user info model.set(model.parseXML(data)); //Trigger the change events model.trigger("change:isMemberOf"); model.trigger("change:isOwnerOf"); model.trigger("change:identities"); model.set("checked", true); }, error: function(){ //Set some blank values and flag as checked model.set("username", ""); model.set("fullName", ""); model.set("notFound", true); model.set("checked", true); } } //Send the request $.ajax(_.extend(requestSettings, appUserModel.createAjaxSettings())); } } } //Send the request $.ajax(_.extend(requestSettings, appUserModel.createAjaxSettings())); }, //Get the pending identity map requests, if the service is turned on getPendingIdentities: function(){ if(!appModel.get("pendingMapsUrl")) return false; var model = this; //Get the pending requests var requestSettings = { url: appModel.get("pendingMapsUrl") + encodeURIComponent(this.get("username")), success: function(data, textStatus, xhr){ //Reset the equivalent id list so we don't just add it to it with push() model.set("pending", model.defaults().pending); var pending = model.get("pending"); _.each($(data).find("person"), function(person, i) { //Don't list yourself as a pending map request var personsUsername = $(person).find("subject").text(); if(personsUsername.toLowerCase() == model.get("username").toLowerCase()) return; //Create a new User Model for this pending identity var pendingUser = new UserModel({ rawData: person }); if(pendingUser.isOrcid()) pendingUser.getInfo(); pending.push(pendingUser); }); model.set("pending", pending); model.trigger("change:pending"); //Trigger the change event }, error: function(xhr, textStatus){ if(xhr.responseText.indexOf("error code 34")){ model.set("pending", model.defaults().pending); model.trigger("change:pending"); //Trigger the change event } } } $.ajax(_.extend(requestSettings, appUserModel.createAjaxSettings())); }, getNameFromSubject: function(username){ var username = username || this.get("username"), fullName = ""; if(!username) return; var ldapUidAttribute = appModel.get("ldapUidAttribute") || "uid" || ""; if((username.indexOf(ldapUidAttribute + "=") > -1) && (username.indexOf(",") > -1)) fullName = username.substring(username.indexOf(ldapUidAttribute + "=") + 4, username.indexOf(",")); else if((username.indexOf("CN=") > -1) && (username.indexOf(",") > -1)) fullName = username.substring(username.indexOf("CN=") + 3, username.indexOf(",")); //Cut off the last string after the name when it contains digits - not part of this person's names if(fullName.lastIndexOf(" ") > fullName.indexOf(" ")){ var lastWord = fullName.substring(fullName.lastIndexOf(" ")); if(lastWord.search(/\d/) > -1) fullName = fullName.substring(0, fullName.lastIndexOf(" ")); } //Default to the username if(!fullName) fullName = this.get("fullname") || username; return fullName; }, isOrcid: function(orcid){ var username = (typeof orcid === "string")? orcid : this.get("username"); //Have we already verified this? if((typeof orcid == "undefined") && (username == this.get("orcid"))) return true; //Checks for ORCIDs using the orcid base URL as a prefix if(username.indexOf("http://orcid.org/") == 0){ this.set("orcid", username); return true; } //If the ORCID base url is not present, we will check if this is a 19-digit ORCID ID //A simple and fast check first //ORCiDs are 16 digits and 3 dashes - 19 characters if(username.length != 19) return false; /* The ORCID checksum algorithm to determine is a character string is an ORCiD * http://support.orcid.org/knowledgebase/articles/116780-structure-of-the-orcid-identifier */ var total = 0, baseDigits = username.replace(/-/g, "").substr(0, 15); for(var i=0; i<baseDigits.length; i++){ var digit = parseInt(baseDigits.charAt(i)); total = (total + digit) * 2; } var remainder = total % 11, result = (12 - remainder) % 11, checkDigit = (result == 10) ? "X" : result.toString(), isOrcid = (checkDigit == username.charAt(username.length-1)); if(isOrcid) this.set("orcid", username); return isOrcid; }, isNode: function(){ var node = _.where(nodeModel.get("members"), { shortIdentifier: this.get("username") }); return (node && node.length) }, // Will check if this user is a Member Node. If so, it will save the MN info to the model saveAsNode: function(){ if(!this.isNode()) return; var node = _.where(nodeModel.get("members"), { shortIdentifier: this.get("username") })[0]; this.set({ type: "node", logo: node.logo, description: node.description, node: node, fullName: node.name, usernameReadable: this.get("username") }); this.updateSearchModel(); this.set("checked", true); }, loginLdap: function(formData, success, error){ if(!formData || !appModel.get("signInUrlLdap")) return false; var model = this; var requestSettings = { type: "POST", url: appModel.get("signInUrlLdap") + window.location.href, data: formData, success: function(data, textStatus, xhr){ if(success) success(this); model.getToken(); //Direct to the Ldap sign in //window.location = appModel.get("signInUrlLdap") + window.location.href; }, error: function(){ /*if(error) error(this); */ model.getToken(); } } $.ajax(_.extend(requestSettings, appUserModel.createAjaxSettings())); }, logout: function(){ //Logout via the registry script if we are not using tokens if((typeof appModel.get("tokenUrl") == "undefined") || !appModel.get("tokenUrl")){ appView.registryView.logout(); return; } //Construct the sign out url and redirect var signOutUrl = appModel.get('signOutUrl'), target = Backbone.history.location.href; // DO NOT include the route otherwise we have an infinite redirect target = target.split("#")[0]; // make sure to include the target signOutUrl += "?target=" + target; // do it! window.location = signOutUrl; }, // call Metacat or the DataONE CN to validate the session and tell us the user's name checkStatus: function(onSuccess, onError) { var model = this; if (!appModel.get("tokenUrl")) { // look up the URL var metacatUrl = appModel.get('metacatServiceUrl'); // ajax call to validate the session/get the user info var requestSettings = { type: "POST", url: metacatUrl, data: { action: "validatesession" }, success: function(data, textStatus, xhr) { // the Metacat (XML) response should have a fullName element var username = $(data).find("name").text(); // set in the model model.set('username', username); //Are we logged in? if(username){ model.set("loggedIn", true); model.getInfo(); } else{ model.set("loggedIn", false); model.trigger("change:loggedIn"); model.set("checked", true); } if(onSuccess) onSuccess(data); }, error: function(data, textStatus, xhr){ //User is not logged in model.reset(); if(onError) onError(); } } $.ajax(_.extend(requestSettings, appUserModel.createAjaxSettings())); } else { // use the token method for checking authentication this.getToken(); } }, getToken: function(customCallback) { var tokenUrl = appModel.get('tokenUrl'); var model = this; if(!tokenUrl) return false; //Set up the function that will be called when we retrieve a token var callback = (typeof customCallback === "function") ? customCallback : function(data, textStatus, xhr) { // the response should have the token var payload = model.parseToken(data), username = payload ? payload.userId : null, fullName = payload ? payload.fullName : model.getNameFromSubject(username) || null, token = payload ? data : null, loggedIn = payload ? true : false; // set in the model model.set('fullName', fullName); model.set('username', username); model.set("token", token); model.set("loggedIn", loggedIn); model.getTokenExpiration(payload); if(username) model.getInfo(); else model.set("checked", true); }; // ajax call to get token var requestSettings = { type: "GET", dataType: "text", xhrFields: { withCredentials: true }, url: tokenUrl, data: {}, success: callback, error: function(xhr, textStatus, errorThrown){ model.set("checked", true); } } $.ajax(requestSettings); }, getTokenExpiration: function(payload){ if(!payload && this.get("token")) var payload = this.parseToken(this.get("token")); if(!payload) return; //The exp claim should be standard - it is in UTC seconds var expires = payload.exp? new Date(payload.exp * 1000) : null; //Use the issuedAt and ttl as a backup (only used in d1 2.0.0 and 2.0.1) if(!expires){ var issuedAt = payload.issuedAt? new Date(payload.issuedAt) : null, lifeSpan = payload.ttl? payload.ttl : null; if(issuedAt && lifeSpan && (lifeSpan > 99999)) issuedAt.setMilliseconds(lifeSpan); else if(issuedAt && lifeSpan) issuedAt.setSeconds(lifeSpan); expires = issuedAt; } this.set("expires", expires); }, checkToken: function(onSuccess, onError){ //First check if the token has expired if(appUserModel.get("expires") > new Date()){ if(onSuccess) onSuccess(); return; } var model = this; var url = appModel.get("tokenUrl"); if(!url) return; var requestSettings = { type: "GET", url: url, success: function(data, textStatus, xhr){ if(data){ // the response should have the token var payload = model.parseToken(data), username = payload ? payload.userId : null, fullName = payload ? payload.fullName : null, token = payload ? data : null, loggedIn = payload ? true : false; // set in the model model.set('fullName', fullName); model.set('username', username); model.set("token", token); model.set("loggedIn", loggedIn); model.getTokenExpiration(payload); appUserModel.set("checked", true); if(onSuccess) onSuccess(data, textStatus, xhr); } else if(onError) onError(data, textStatus, xhr); }, error: function(data, textStatus, xhr){ //If this token in invalid, then reset the user model/log out appUserModel.reset(); if(onError) onError(data, textStatus, xhr); } } $.ajax(_.extend(requestSettings, appUserModel.createAjaxSettings())); }, parseToken: function(token) { if(typeof token == "undefined") var token = this.get("token"); var jws = new KJUR.jws.JWS(); var result = 0; try { result = jws.parseJWS(token); } catch (ex) { result = 0; } if(!jws.parsedJWS) return ""; var payload = $.parseJSON(jws.parsedJWS.payloadS); return payload; }, verifyLoginStatus: function(){ if(!appUserModel.get("loggedIn")) return; if(appModel.get("tokenUrl")){ //If the user's token is no longer valid, then refresh the page appUserModel.checkToken(); } else{ appUserModel.checkStatus(); } }, update: function(onSuccess, onError){ var model = this; var person = '<?xml version="1.0" encoding="UTF-8"?>' + '<d1:person xmlns:d1="http://ns.dataone.org/service/types/v1">' + '<subject>' + this.get("username") + '</subject>' + '<givenName>' + this.get("firstName") + '</givenName>' + '<familyName>' + this.get("lastName") + '</familyName>' + '<email>' + this.get("email") + '</email>' + '</d1:person>'; var xmlBlob = new Blob([person], {type : 'application/xml'}); var formData = new FormData(); formData.append("subject", this.get("username")); formData.append("person", xmlBlob, "person"); var updateUrl = appModel.get("accountsUrl") + encodeURIComponent(this.get("username")); // ajax call to update var requestSettings = { type: "PUT", cache: false, contentType: false, processData: false, url: updateUrl, data: formData, success: function(data, textStatus, xhr) { if(typeof onSuccess != "undefined") onSuccess(data); //model.getInfo(); }, error: function(data, textStatus, xhr) { if(typeof onError != "undefined") onError(data); } } $.ajax(_.extend(requestSettings, appUserModel.createAjaxSettings())); }, confirmMapRequest: function(otherUsername, onSuccess, onError){ if(!otherUsername) return; var mapUrl = appModel.get("pendingMapsUrl") + encodeURIComponent(otherUsername), model = this; if(!onSuccess) var onSuccess = function(){}; if(!onError) var onError = function(){}; // ajax call to confirm map var requestSettings = { type: "PUT", url: mapUrl, success: function(data, textStatus, xhr) { if(onSuccess) onSuccess(data, textStatus, xhr); //Get updated info model.getInfo(); }, error: function(xhr, textStatus, error) { if(onError) onError(xhr, textStatus, error); } } $.ajax(_.extend(requestSettings, appUserModel.createAjaxSettings())); }, denyMapRequest: function(otherUsername, onSuccess, onError){ if(!otherUsername) return; var mapUrl = appModel.get("pendingMapsUrl") + encodeURIComponent(otherUsername), model = this; // ajax call to reject map var requestSettings = { type: "DELETE", url: mapUrl, success: function(data, textStatus, xhr) { if(typeof onSuccess == "function") onSuccess(data, textStatus, xhr); model.getInfo(); }, error: function(xhr, textStatus, error) { if(typeof onError == "function") onError(xhr, textStatus, error); } } $.ajax(_.extend(requestSettings, appUserModel.createAjaxSettings())); }, addMap: function(otherUsername, onSuccess, onError){ if(!otherUsername) return; var mapUrl = appModel.get("pendingMapsUrl"), model = this; // ajax call to map var requestSettings = { type: "POST", xhrFields: { withCredentials: true }, headers: { "Authorization": "Bearer " + this.get("token") }, url: mapUrl, data: { subject: otherUsername }, success: function(data, textStatus, xhr) { if(typeof onSuccess == "function") onSuccess(data, textStatus, xhr); model.getInfo(); }, error: function(xhr, textStatus, error) { if(typeof onError == "function") onError(xhr, textStatus, error); } } $.ajax(_.extend(requestSettings, appUserModel.createAjaxSettings())); }, removeMap: function(otherUsername, onSuccess, onError){ if(!otherUsername) return; var mapUrl = appModel.get("accountsMapsUrl") + encodeURIComponent(otherUsername), model = this; // ajax call to remove mapping var requestSettings = { type: "DELETE", url: mapUrl, success: function(data, textStatus, xhr) { if(typeof onSuccess == "function") onSuccess(data, textStatus, xhr); model.getInfo(); }, error: function(xhr, textStatus, error) { if(typeof onError == "function") onError(xhr, textStatus, error); } } $.ajax(_.extend(requestSettings, appUserModel.createAjaxSettings())); }, failedLdapLogin: function(){ this.set("loggedIn", false); this.set("checked", true); this.set("ldapError", true); }, pluckIdentityUsernames: function(){ var models = this.get("identities"), usernames = []; _.each(models, function(m){ usernames.push(m.get("username").toLowerCase()); }); this.set("identitiesUsernames", usernames); this.trigger("change:identitiesUsernames"); }, createReadableUsername: function(){ if(!this.get("username")) return; var username = this.get("username"), readableUsername = username.substring(username.indexOf("=")+1, username.indexOf(",")) || username; this.set("usernameReadable", readableUsername); }, createAjaxSettings: function(){ if(!this.get("token")){ return { xhrFields: { withCredentials: true } } } return { xhrFields: { withCredentials: true }, headers: { "Authorization": "Bearer " + this.get("token") } } }, reset: function(){ var defaults = _.omit(this.defaults(), ["searchModel", "searchResults"]); this.set(defaults); } }); return UserModel; });