using System; using System.Collections.Generic; using System.ComponentModel; using System.Linq; using System.Reflection; using System.Runtime.Serialization; using Umbraco.Core.Composing; using Umbraco.Core.Exceptions; using Umbraco.Core.Logging; using Umbraco.Core.Models.Entities; namespace Umbraco.Core.Models { /// /// Represents a Member object /// [Serializable] [DataContract(IsReference = true)] public class Member : ContentBase, IMember { private IDictionary _additionalData; private IMemberType _contentType; private readonly string _contentTypeAlias; private string _username; private string _email; private string _rawPasswordValue; private object _providerUserKey; /// /// Constructor for creating an empty Member object /// /// ContentType for the current Content object public Member(IMemberType contentType) : base("", -1, contentType, new PropertyCollection()) { _contentType = contentType ?? throw new ArgumentNullException(nameof(contentType)); _contentTypeAlias = contentType.Alias; IsApproved = true; //this cannot be null but can be empty _rawPasswordValue = ""; _email = ""; _username = ""; } /// /// Constructor for creating a Member object /// /// Name of the content /// ContentType for the current Content object public Member(string name, IMemberType contentType) : base(name, -1, contentType, new PropertyCollection()) { if (string.IsNullOrWhiteSpace(name)) throw new ArgumentNullOrEmptyException(nameof(name)); _contentType = contentType ?? throw new ArgumentNullException(nameof(contentType)); _contentTypeAlias = contentType.Alias; IsApproved = true; //this cannot be null but can be empty _rawPasswordValue = ""; _email = ""; _username = ""; } /// /// Constructor for creating a Member object /// /// /// /// /// public Member(string name, string email, string username, IMemberType contentType, bool isApproved = true) : base(name, -1, contentType, new PropertyCollection()) { if (string.IsNullOrWhiteSpace(email)) throw new ArgumentNullOrEmptyException(nameof(email)); if (string.IsNullOrWhiteSpace(name)) throw new ArgumentNullOrEmptyException(nameof(name)); if (string.IsNullOrWhiteSpace(username)) throw new ArgumentNullOrEmptyException(nameof(username)); _contentType = contentType ?? throw new ArgumentNullException(nameof(contentType)); _contentTypeAlias = contentType.Alias; _email = email; _username = username; IsApproved = isApproved; //this cannot be null but can be empty _rawPasswordValue = ""; } /// /// Constructor for creating a Member object /// /// /// /// /// /// The password value passed in to this parameter should be the encoded/encrypted/hashed format of the member's password /// /// public Member(string name, string email, string username, string rawPasswordValue, IMemberType contentType) : base(name, -1, contentType, new PropertyCollection()) { _contentType = contentType ?? throw new ArgumentNullException(nameof(contentType)); _contentTypeAlias = contentType.Alias; _email = email; _username = username; _rawPasswordValue = rawPasswordValue; IsApproved = true; } /// /// Constructor for creating a Member object /// /// /// /// /// /// The password value passed in to this parameter should be the encoded/encrypted/hashed format of the member's password /// /// /// public Member(string name, string email, string username, string rawPasswordValue, IMemberType contentType, bool isApproved) : base(name, -1, contentType, new PropertyCollection()) { _contentType = contentType ?? throw new ArgumentNullException(nameof(contentType)); _contentTypeAlias = contentType.Alias; _email = email; _username = username; _rawPasswordValue = rawPasswordValue; IsApproved = isApproved; } private static readonly Lazy Ps = new Lazy(); private class PropertySelectors { public readonly PropertyInfo UsernameSelector = ExpressionHelper.GetPropertyInfo(x => x.Username); public readonly PropertyInfo EmailSelector = ExpressionHelper.GetPropertyInfo(x => x.Email); public readonly PropertyInfo PasswordSelector = ExpressionHelper.GetPropertyInfo(x => x.RawPasswordValue); public readonly PropertyInfo ProviderUserKeySelector = ExpressionHelper.GetPropertyInfo(x => x.ProviderUserKey); } /// /// Gets or sets the Username /// [DataMember] public string Username { get { return _username; } set { SetPropertyValueAndDetectChanges(value, ref _username, Ps.Value.UsernameSelector); } } /// /// Gets or sets the Email /// [DataMember] public string Email { get { return _email; } set { SetPropertyValueAndDetectChanges(value, ref _email, Ps.Value.EmailSelector); } } /// /// Gets or sets the raw password value /// [IgnoreDataMember] public string RawPasswordValue { get { return _rawPasswordValue; } set { if (value == null) { //special case, this is used to ensure that the password is not updated when persisting, in this case //we don't want to track changes either _rawPasswordValue = null; } else { SetPropertyValueAndDetectChanges(value, ref _rawPasswordValue, Ps.Value.PasswordSelector); } } } /// /// Gets or sets the Groups that Member is part of /// [DataMember] public IEnumerable Groups { get; set; } //TODO: When get/setting all of these properties we MUST: // * Check if we are using the umbraco membership provider, if so then we need to use the configured fields - not the explicit fields below // * If any of the fields don't exist, what should we do? Currently it will throw an exception! /// /// Gets or sets the Password Question /// /// /// Alias: umbracoMemberPasswordRetrievalQuestion /// Part of the standard properties collection. /// [DataMember] public string PasswordQuestion { get { var a = WarnIfPropertyTypeNotFoundOnGet(Constants.Conventions.Member.PasswordQuestion, "PasswordQuestion", default(string)); if (a.Success == false) return a.Result; return Properties[Constants.Conventions.Member.PasswordQuestion].GetValue() == null ? string.Empty : Properties[Constants.Conventions.Member.PasswordQuestion].GetValue().ToString(); } set { if (WarnIfPropertyTypeNotFoundOnSet( Constants.Conventions.Member.PasswordQuestion, "PasswordQuestion") == false) return; Properties[Constants.Conventions.Member.PasswordQuestion].SetValue(value); } } /// /// Gets or sets the raw password answer value /// /// /// For security reasons this value should be encrypted, the encryption process is handled by the memberhip provider /// Alias: umbracoMemberPasswordRetrievalAnswer /// /// Part of the standard properties collection. /// [IgnoreDataMember] public string RawPasswordAnswerValue { get { var a = WarnIfPropertyTypeNotFoundOnGet(Constants.Conventions.Member.PasswordAnswer, "PasswordAnswer", default(string)); if (a.Success == false) return a.Result; return Properties[Constants.Conventions.Member.PasswordAnswer].GetValue() == null ? string.Empty : Properties[Constants.Conventions.Member.PasswordAnswer].GetValue().ToString(); } set { if (WarnIfPropertyTypeNotFoundOnSet( Constants.Conventions.Member.PasswordAnswer, "PasswordAnswer") == false) return; Properties[Constants.Conventions.Member.PasswordAnswer].SetValue(value); } } /// /// Gets or set the comments for the member /// /// /// Alias: umbracoMemberComments /// Part of the standard properties collection. /// [DataMember] public string Comments { get { var a = WarnIfPropertyTypeNotFoundOnGet(Constants.Conventions.Member.Comments, "Comments", default(string)); if (a.Success == false) return a.Result; return Properties[Constants.Conventions.Member.Comments].GetValue() == null ? string.Empty : Properties[Constants.Conventions.Member.Comments].GetValue().ToString(); } set { if (WarnIfPropertyTypeNotFoundOnSet( Constants.Conventions.Member.Comments, "Comments") == false) return; Properties[Constants.Conventions.Member.Comments].SetValue(value); } } /// /// Gets or sets a boolean indicating whether the Member is approved /// /// /// Alias: umbracoMemberApproved /// Part of the standard properties collection. /// [DataMember] public bool IsApproved { get { var a = WarnIfPropertyTypeNotFoundOnGet(Constants.Conventions.Member.IsApproved, "IsApproved", //This is the default value if the prop is not found true); if (a.Success == false) return a.Result; if (Properties[Constants.Conventions.Member.IsApproved].GetValue() == null) return true; var tryConvert = Properties[Constants.Conventions.Member.IsApproved].GetValue().TryConvertTo(); if (tryConvert.Success) { return tryConvert.Result; } //if the property exists but it cannot be converted, we will assume true return true; } set { if (WarnIfPropertyTypeNotFoundOnSet( Constants.Conventions.Member.IsApproved, "IsApproved") == false) return; Properties[Constants.Conventions.Member.IsApproved].SetValue(value); } } /// /// Gets or sets a boolean indicating whether the Member is locked out /// /// /// Alias: umbracoMemberLockedOut /// Part of the standard properties collection. /// [DataMember] public bool IsLockedOut { get { var a = WarnIfPropertyTypeNotFoundOnGet(Constants.Conventions.Member.IsLockedOut, "IsLockedOut", false); if (a.Success == false) return a.Result; if (Properties[Constants.Conventions.Member.IsLockedOut].GetValue() == null) return false; var tryConvert = Properties[Constants.Conventions.Member.IsLockedOut].GetValue().TryConvertTo(); if (tryConvert.Success) { return tryConvert.Result; } return false; //TODO: Use TryConvertTo instead } set { if (WarnIfPropertyTypeNotFoundOnSet( Constants.Conventions.Member.IsLockedOut, "IsLockedOut") == false) return; Properties[Constants.Conventions.Member.IsLockedOut].SetValue(value); } } /// /// Gets or sets the date for last login /// /// /// Alias: umbracoMemberLastLogin /// Part of the standard properties collection. /// [DataMember] public DateTime LastLoginDate { get { var a = WarnIfPropertyTypeNotFoundOnGet(Constants.Conventions.Member.LastLoginDate, "LastLoginDate", default(DateTime)); if (a.Success == false) return a.Result; if (Properties[Constants.Conventions.Member.LastLoginDate].GetValue() == null) return default(DateTime); var tryConvert = Properties[Constants.Conventions.Member.LastLoginDate].GetValue().TryConvertTo(); if (tryConvert.Success) { return tryConvert.Result; } return default(DateTime); //TODO: Use TryConvertTo instead } set { if (WarnIfPropertyTypeNotFoundOnSet( Constants.Conventions.Member.LastLoginDate, "LastLoginDate") == false) return; Properties[Constants.Conventions.Member.LastLoginDate].SetValue(value); } } /// /// Gest or sets the date for last password change /// /// /// Alias: umbracoMemberLastPasswordChangeDate /// Part of the standard properties collection. /// [DataMember] public DateTime LastPasswordChangeDate { get { var a = WarnIfPropertyTypeNotFoundOnGet(Constants.Conventions.Member.LastPasswordChangeDate, "LastPasswordChangeDate", default(DateTime)); if (a.Success == false) return a.Result; if (Properties[Constants.Conventions.Member.LastPasswordChangeDate].GetValue() == null) return default(DateTime); var tryConvert = Properties[Constants.Conventions.Member.LastPasswordChangeDate].GetValue().TryConvertTo(); if (tryConvert.Success) { return tryConvert.Result; } return default(DateTime); //TODO: Use TryConvertTo instead } set { if (WarnIfPropertyTypeNotFoundOnSet( Constants.Conventions.Member.LastPasswordChangeDate, "LastPasswordChangeDate") == false) return; Properties[Constants.Conventions.Member.LastPasswordChangeDate].SetValue(value); } } /// /// Gets or sets the date for when Member was locked out /// /// /// Alias: umbracoMemberLastLockoutDate /// Part of the standard properties collection. /// [DataMember] public DateTime LastLockoutDate { get { var a = WarnIfPropertyTypeNotFoundOnGet(Constants.Conventions.Member.LastLockoutDate, "LastLockoutDate", default(DateTime)); if (a.Success == false) return a.Result; if (Properties[Constants.Conventions.Member.LastLockoutDate].GetValue() == null) return default(DateTime); var tryConvert = Properties[Constants.Conventions.Member.LastLockoutDate].GetValue().TryConvertTo(); if (tryConvert.Success) { return tryConvert.Result; } return default(DateTime); //TODO: Use TryConvertTo instead } set { if (WarnIfPropertyTypeNotFoundOnSet( Constants.Conventions.Member.LastLockoutDate, "LastLockoutDate") == false) return; Properties[Constants.Conventions.Member.LastLockoutDate].SetValue(value); } } /// /// Gets or sets the number of failed password attempts. /// This is the number of times the password was entered incorrectly upon login. /// /// /// Alias: umbracoMemberFailedPasswordAttempts /// Part of the standard properties collection. /// [DataMember] public int FailedPasswordAttempts { get { var a = WarnIfPropertyTypeNotFoundOnGet(Constants.Conventions.Member.FailedPasswordAttempts, "FailedPasswordAttempts", 0); if (a.Success == false) return a.Result; if (Properties[Constants.Conventions.Member.FailedPasswordAttempts].GetValue() == null) return default(int); var tryConvert = Properties[Constants.Conventions.Member.FailedPasswordAttempts].GetValue().TryConvertTo(); if (tryConvert.Success) { return tryConvert.Result; } return default(int); //TODO: Use TryConvertTo instead } set { if (WarnIfPropertyTypeNotFoundOnSet( Constants.Conventions.Member.FailedPasswordAttempts, "FailedPasswordAttempts") == false) return; Properties[Constants.Conventions.Member.FailedPasswordAttempts].SetValue(value); } } /// /// String alias of the default ContentType /// [DataMember] public virtual string ContentTypeAlias { get { return _contentTypeAlias; } } /// /// User key from the Provider. /// /// /// When using standard umbraco provider this key will /// correspond to the guid UniqueId/Key. /// Otherwise it will the one available from the asp.net /// membership provider. /// [DataMember] public virtual object ProviderUserKey { get { return _providerUserKey; } set { SetPropertyValueAndDetectChanges(value, ref _providerUserKey, Ps.Value.ProviderUserKeySelector); } } /// /// Method to call when Entity is being saved /// /// Created date is set and a Unique key is assigned internal override void AddingEntity() { base.AddingEntity(); if (ProviderUserKey == null) ProviderUserKey = Key; } /// /// Gets the ContentType used by this content object /// [IgnoreDataMember] public IMemberType ContentType { get { return _contentType; } } /* Internal experiment - only used for mapping queries. * Adding these to have first level properties instead of the Properties collection. */ [IgnoreDataMember] internal string LongStringPropertyValue { get; set; } [IgnoreDataMember] internal string ShortStringPropertyValue { get; set; } [IgnoreDataMember] internal int IntegerPropertyValue { get; set; } [IgnoreDataMember] internal bool BoolPropertyValue { get; set; } [IgnoreDataMember] internal DateTime DateTimePropertyValue { get; set; } [IgnoreDataMember] internal string PropertyTypeAlias { get; set; } private Attempt WarnIfPropertyTypeNotFoundOnGet(string propertyAlias, string propertyName, T defaultVal) { void DoLog(string logPropertyAlias, string logPropertyName) { Current.Logger.Warn("Trying to access the '{PropertyName}' property on '{MemberType}' " + "but the {PropertyAlias} property does not exist on the member type so a default value is returned. " + "Ensure that you have a property type with alias: {PropertyAlias} configured on your member type in order to use the '{PropertyName}' property on the model correctly.", logPropertyName, typeof(Member), logPropertyAlias); } // if the property doesn't exist, if (Properties.Contains(propertyAlias) == false) { // put a warn in the log if this entity has been persisted // then return a failure if (HasIdentity) DoLog(propertyAlias, propertyName); return Attempt.Fail(defaultVal); } return Attempt.Succeed(); } private bool WarnIfPropertyTypeNotFoundOnSet(string propertyAlias, string propertyName) { void DoLog(string logPropertyAlias, string logPropertyName) { Current.Logger.Warn("An attempt was made to set a value on the property '{PropertyName}' on type '{MemberType}' but the " + "property type {PropertyAlias} does not exist on the member type, ensure that this property type exists so that setting this property works correctly.", logPropertyName, typeof(Member), logPropertyAlias); } // if the property doesn't exist, if (Properties.Contains(propertyAlias) == false) { // put a warn in the log if this entity has been persisted // then return a failure if (HasIdentity) DoLog(propertyAlias, propertyName); return false; } return true; } public override object DeepClone() { var clone = (Member)base.DeepClone(); //turn off change tracking clone.DisableChangeTracking(); //need to manually clone this since it's not settable clone._contentType = (IMemberType)ContentType.DeepClone(); //this shouldn't really be needed since we're not tracking clone.ResetDirtyProperties(false); //re-enable tracking clone.EnableChangeTracking(); return clone; } /// [DataMember] [DoNotClone] public IDictionary AdditionalData => _additionalData ?? (_additionalData = new Dictionary()); /// [IgnoreDataMember] public bool HasAdditionalData => _additionalData != null; } }