/*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 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 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 = '' + '' + '' + this.get("username") + '' + '' + this.get("firstName") + '' + '' + this.get("lastName") + '' + '' + this.get("email") + '' + ''; 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; });