//--------------------------------------------------------------------------------------------------------------------- /// <summary>Returns the existing user with the specified username or creates that user with default account settings.</summary> /// <param name="context">The execution environment context.</param> /// <param name="username">The username.</param> /// <param name="authenticationType">The used authentication type. This parameter can be null. If specified, the username is looked for only among the external usernames of the specified authentication type, otherwise among the web portal's own usernames.</param> /// <returns>The User instance. New users are not stored in the database, this must be done by the calling code.</returns> public static User GetOrCreate(IfyContext context, string username, AuthenticationType authenticationType) { User result = null; Activity activity = null; int userId = GetUserId(context, username, authenticationType); if (userId != 0) { var oldAccessLevel = context.AccessLevel; context.AccessLevel = EntityAccessLevel.Administrator; result = FromId(context, userId); context.AccessLevel = oldAccessLevel; return(result); } else { IfyWebContext webContext = context as IfyWebContext; result = User.GetInstance(context); result.Username = username; result.AccountStatus = (authenticationType == null ? webContext == null ? AccountStatusType.Deactivated : webContext.DefaultAccountStatus : authenticationType.GetDefaultAccountStatus()); result.Level = UserLevel.User; result.TimeZone = "UTC"; result.IsNormalAccount = true; AuthenticationType authType; authType = IfyWebContext.GetAuthenticationType(typeof(PasswordAuthenticationType)); result.PasswordAuthenticationAllowed = (authType != null && authType.NormalAccountRule == RuleApplicationType.Always); authType = IfyWebContext.GetAuthenticationType(typeof(PasswordAuthenticationType)); result.SessionlessRequestsAllowed = (authType != null && authType.NormalAccountRule == RuleApplicationType.Always); activity = new Activity(context, result, EntityOperationType.Create); activity.Store(); return(result); } }
/// <summary> /// Initializes a new instance of the <see cref="Terradue.Tep.WebServer.WebUserTep"/> class. /// </summary> /// <param name="entity">Entity.</param> public WebUserTep(IfyWebContext context, UserTep entity, bool umsso = false) : base(entity) { if (umsso) { AuthenticationType umssoauthType = IfyWebContext.GetAuthenticationType(typeof(UmssoAuthenticationType)); var umssoUser = umssoauthType.GetUserProfile(context, HttpContext.Current.Request, false); if (umssoUser != null) { this.UmssoEmail = umssoUser.Email; } } //only current user can know the api key if (context.UserId == entity.Id) { this.ApiKey = entity.ApiKey; this.T2ProfileError = HttpContext.Current.Session["t2profileError"] as string; if ((string.IsNullOrEmpty(entity.Affiliation) || string.IsNullOrEmpty(entity.Country) || string.IsNullOrEmpty(entity.FirstName) || string.IsNullOrEmpty(entity.LastName))) { this.T2ProfileError += (string.IsNullOrEmpty(this.T2ProfileError) ? "" : "\n") + "Profile not complete"; } this.T2ApiKey = entity.GetSessionApiKey(); } if (context.UserId == entity.Id || context.UserLevel == UserLevel.Administrator) { this.T2Username = entity.TerradueCloudUsername; if (context.GetConfigBooleanValue("accounting-enabled")) { this.Balance = entity.GetAccountingBalance(); } this.Roles = GetUserCommunityRoles(context, entity); if (context.UserLevel == UserLevel.Administrator) { if (entity.RegistrationDate == DateTime.MinValue) { entity.LoadRegistrationInfo(); } this.RegistrationDate = entity.RegistrationDate; } } else { this.Email = null; this.Affiliation = null; this.Level = 0; this.AccountStatus = 0; this.DomainId = null; } }
/// <summary> /// This method allows user to request the confirmation email /// </summary> /// <param name="request">Request.</param> public object Post(SendUserEmailConfirmationEmail request) { var context = TepWebContext.GetWebContext(PagePrivileges.UserView); try { context.Open(); context.LogInfo(this, string.Format("/user/emailconfirm POST")); context.LogError(this, string.Format("Email already confirmed for user {0}", context.Username)); return(new HttpError(System.Net.HttpStatusCode.BadRequest, new InvalidOperationException("Account does not require email confirmation"))); } catch (PendingActivationException) { context.LogDebug(this, string.Format("Pending activation for user {0}", context.Username)); AuthenticationType umssoauthType = IfyWebContext.GetAuthenticationType(typeof(UmssoAuthenticationType)); var umssoUser = umssoauthType.GetUserProfile(context, HttpContext.Current.Request, false); if (umssoUser == null) { context.LogError(this, string.Format("User not logged in UMSSO")); return(new HttpError(System.Net.HttpStatusCode.BadRequest, new UnauthorizedAccessException("Not logged in UM-SSO"))); } if (Request.Headers["Umsso-Person-Email"] != umssoUser.Email) { umssoUser.Email = Request.Headers["Umsso-Person-Email"]; umssoUser.Store(); context.LogError(this, string.Format("Confirmation email and UM-SSO email do not match")); } string emailFrom = context.GetConfigValue("MailSenderAddress"); string subject = context.GetConfigValue("RegistrationMailSubject"); subject = subject.Replace("$(SITENAME)", context.GetConfigValue("SiteName")); string confirmUrl = context.GetConfigValue("EmailConfirmationUrl").Replace("$(BASEURL)", context.GetConfigValue("BaseUrl")).Replace("$(TOKEN)", umssoUser.ActivationToken); string body = context.GetConfigValue("RegistrationMailBody"); body = body.Replace("$(USERNAME)", umssoUser.Username); body = body.Replace("$(SITENAME)", context.GetConfigValue("SiteName")); body = body.Replace("$(ACTIVATIONURL)", confirmUrl); context.SendMail(emailFrom, umssoUser.Email, subject, body); return(new HttpResult(new EmailConfirmationMessage() { Status = "sent", Email = umssoUser.Email })); } }
public object Get(CallBackRequest request) { var redirect = ""; TepWebContext context = new TepWebContext(PagePrivileges.EverybodyView); UserTep user = null; try { context.Open(); context.LogInfo(this, string.Format("/cb GET")); if (!string.IsNullOrEmpty(request.error)) { context.LogError(this, request.error); context.EndSession(); return(OAuthUtils.DoRedirect(context.BaseUrl, false)); } Connect2IdClient client = new Connect2IdClient(context.GetConfigValue("sso-configUrl")); client.SSOAuthEndpoint = context.GetConfigValue("sso-authEndpoint"); client.SSOApiClient = context.GetConfigValue("sso-clientId"); client.SSOApiSecret = context.GetConfigValue("sso-clientSecret"); client.SSOApiToken = context.GetConfigValue("sso-apiAccessToken"); client.RedirectUri = context.GetConfigValue("sso-callback"); OauthTokenResponse tokenresponse; try { tokenresponse = client.AccessToken(request.Code); DBCookie.StoreDBCookie(context, context.GetConfigValue("cookieID-token-access"), tokenresponse.access_token, null, tokenresponse.expires_in); DBCookie.StoreDBCookie(context, context.GetConfigValue("cookieID-token-refresh"), tokenresponse.refresh_token, null); DBCookie.StoreDBCookie(context, context.GetConfigValue("cookieID-token-id"), tokenresponse.id_token, null, tokenresponse.expires_in); } catch (Exception e) { DBCookie.DeleteDBCookie(context, context.GetConfigValue("cookieID-token-access")); DBCookie.DeleteDBCookie(context, context.GetConfigValue("cookieID-token-refresh")); DBCookie.DeleteDBCookie(context, context.GetConfigValue("cookieID-token-id")); throw e; } TepLdapAuthenticationType auth = (TepLdapAuthenticationType)IfyWebContext.GetAuthenticationType(typeof(TepLdapAuthenticationType)); auth.SetConnect2IdCLient(client); auth.TrustEmail = true; user = (UserTep)auth.GetUserProfile(context); if (user == null) { throw new Exception("Unable to load user"); } context.LogDebug(this, string.Format("Loaded user '{0}'", user.Username)); if (string.IsNullOrEmpty(user.Email)) { throw new Exception("Invalid email"); } context.StartSession(auth, user); context.SetUserInformation(auth, user); DBCookie.StoreDBCookie(context, context.GetConfigValue("cookieID-token-access"), tokenresponse.access_token, user.Username, tokenresponse.expires_in); DBCookie.StoreDBCookie(context, context.GetConfigValue("cookieID-token-refresh"), tokenresponse.refresh_token, user.Username); DBCookie.StoreDBCookie(context, context.GetConfigValue("cookieID-token-id"), tokenresponse.id_token, user.Username, tokenresponse.expires_in); redirect = context.GetConfigValue("dashboard_page"); if (string.IsNullOrEmpty(redirect)) { redirect = context.GetConfigValue("BaseUrl"); } if (!string.IsNullOrEmpty(HttpContext.Current.Session["return_to"] as string)) { redirect = HttpContext.Current.Session["return_to"] as string; HttpContext.Current.Session["return_to"] = null; } context.Close(); } catch (Exception e) { context.LogError(this, e.Message, e); context.Close(); throw e; } return(OAuthUtils.DoRedirect(redirect, false)); }
/// <summary> /// Gets the user profile. /// </summary> /// <returns>The user profile.</returns> /// <param name="context">Context.</param> /// <param name="request">Request.</param> public override User GetUserProfile(IfyWebContext context, HttpRequest request = null, bool strict = false) { User usr = null; AuthenticationType authType = IfyWebContext.GetAuthenticationType(typeof(TepOauthAuthenticationType)); var refreshCookie = Client.LoadTokenRefresh(); var accessCookie = Client.LoadTokenAccess(); var refreshToken = refreshCookie.Value; var accessToken = accessCookie.Value; TimeSpan span = accessCookie.Expire.Subtract(DateTime.UtcNow); var refresh = (!string.IsNullOrEmpty(refreshToken) && (string.IsNullOrEmpty(accessToken) || span.TotalMinutes < context.GetConfigIntegerValue("AccessTokenExpireMinutes"))); if (refresh) { context.LogDebug(this, "Refresh token"); // refresh the token try { Client.RefreshToken(refreshToken, refreshCookie.Username); refreshToken = Client.LoadTokenRefresh().Value; accessToken = Client.LoadTokenAccess().Value; } catch (Exception e) { context.LogError(this, e.Message); return(null); } } if (!string.IsNullOrEmpty(accessToken)) { context.LogDebug(this, "Get user info"); TepOauthUserInfoResponse usrInfo; try { usrInfo = Client.GetUserInfo <TepOauthUserInfoResponse>(accessToken); } catch (Exception e) { context.LogError(this, e.Message); return(null); } if (usrInfo == null) { return(null); } context.LogDebug(this, "Get user info - sub = " + usrInfo.sub); context.AccessLevel = EntityAccessLevel.Administrator; //first try to get user from username try { usr = User.FromUsername(context, usrInfo.screenname); usr.AccountStatus = AccountStatusType.Enabled; } catch (Exception) { try { usr = User.GetOrCreate(context, usrInfo.sub, authType); if (usr.Id == 0) { usr.Username = usrInfo.preferred_username; } usr.AccountStatus = AccountStatusType.Enabled; } catch (Exception e2) { Client.RevokeSessionCookies(); throw e2; } } bool isnew = (usr.Id == 0); //update user infos if (!string.IsNullOrEmpty(usrInfo.given_name)) { usr.FirstName = usrInfo.given_name; } if (!string.IsNullOrEmpty(usrInfo.email)) { usr.Email = usrInfo.email; } if (!string.IsNullOrEmpty(usrInfo.family_name)) { usr.LastName = usrInfo.family_name; } if (isnew && !string.IsNullOrEmpty(usrInfo.screenname)) { usr.Username = usrInfo.screenname; } usr.Store(); if (isnew) { usr.LinkToAuthenticationProvider(authType, usrInfo.sub); } //roles if (usrInfo.roles != null) { //for now, if user has a role, he can process if (usr.Level < UserLevel.Manager) { usr.Level = UserLevel.Manager; usr.Store(); } UserTep usrtep = UserTep.FromId(context, usr.Id); //Add role to domains // foreach(var rolestring in usrInfo.roles){ // var roleIdentifier = rolestring.Substring(rolestring.LastIndexOf("-") + 1); // var domainIdentifier = rolestring.Substring(0,rolestring.LastIndexOf("-")); // var domain = ActivationFactory.GetOrCreateDomainForActivation(context, domainIdentifier); // var role = ActivationFactory.GetOrCreateRole(context, roleIdentifier); // role.GrantToUser(usrtep.Id, domain.Id); // } //Remove role if user does not have it anymore // var communities = usrtep.GetUserCommunities(); // foreach (var community in communities) { // try { // var roles = usrtep.GetUserRoles(community); // if (roles.Count > 0) { // foreach (var role in roles){ // bool exists = false; // foreach(var rolestring in usrInfo.roles){ // var roleIdentifier = rolestring.Substring(rolestring.LastIndexOf("-") + 1); // var domainIdentifier = rolestring.Substring(0,rolestring.LastIndexOf("-")); // if(community.Identifier == domainIdentifier && role.Identifier == roleIdentifier) exists = true; // } // if(!exists) role.RevokeFromUser(usr, community); // } // } // } catch (Exception e) { // context.LogError(this, e.Message); // } // } } return(usr); } return(null); }
/// <summary> /// Gets the user profile. /// </summary> /// <returns>The user profile.</returns> /// <param name="context">Context.</param> /// <param name="request">Request.</param> public override User GetUserProfile(IfyWebContext context, HttpRequest request, bool strict) { string ConfigurationFile = context.SiteConfigFolder + Path.DirectorySeparatorChar + "auth.xml"; XmlDocument authDoc = new XmlDocument(); try { authDoc.Load(ConfigurationFile); foreach (XmlNode typeNode in authDoc.SelectNodes("/externalAuthentication/method[@active='true']/accountType")) { XmlElement methodElem = typeNode.ParentNode as XmlElement; XmlElement typeElem = typeNode as XmlElement; if (typeElem == null || methodElem == null) { continue; } // The received "Host" header must match exactly the value of the "host" attribute. if (methodElem.HasAttribute("host") && methodElem.Attributes["host"].Value != HttpContext.Current.Request.Headers["Host"]) { continue; } // The request origin ("REMOTE_HOST" server variable) must have (or be) the same IP address as the hostname (or IP address) in the value of the "remoteHost" attribute. if (methodElem.HasAttribute("remoteHost") && !context.IsRequestFromHost(methodElem.Attributes["remoteHost"].Value)) { continue; } bool match = true; foreach (XmlNode conditionNode in typeElem.SelectNodes("condition")) { XmlElement conditionElem = conditionNode as XmlElement; if (conditionElem == null) { continue; } string value = null, pattern = null; if (conditionElem.HasAttribute("header")) { value = HttpContext.Current.Request.Headers[conditionElem.Attributes["header"].Value]; } else { continue; } if (conditionElem.HasAttribute("pattern")) { pattern = conditionElem.Attributes["pattern"].Value; } else { continue; } if (value == null || pattern == null) { continue; } if (!Regex.Match(value, pattern).Success) { match = false; break; } } if (!match) { continue; } XmlElement loginElem = typeElem["login"]; if (loginElem == null) { continue; } // Get username from <login> element string externalUsername = null; if (loginElem.HasAttribute("header")) { externalUsername = HttpContext.Current.Request.Headers[loginElem.Attributes["header"].Value]; } if (externalUsername == null) { continue; } //context.IsUserIdentified = true; AuthenticationType authType = IfyWebContext.GetAuthenticationType(typeof(UmssoAuthenticationType)); bool exists = User.DoesUserExist(context, externalUsername, authType); User user = User.GetOrCreate(context, externalUsername, authType); bool register = !exists && loginElem.HasAttribute("register") && loginElem.Attributes["register"].Value == "true"; bool refresh = exists && loginElem.HasAttribute("refresh") && loginElem.Attributes["refresh"].Value == "true"; context.LogInfo(this, string.Format("EO-SSO Get user : {0}", user.Username)); context.LogDebug(this, string.Format("EO-SSO Get user : exists = {0} -- refresh = {1}", exists, refresh)); // If username was not found and automatic registration is configured, create new user // If username was found return with success //if (register) user.AccountStatus = AccountStatusType.PendingActivation; string email = null; foreach (XmlElement elem in loginElem.ChildNodes) { if (register || refresh) { if (elem == null) { continue; } string value = null; if (elem.HasAttribute("header")) { value = HttpContext.Current.Request.Headers[elem.Attributes["header"].Value]; } context.LogDebug(this, string.Format("EO-SSO Get user : {0} = {1}", elem.Name, value)); if (!string.IsNullOrEmpty(value)) { switch (elem.Name) { case "firstName": user.FirstName = value; break; case "lastName": user.LastName = value; break; case "email": email = value; break; case "affiliation": user.Affiliation = value; break; case "country": user.Country = value; break; case "credits": int credits; int.TryParse(value, out credits); user.TotalCredits = credits; break; case "proxyUsername": //user.ProxyUsername = value; break; case "proxyPassword": //user.ProxyPassword = value; break; } } } else { if (elem.HasAttribute("header") && elem.Name.Equals("email")) { user.Email = HttpContext.Current.Request.Headers[elem.Attributes["header"].Value]; break; } } } if (register && user.Username.Contains("@")) { user.Username = user.Username.Substring(0, user.Username.IndexOf("@")).Replace(".", ""); } if (refresh) { user.Store(); } //we do not store the email in case of email change if (!string.IsNullOrEmpty(email)) { user.Email = email; } return(user); } return(null); } catch (Exception e) { throw new Exception("Invalid authentication settings" + " " + e.Message + " " + e.StackTrace); } }
/// <summary> /// This method allows user to confirm its email adress with a token key /// </summary> /// <param name="request">Request.</param> public object Get(ConfirmUserEmail request) { var context = TepWebContext.GetWebContext(PagePrivileges.UserView); // Let's try to open context try { context.LogInfo(this, string.Format("/user/emailconfirm GET")); context.Open(); context.LogError(this, string.Format("Email already confirmed for user {0}", context.Username)); context.Close(); return(new HttpError(System.Net.HttpStatusCode.MethodNotAllowed, new InvalidOperationException("Email already confirmed"))); } catch (Exception e) { AuthenticationType authType = IfyWebContext.GetAuthenticationType(typeof(TokenAuthenticationType)); AuthenticationType umssoauthType = IfyWebContext.GetAuthenticationType(typeof(UmssoAuthenticationType)); var umssoUser = umssoauthType.GetUserProfile(context, HttpContext.Current.Request, false); if (umssoUser == null) { context.LogError(this, string.Format("User not logged in EOSSO")); throw new ResourceNotFoundException("Not logged in EO-SSO"); } if (e is PendingActivationException) { context.LogDebug(this, string.Format("Pending activation for user {0}", context.Username)); // User is logged, now we confirm the email with the token context.LogDebug(this, string.Format("User now logged -- Confirm email with token")); User tokenUser = ((TokenAuthenticationType)authType).AuthenticateUser(context, request.Token); // We must check that the logged user if the one that received the email // If not, we rollback to previous status if (tokenUser.Email != Request.Headers["Umsso-Person-Email"]) { tokenUser.AccountStatus = AccountStatusType.PendingActivation; tokenUser.Store(); context.LogError(this, string.Format("Confirmation email and UM-SSO email do not match")); return(new HttpError(System.Net.HttpStatusCode.BadRequest, new UnauthorizedAccessException("Confirmation email and UM-SSO email do not match"))); } context.LogDebug(this, string.Format("User now logged -- Email confirmed")); //send an email to Support to warn them try { string emailFrom = context.GetConfigValue("MailSenderAddress"); string subject = string.Format("[{0}] - Email verification for user {1}", context.GetConfigValue("SiteName"), umssoUser.Username); string body = context.GetConfigValue("EmailConfirmedNotification"); body = body.Replace("$(USERNAME)", umssoUser.Username); body = body.Replace("$(EMAIL)", umssoUser.Email); context.SendMail(emailFrom, emailFrom, subject, body); } catch (Exception e1) { context.LogError(this, e1.Message, e1); } } else { context.LogError(this, e.Message, e); throw e; } } context.Close(); return(new WebResponseBool(true)); }
public override User GetUserProfile(IfyWebContext context, HttpRequest request = null, bool strict = false) { NewUserCreated = false; UserTep usr = null; AuthenticationType authType = IfyWebContext.GetAuthenticationType(typeof(TepLdapAuthenticationType)); var tokenrefresh = DBCookie.LoadDBCookie(context, context.GetConfigValue("cookieID-token-refresh")); var tokenaccess = DBCookie.LoadDBCookie(context, context.GetConfigValue("cookieID-token-access")); context.LogDebug(this, string.Format("GetUserProfile -- tokenrefresh = {0} ; tokenaccess = {1}", tokenrefresh.Value, tokenaccess.Value)); if (!string.IsNullOrEmpty(tokenrefresh.Value) && DateTime.UtcNow > tokenaccess.Expire) { // refresh the token try { var tokenresponse = client.RefreshToken(tokenrefresh.Value); DBCookie.StoreDBCookie(context, context.GetConfigValue("cookieID-token-access"), tokenresponse.access_token, tokenaccess.Username, tokenresponse.expires_in); DBCookie.StoreDBCookie(context, context.GetConfigValue("cookieID-token-refresh"), tokenresponse.refresh_token, tokenrefresh.Username); DBCookie.StoreDBCookie(context, context.GetConfigValue("cookieID-token-id"), tokenresponse.id_token, tokenrefresh.Username, tokenresponse.expires_in); tokenaccess = DBCookie.LoadDBCookie(context, context.GetConfigValue("cookieID-token-access")); context.LogDebug(this, string.Format("GetUserProfile - refresh -- tokenrefresh = {0} ; tokenaccess = {1}", tokenrefresh.Value, tokenaccess.Value)); } catch (Exception) { return(null); } } if (!string.IsNullOrEmpty(tokenaccess.Value)) { OauthUserInfoResponse usrInfo = client.GetUserInfo(tokenaccess.Value); context.LogDebug(this, string.Format("GetUserProfile -- usrInfo")); if (usrInfo == null) { return(null); } context.LogDebug(this, string.Format("GetUserProfile -- usrInfo = {0}", usrInfo.sub)); //Check if association auth / username exists int userId = User.GetUserId(context, usrInfo.sub, authType); bool userHasAuthAssociated = userId != 0; //user has ldap auth associated to his account if (userHasAuthAssociated) { //User exists, we load it usr = UserTep.FromId(context, userId); //test if TerradueCloudUsername was set if (string.IsNullOrEmpty(usr.TerradueCloudUsername)) { usr.LoadCloudUsername(); if (string.IsNullOrEmpty(usr.TerradueCloudUsername)) { usr.TerradueCloudUsername = usrInfo.sub; usr.StoreCloudUsername(); } } //update user infos if (!string.IsNullOrEmpty(usrInfo.given_name)) { usr.FirstName = usrInfo.given_name; } if (!string.IsNullOrEmpty(usrInfo.family_name)) { usr.LastName = usrInfo.family_name; } if (!string.IsNullOrEmpty(usrInfo.zoneinfo)) { usr.TimeZone = usrInfo.zoneinfo; } if (!string.IsNullOrEmpty(usrInfo.locale)) { usr.Language = usrInfo.locale; } return(usr); } if (string.IsNullOrEmpty(usrInfo.email)) { throw new Exception("Null email returned by the Oauth mechanism, please contact support."); } //user does not have ldap auth associated to his account try { //check if a user with the same email exists usr = UserTep.FromEmail(context, usrInfo.email); //user with the same email exists but not yet associated to ldap auth usr.LinkToAuthenticationProvider(authType, usrInfo.sub); return(usr); //TODO: what about if user Cloud username is different ? force to new one ? } catch (Exception e) { context.LogError(this, e.Message); } //user with this email does not exist, we should create it usr = (UserTep)User.GetOrCreate(context, usrInfo.sub, authType); usr.Level = UserCreationDefaultLevel; //update user infos if (!string.IsNullOrEmpty(usrInfo.given_name)) { usr.FirstName = usrInfo.given_name; } if (!string.IsNullOrEmpty(usrInfo.family_name)) { usr.LastName = usrInfo.family_name; } if (!string.IsNullOrEmpty(usrInfo.email) && (TrustEmail || usrInfo.email_verifier)) { usr.Email = usrInfo.email; } if (!string.IsNullOrEmpty(usrInfo.zoneinfo)) { usr.TimeZone = usrInfo.zoneinfo; } if (!string.IsNullOrEmpty(usrInfo.locale)) { usr.Language = usrInfo.locale; } if (usr.Id == 0) { usr.AccessLevel = EntityAccessLevel.Administrator; NewUserCreated = true; } usr.Store(); usr.LinkToAuthenticationProvider(authType, usrInfo.sub); usr.TerradueCloudUsername = usrInfo.sub; usr.StoreCloudUsername(); return(usr); } else { } context.LogDebug(this, string.Format("GetUserProfile -- return null")); return(null); }