using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Data.Common;
using System.Data.SqlClient;
using System.Linq;
using Umbraco.Core.Events;
using Umbraco.Core.Logging;
using Umbraco.Core.Models.Membership;
using Umbraco.Core.Persistence;
using Umbraco.Core.Persistence.Querying;
using Umbraco.Core.Persistence.UnitOfWork;
using Umbraco.Core.Security;
namespace Umbraco.Core.Services
{
///
/// Represents the UserService, which is an easy access to operations involving , and eventually Backoffice Users.
///
public class UserService : RepositoryService, IUserService
{
//TODO: We need to change the isUpgrading flag to use an app state enum as described here: http://issues.umbraco.org/issue/U4-6816
// in the meantime, we will use a boolean which we are currently using during upgrades to ensure that a user object is not persisted during this phase, otherwise
// exceptions can occur if the db is not in it's correct state.
internal bool IsUpgrading { get; set; }
public UserService(IDatabaseUnitOfWorkProvider provider, RepositoryFactory repositoryFactory, ILogger logger, IEventMessagesFactory eventMessagesFactory)
: base(provider, repositoryFactory, logger, eventMessagesFactory)
{
IsUpgrading = false;
}
#region Implementation of IMembershipUserService
///
/// Gets the default MemberType alias
///
/// By default we'll return the 'writer', but we need to check it exists. If it doesn't we'll
/// return the first type that is not an admin, otherwise if there's only one we will return that one.
/// Alias of the default MemberType
public string GetDefaultMemberType()
{
using (var repository = RepositoryFactory.CreateUserTypeRepository(UowProvider.GetUnitOfWork()))
{
var types = repository.GetAll().Select(x => x.Alias).ToArray();
if (types.Any() == false)
{
throw new EntityNotFoundException("No member types could be resolved");
}
if (types.InvariantContains("writer"))
{
return types.First(x => x.InvariantEquals("writer"));
}
if (types.Length == 1)
{
return types.First();
}
//first that is not admin
return types.First(x => x.InvariantEquals("admin") == false);
}
}
///
/// Checks if a User with the username exists
///
/// Username to check
/// True if the User exists otherwise False
public bool Exists(string username)
{
using (var repository = RepositoryFactory.CreateUserRepository(UowProvider.GetUnitOfWork()))
{
return repository.Exists(username);
}
}
///
/// Creates a new User
///
/// The user will be saved in the database and returned with an Id
/// Username of the user to create
/// Email of the user to create
/// which the User should be based on
///
public IUser CreateUserWithIdentity(string username, string email, IUserType userType)
{
return CreateUserWithIdentity(username, email, "", userType);
}
///
/// Creates and persists a new
///
/// Username of the to create
/// Email of the to create
/// This value should be the encoded/encrypted/hashed value for the password that will be stored in the database
/// Alias of the Type
///
IUser IMembershipMemberService.CreateWithIdentity(string username, string email, string passwordValue, string memberTypeAlias)
{
var userType = GetUserTypeByAlias(memberTypeAlias);
if (userType == null)
{
throw new EntityNotFoundException("The user type " + memberTypeAlias + " could not be resolved");
}
return CreateUserWithIdentity(username, email, passwordValue, userType);
}
///
/// Creates and persists a Member
///
/// Using this method will persist the Member object before its returned
/// meaning that it will have an Id available (unlike the CreateMember method)
/// Username of the Member to create
/// Email of the Member to create
/// This value should be the encoded/encrypted/hashed value for the password that will be stored in the database
/// MemberType the Member should be based on
///
private IUser CreateUserWithIdentity(string username, string email, string passwordValue, IUserType userType)
{
if (userType == null) throw new ArgumentNullException("userType");
//TODO: PUT lock here!!
var uow = UowProvider.GetUnitOfWork();
using (var repository = RepositoryFactory.CreateUserRepository(uow))
{
var loginExists = uow.Database.ExecuteScalar("SELECT COUNT(id) FROM umbracoUser WHERE userLogin = @Login", new { Login = username }) != 0;
if (loginExists)
throw new ArgumentException("Login already exists");
var user = new User(userType)
{
DefaultToLiveEditing = false,
Email = email,
Language = Configuration.GlobalSettings.DefaultUILanguage,
Name = username,
RawPasswordValue = passwordValue,
Username = username,
StartContentId = -1,
StartMediaId = -1,
IsLockedOut = false,
IsApproved = true
};
//adding default sections content and media
user.AddAllowedSection("content");
user.AddAllowedSection("media");
if (SavingUser.IsRaisedEventCancelled(new SaveEventArgs(user), this))
return user;
repository.AddOrUpdate(user);
uow.Commit();
SavedUser.RaiseEvent(new SaveEventArgs(user, false), this);
return user;
}
}
///
/// Gets a User by its integer id
///
/// Id
///
public IUser GetById(int id)
{
using (var repository = RepositoryFactory.CreateUserRepository(UowProvider.GetUnitOfWork()))
{
var user = repository.Get((int)id);
return user;
}
}
///
/// Gets an by its provider key
///
/// Id to use for retrieval
///
public IUser GetByProviderKey(object id)
{
var asInt = id.TryConvertTo();
if (asInt.Success)
{
return GetById((int)id);
}
return null;
}
///
/// Get an by email
///
/// Email to use for retrieval
///
public IUser GetByEmail(string email)
{
using (var repository = RepositoryFactory.CreateUserRepository(UowProvider.GetUnitOfWork()))
{
var query = Query.Builder.Where(x => x.Email.Equals(email));
var user = repository.GetByQuery(query).FirstOrDefault();
return user;
}
}
///
/// Get an by username
///
/// Username to use for retrieval
///
public IUser GetByUsername(string username)
{
using (var repository = RepositoryFactory.CreateUserRepository(UowProvider.GetUnitOfWork()))
{
var query = Query.Builder.Where(x => x.Username.Equals(username));
var user = repository.GetByQuery(query).FirstOrDefault();
return user;
}
}
///
/// Deletes an
///
/// to Delete
public void Delete(IUser membershipUser)
{
//disable
membershipUser.IsApproved = false;
//can't rename if it's going to take up too many chars
if (membershipUser.Username.Length + 9 <= 125)
{
membershipUser.Username = DateTime.Now.ToString("yyyyMMdd") + "_" + membershipUser.Username;
}
Save(membershipUser);
}
///
/// This is simply a helper method which essentially just wraps the MembershipProvider's ChangePassword method
///
///
/// This method exists so that Umbraco developers can use one entry point to create/update users if they choose to.
///
/// The user to save the password for
/// The password to save
public void SavePassword(IUser user, string password)
{
if (user == null) throw new ArgumentNullException("user");
var provider = MembershipProviderExtensions.GetUsersMembershipProvider();
if (provider.IsUmbracoMembershipProvider() == false)
throw new NotSupportedException("When using a non-Umbraco membership provider you must change the user password by using the MembershipProvider.ChangePassword method");
provider.ChangePassword(user.Username, "", password);
//go re-fetch the member and update the properties that may have changed
var result = GetByUsername(user.Username);
if (result != null)
{
//should never be null but it could have been deleted by another thread.
user.RawPasswordValue = result.RawPasswordValue;
user.LastPasswordChangeDate = result.LastPasswordChangeDate;
user.UpdateDate = user.UpdateDate;
}
}
///
/// Deletes or disables a User
///
/// to delete
/// True to permanently delete the user, False to disable the user
public void Delete(IUser user, bool deletePermanently)
{
if (deletePermanently == false)
{
Delete(user);
}
else
{
if (DeletingUser.IsRaisedEventCancelled(new DeleteEventArgs(user), this))
return;
var uow = UowProvider.GetUnitOfWork();
using (var repository = RepositoryFactory.CreateUserRepository(uow))
{
repository.Delete(user);
uow.Commit();
}
DeletedUser.RaiseEvent(new DeleteEventArgs(user, false), this);
}
}
///
/// Saves an
///
/// to Save
/// Optional parameter to raise events.
/// Default is True otherwise set to False to not raise events
public void Save(IUser entity, bool raiseEvents = true)
{
if (raiseEvents)
{
if (SavingUser.IsRaisedEventCancelled(new SaveEventArgs(entity), this))
return;
}
var uow = UowProvider.GetUnitOfWork();
using (var repository = RepositoryFactory.CreateUserRepository(uow))
{
repository.AddOrUpdate(entity);
try
{
uow.Commit();
}
catch (DbException ex)
{
//Special case, if we are upgrading and an exception occurs, just continue
if (IsUpgrading == false) throw;
Logger.WarnWithException("An error occurred attempting to save a user instance during upgrade, normally this warning can be ignored", ex);
return;
}
}
if (raiseEvents)
SavedUser.RaiseEvent(new SaveEventArgs(entity, false), this);
}
///
/// Saves a list of objects
///
/// to save
/// Optional parameter to raise events.
/// Default is True otherwise set to False to not raise events
public void Save(IEnumerable entities, bool raiseEvents = true)
{
if (raiseEvents)
{
if (SavingUser.IsRaisedEventCancelled(new SaveEventArgs(entities), this))
return;
}
var uow = UowProvider.GetUnitOfWork();
using (var repository = RepositoryFactory.CreateUserRepository(uow))
{
foreach (var member in entities)
{
repository.AddOrUpdate(member);
}
//commit the whole lot in one go
uow.Commit();
}
if (raiseEvents)
SavedUser.RaiseEvent(new SaveEventArgs(entities, false), this);
}
///
/// Finds a list of objects by a partial email string
///
/// Partial email string to match
/// Current page index
/// Size of the page
/// Total number of records found (out)
/// The type of match to make as . Default is
///
public IEnumerable FindByEmail(string emailStringToMatch, int pageIndex, int pageSize, out int totalRecords, StringPropertyMatchType matchType = StringPropertyMatchType.StartsWith)
{
var uow = UowProvider.GetUnitOfWork();
using (var repository = RepositoryFactory.CreateUserRepository(uow))
{
var query = new Query();
switch (matchType)
{
case StringPropertyMatchType.Exact:
query.Where(member => member.Email.Equals(emailStringToMatch));
break;
case StringPropertyMatchType.Contains:
query.Where(member => member.Email.Contains(emailStringToMatch));
break;
case StringPropertyMatchType.StartsWith:
query.Where(member => member.Email.StartsWith(emailStringToMatch));
break;
case StringPropertyMatchType.EndsWith:
query.Where(member => member.Email.EndsWith(emailStringToMatch));
break;
case StringPropertyMatchType.Wildcard:
query.Where(member => member.Email.SqlWildcard(emailStringToMatch, TextColumnType.NVarchar));
break;
default:
throw new ArgumentOutOfRangeException("matchType");
}
return repository.GetPagedResultsByQuery(query, pageIndex, pageSize, out totalRecords, dto => dto.Email);
}
}
///
/// Finds a list of objects by a partial username
///
/// Partial username to match
/// Current page index
/// Size of the page
/// Total number of records found (out)
/// The type of match to make as . Default is
///
public IEnumerable FindByUsername(string login, int pageIndex, int pageSize, out int totalRecords, StringPropertyMatchType matchType = StringPropertyMatchType.StartsWith)
{
var uow = UowProvider.GetUnitOfWork();
using (var repository = RepositoryFactory.CreateUserRepository(uow))
{
var query = new Query();
switch (matchType)
{
case StringPropertyMatchType.Exact:
query.Where(member => member.Username.Equals(login));
break;
case StringPropertyMatchType.Contains:
query.Where(member => member.Username.Contains(login));
break;
case StringPropertyMatchType.StartsWith:
query.Where(member => member.Username.StartsWith(login));
break;
case StringPropertyMatchType.EndsWith:
query.Where(member => member.Username.EndsWith(login));
break;
case StringPropertyMatchType.Wildcard:
query.Where(member => member.Email.SqlWildcard(login, TextColumnType.NVarchar));
break;
default:
throw new ArgumentOutOfRangeException("matchType");
}
return repository.GetPagedResultsByQuery(query, pageIndex, pageSize, out totalRecords, dto => dto.Username);
}
}
///
/// Gets the total number of Users based on the count type
///
///
/// The way the Online count is done is the same way that it is done in the MS SqlMembershipProvider - We query for any members
/// that have their last active date within the Membership.UserIsOnlineTimeWindow (which is in minutes). It isn't exact science
/// but that is how MS have made theirs so we'll follow that principal.
///
/// to count by
/// with number of Users for passed in type
public int GetCount(MemberCountType countType)
{
using (var repository = RepositoryFactory.CreateUserRepository(UowProvider.GetUnitOfWork()))
{
IQuery query;
switch (countType)
{
case MemberCountType.All:
query = new Query();
return repository.Count(query);
case MemberCountType.Online:
throw new NotImplementedException();
//var fromDate = DateTime.Now.AddMinutes(-Membership.UserIsOnlineTimeWindow);
//query =
// Query.Builder.Where(
// x =>
// ((Member)x).PropertyTypeAlias == Constants.Conventions.Member.LastLoginDate &&
// ((Member)x).DateTimePropertyValue > fromDate);
//return repository.GetCountByQuery(query);
case MemberCountType.LockedOut:
query =
Query.Builder.Where(
x => x.IsLockedOut);
return repository.GetCountByQuery(query);
case MemberCountType.Approved:
query =
Query.Builder.Where(
x => x.IsApproved);
return repository.GetCountByQuery(query);
default:
throw new ArgumentOutOfRangeException("countType");
}
}
}
///
/// Gets a list of paged objects
///
/// Current page index
/// Size of the page
/// Total number of records found (out)
///
public IEnumerable GetAll(int pageIndex, int pageSize, out int totalRecords)
{
var uow = UowProvider.GetUnitOfWork();
using (var repository = RepositoryFactory.CreateUserRepository(uow))
{
return repository.GetPagedResultsByQuery(null, pageIndex, pageSize, out totalRecords, member => member.Username);
}
}
#endregion
#region Implementation of IUserService
///
/// Gets an IProfile by User Id.
///
/// Id of the User to retrieve
///
public IProfile GetProfileById(int id)
{
var user = GetUserById(id);
return user.ProfileData;
}
///
/// Gets a profile by username
///
/// Username
///
public IProfile GetProfileByUserName(string username)
{
var user = GetByUsername(username);
return user.ProfileData;
}
///
/// Gets a user by Id
///
/// Id of the user to retrieve
///
public IUser GetUserById(int id)
{
using (var repository = RepositoryFactory.CreateUserRepository(UowProvider.GetUnitOfWork()))
{
return repository.Get(id);
}
}
///
/// Replaces the same permission set for a single user to any number of entities
///
/// If no 'entityIds' are specified all permissions will be removed for the specified user.
/// Id of the user
/// Permissions as enumerable list of
/// Specify the nodes to replace permissions for. If nothing is specified all permissions are removed.
public void ReplaceUserPermissions(int userId, IEnumerable permissions, params int[] entityIds)
{
var uow = UowProvider.GetUnitOfWork();
using (var repository = RepositoryFactory.CreateUserRepository(uow))
{
repository.ReplaceUserPermissions(userId, permissions, entityIds);
}
}
///
/// Assigns the same permission set for a single user to any number of entities
///
/// Id of the user
///
/// Specify the nodes to replace permissions for
public void AssignUserPermission(int userId, char permission, params int[] entityIds)
{
var uow = UowProvider.GetUnitOfWork();
using (var repository = RepositoryFactory.CreateUserRepository(uow))
{
repository.AssignUserPermission(userId, permission, entityIds);
}
}
///
/// Gets all UserTypes or thosed specified as parameters
///
/// Optional Ids of UserTypes to retrieve
/// An enumerable list of
public IEnumerable GetAllUserTypes(params int[] ids)
{
var uow = UowProvider.GetUnitOfWork();
using (var repository = RepositoryFactory.CreateUserTypeRepository(uow))
{
return repository.GetAll(ids);
}
}
///
/// Gets a UserType by its Alias
///
/// Alias of the UserType to retrieve
///
public IUserType GetUserTypeByAlias(string alias)
{
using (var repository = RepositoryFactory.CreateUserTypeRepository(UowProvider.GetUnitOfWork()))
{
var query = Query.Builder.Where(x => x.Alias == alias);
var contents = repository.GetByQuery(query);
return contents.SingleOrDefault();
}
}
///
/// Gets a UserType by its Id
///
/// Id of the UserType to retrieve
///
public IUserType GetUserTypeById(int id)
{
using (var repository = RepositoryFactory.CreateUserTypeRepository(UowProvider.GetUnitOfWork()))
{
return repository.Get(id);
}
}
///
/// Gets a UserType by its Name
///
/// Name of the UserType to retrieve
///
public IUserType GetUserTypeByName(string name)
{
using (var repository = RepositoryFactory.CreateUserTypeRepository(UowProvider.GetUnitOfWork()))
{
var query = Query.Builder.Where(x => x.Name == name);
var contents = repository.GetByQuery(query);
return contents.SingleOrDefault();
}
}
///
/// Saves a UserType
///
/// UserType to save
/// Optional parameter to raise events.
/// Default is True otherwise set to False to not raise events
public void SaveUserType(IUserType userType, bool raiseEvents = true)
{
if (raiseEvents)
{
if (SavingUserType.IsRaisedEventCancelled(new SaveEventArgs(userType), this))
return;
}
var uow = UowProvider.GetUnitOfWork();
using (var repository = RepositoryFactory.CreateUserTypeRepository(uow))
{
repository.AddOrUpdate(userType);
uow.Commit();
}
if (raiseEvents)
SavedUserType.RaiseEvent(new SaveEventArgs(userType, false), this);
}
///
/// Deletes a UserType
///
/// UserType to delete
public void DeleteUserType(IUserType userType)
{
if (DeletingUserType.IsRaisedEventCancelled(new DeleteEventArgs(userType), this))
return;
var uow = UowProvider.GetUnitOfWork();
using (var repository = RepositoryFactory.CreateUserTypeRepository(uow))
{
repository.Delete(userType);
uow.Commit();
}
DeletedUserType.RaiseEvent(new DeleteEventArgs(userType, false), this);
}
///
/// Removes a specific section from all users
///
/// This is useful when an entire section is removed from config
/// Alias of the section to remove
public void DeleteSectionFromAllUsers(string sectionAlias)
{
var uow = UowProvider.GetUnitOfWork();
using (var repository = RepositoryFactory.CreateUserRepository(uow))
{
var assignedUsers = repository.GetUsersAssignedToSection(sectionAlias);
foreach (var user in assignedUsers)
{
//now remove the section for each user and commit
user.RemoveAllowedSection(sectionAlias);
repository.AddOrUpdate(user);
}
uow.Commit();
}
}
///
/// Add a specific section to all users or those specified as parameters
///
/// This is useful when a new section is created to allow specific users accessing it
/// Alias of the section to add
/// Specifiying nothing will add the section to all user
public void AddSectionToAllUsers(string sectionAlias, params int[] userIds)
{
var uow = UowProvider.GetUnitOfWork();
using (var repository = RepositoryFactory.CreateUserRepository(uow))
{
IEnumerable users;
if (userIds.Any())
{
users = repository.GetAll(userIds);
}
else
{
users = repository.GetAll();
}
foreach (var user in users.Where(u => !u.AllowedSections.InvariantContains(sectionAlias)))
{
//now add the section for each user and commit
user.AddAllowedSection(sectionAlias);
repository.AddOrUpdate(user);
}
uow.Commit();
}
}
///
/// Get permissions set for a user and optional node ids
///
/// If no permissions are found for a particular entity then the user's default permissions will be applied
/// User to retrieve permissions for
/// Specifiying nothing will return all user permissions for all nodes
/// An enumerable list of
public IEnumerable GetPermissions(IUser user, params int[] nodeIds)
{
var uow = UowProvider.GetUnitOfWork();
using (var repository = RepositoryFactory.CreateUserRepository(uow))
{
var explicitPermissions = repository.GetUserPermissionsForEntities(user.Id, nodeIds);
//if no permissions are assigned to a particular node then we will fill in those permissions with the user's defaults
var result = new List(explicitPermissions);
var missingIds = nodeIds.Except(result.Select(x => x.EntityId));
foreach (var id in missingIds)
{
result.Add(
new EntityPermission(
user.Id,
id,
user.DefaultPermissions.ToArray()));
}
return result;
}
}
#endregion
///
/// Occurs before Save
///
public static event TypedEventHandler> SavingUser;
///
/// Occurs after Save
///
public static event TypedEventHandler> SavedUser;
///
/// Occurs before Delete
///
public static event TypedEventHandler> DeletingUser;
///
/// Occurs after Delete
///
public static event TypedEventHandler> DeletedUser;
///
/// Occurs before Save
///
public static event TypedEventHandler> SavingUserType;
///
/// Occurs after Save
///
public static event TypedEventHandler> SavedUserType;
///
/// Occurs before Delete
///
public static event TypedEventHandler> DeletingUserType;
///
/// Occurs after Delete
///
public static event TypedEventHandler> DeletedUserType;
}
}