using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Collections.Specialized; using System.Linq; using System.Reflection; using System.Runtime.Serialization; using Umbraco.Core.Configuration; using Umbraco.Core.Models.EntityBase; namespace Umbraco.Core.Models.Membership { /// /// Represents a backoffice user /// /// /// Should be internal until a proper user/membership implementation /// is part of the roadmap. /// [Serializable] [DataContract(IsReference = true)] public class User : Entity, IUser { public User(IUserType userType) { if (userType == null) throw new ArgumentNullException("userType"); _userType = userType; _defaultPermissions = _userType.Permissions; //Groups = new List { userType }; SessionTimeout = 60; _sectionCollection = new ObservableCollection(); _addedSections = new List(); _removedSections = new List(); _language = GlobalSettings.DefaultUILanguage; _sectionCollection.CollectionChanged += SectionCollectionChanged; _isApproved = true; _isLockedOut = false; _startContentId = -1; _startMediaId = -1; //cannot be null _rawPasswordValue = ""; } public User(string name, string email, string username, string rawPasswordValue, IUserType userType) : this(userType) { _name = name; _email = email; _username = username; _rawPasswordValue = rawPasswordValue; _isApproved = true; _isLockedOut = false; _startContentId = -1; _startMediaId = -1; } private IUserType _userType; private string _name; private List _addedSections; private List _removedSections; private ObservableCollection _sectionCollection; private int _sessionTimeout; private int _startContentId; private int _startMediaId; private string _username; private string _email; private string _rawPasswordValue; private bool _isApproved; private bool _isLockedOut; private string _language; private IEnumerable _defaultPermissions; private bool _defaultToLiveEditing; private static readonly PropertyInfo SessionTimeoutSelector = ExpressionHelper.GetPropertyInfo(x => x.SessionTimeout); private static readonly PropertyInfo StartContentIdSelector = ExpressionHelper.GetPropertyInfo(x => x.StartContentId); private static readonly PropertyInfo StartMediaIdSelector = ExpressionHelper.GetPropertyInfo(x => x.StartMediaId); private static readonly PropertyInfo AllowedSectionsSelector = ExpressionHelper.GetPropertyInfo>(x => x.AllowedSections); private static readonly PropertyInfo NameSelector = ExpressionHelper.GetPropertyInfo(x => x.Name); private static readonly PropertyInfo UsernameSelector = ExpressionHelper.GetPropertyInfo(x => x.Username); private static readonly PropertyInfo EmailSelector = ExpressionHelper.GetPropertyInfo(x => x.Email); private static readonly PropertyInfo PasswordSelector = ExpressionHelper.GetPropertyInfo(x => x.RawPasswordValue); private static readonly PropertyInfo IsLockedOutSelector = ExpressionHelper.GetPropertyInfo(x => x.IsLockedOut); private static readonly PropertyInfo IsApprovedSelector = ExpressionHelper.GetPropertyInfo(x => x.IsApproved); private static readonly PropertyInfo LanguageSelector = ExpressionHelper.GetPropertyInfo(x => x.Language); private static readonly PropertyInfo DefaultPermissionsSelector = ExpressionHelper.GetPropertyInfo>(x => x.DefaultPermissions); private static readonly PropertyInfo DefaultToLiveEditingSelector = ExpressionHelper.GetPropertyInfo(x => x.DefaultToLiveEditing); private static readonly PropertyInfo UserTypeSelector = ExpressionHelper.GetPropertyInfo(x => x.UserType); #region Implementation of IMembershipUser [IgnoreDataMember] public object ProviderUserKey { get { return Id; } set { throw new NotSupportedException("Cannot set the provider user key for a user"); } } [DataMember] public string Username { get { return _username; } set { SetPropertyValueAndDetectChanges(o => { _username = value; return _username; }, _username, UsernameSelector); } } [DataMember] public string Email { get { return _email; } set { SetPropertyValueAndDetectChanges(o => { _email = value; return _email; }, _email, EmailSelector); } } [DataMember] public string RawPasswordValue { get { return _rawPasswordValue; } set { SetPropertyValueAndDetectChanges(o => { _rawPasswordValue = value; return _rawPasswordValue; }, _rawPasswordValue, PasswordSelector); } } [DataMember] public bool IsApproved { get { return _isApproved; } set { SetPropertyValueAndDetectChanges(o => { _isApproved = value; return _isApproved; }, _isApproved, IsApprovedSelector); } } [DataMember] public bool IsLockedOut { get { return _isLockedOut; } set { SetPropertyValueAndDetectChanges(o => { _isLockedOut = value; return _isLockedOut; }, _isLockedOut, IsLockedOutSelector); } } //TODO: Figure out how to support all of this! - we cannot have NotImplementedExceptions because these get used by the IMembershipMemberService service so // we'll just have them as generic get/set which don't interact with the db. [IgnoreDataMember] public string PasswordQuestion { get; set; } [IgnoreDataMember] public string RawPasswordAnswerValue { get; set; } [IgnoreDataMember] public string Comments { get; set; } [IgnoreDataMember] public DateTime LastLoginDate { get; set; } [IgnoreDataMember] public DateTime LastPasswordChangeDate { get; set; } [IgnoreDataMember] public DateTime LastLockoutDate { get; set; } [IgnoreDataMember] public int FailedPasswordAttempts { get; set; } #endregion #region Implementation of IUser [DataMember] public string Name { get { return _name; } set { SetPropertyValueAndDetectChanges(o => { _name = value; return _name; }, _name, NameSelector); } } public IEnumerable AllowedSections { get { return _sectionCollection; } } public void RemoveAllowedSection(string sectionAlias) { if (_sectionCollection.Contains(sectionAlias)) { _sectionCollection.Remove(sectionAlias); } } public void AddAllowedSection(string sectionAlias) { if (_sectionCollection.Contains(sectionAlias) == false) { _sectionCollection.Add(sectionAlias); } } public IProfile ProfileData { get { return new UserProfile(this); } } /// /// Used internally to check if we need to add a section in the repository to the db /// internal IEnumerable AddedSections { get { return _addedSections; } } /// /// Used internally to check if we need to remove a section in the repository to the db /// internal IEnumerable RemovedSections { get { return _removedSections; } } /// /// Gets or sets the session timeout. /// /// /// The session timeout. /// [DataMember] public int SessionTimeout { get { return _sessionTimeout; } set { SetPropertyValueAndDetectChanges(o => { _sessionTimeout = value; return _sessionTimeout; }, _sessionTimeout, SessionTimeoutSelector); } } /// /// Gets or sets the start content id. /// /// /// The start content id. /// [DataMember] public int StartContentId { get { return _startContentId; } set { SetPropertyValueAndDetectChanges(o => { _startContentId = value; return _startContentId; }, _startContentId, StartContentIdSelector); } } /// /// Gets or sets the start media id. /// /// /// The start media id. /// [DataMember] public int StartMediaId { get { return _startMediaId; } set { SetPropertyValueAndDetectChanges(o => { _startMediaId = value; return _startMediaId; }, _startMediaId, StartMediaIdSelector); } } [DataMember] public string Language { get { return _language; } set { SetPropertyValueAndDetectChanges(o => { _language = value; return _language; }, _language, LanguageSelector); } } [DataMember] public IEnumerable DefaultPermissions { get { return _defaultPermissions; } set { SetPropertyValueAndDetectChanges(o => { _defaultPermissions = value; return _defaultPermissions; }, _defaultPermissions, DefaultPermissionsSelector); } } [IgnoreDataMember] internal bool DefaultToLiveEditing { get { return _defaultToLiveEditing; } set { SetPropertyValueAndDetectChanges(o => { _defaultToLiveEditing = value; return _defaultToLiveEditing; }, _defaultToLiveEditing, DefaultToLiveEditingSelector); } } [IgnoreDataMember] public IUserType UserType { get { return _userType; } set { if (value.HasIdentity == false) { throw new InvalidOperationException("Cannot assign a User Type that has not been persisted"); } SetPropertyValueAndDetectChanges(o => { _userType = value; return _userType; }, _userType, UserTypeSelector); } } #endregion /// /// Whenever resetting occurs, clear the remembered add/removed collections, even if /// rememberPreviouslyChangedProperties is true, the AllowedSections property will still /// be flagged as dirty. /// /// public override void ResetDirtyProperties(bool rememberPreviouslyChangedProperties) { _addedSections.Clear(); _removedSections.Clear(); base.ResetDirtyProperties(rememberPreviouslyChangedProperties); } /// /// Handles the collection changed event in order for us to flag the AllowedSections property as changed /// /// /// void SectionCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) { OnPropertyChanged(AllowedSectionsSelector); if (e.Action == NotifyCollectionChangedAction.Add) { var item = e.NewItems.Cast().First(); if (_addedSections.Contains(item) == false) { _addedSections.Add(item); } } else if (e.Action == NotifyCollectionChangedAction.Remove) { var item = e.OldItems.Cast().First(); if (_removedSections.Contains(item) == false) { _removedSections.Add(item); } } } public override object DeepClone() { var clone = (User)base.DeepClone(); //need to create new collections otherwise they'll get copied by ref clone._addedSections = new List(); clone._removedSections = new List(); clone._sectionCollection = new ObservableCollection(_sectionCollection.ToList()); //re-create the event handler clone._sectionCollection.CollectionChanged += clone.SectionCollectionChanged; clone.ResetDirtyProperties(false); return clone; } /// /// Internal class used to wrap the user in a profile /// private class UserProfile : IProfile { private readonly IUser _user; public UserProfile(IUser user) { _user = user; } public object Id { get { return _user.Id; } set { _user.Id = (int)value; } } public string Name { get { return _user.Name; } set { _user.Name = value; } } protected bool Equals(UserProfile other) { return _user.Equals(other._user); } public override bool Equals(object obj) { if (ReferenceEquals(null, obj)) return false; if (ReferenceEquals(this, obj)) return true; if (obj.GetType() != this.GetType()) return false; return Equals((UserProfile) obj); } public override int GetHashCode() { return _user.GetHashCode(); } } } }