/// <summary> /// Updates the last login, writes to the person's history log, and saves changes to the database /// </summary> /// <param name="userName">Name of the user.</param> public static void UpdateLastLogin(string userName) { if (string.IsNullOrWhiteSpace(userName)) { return; } int? personId = null; bool impersonated = userName.StartsWith("rckipid="); using (var rockContext = new RockContext()) { if (!impersonated) { var userLogin = new UserLoginService(rockContext).Queryable().Where(a => a.UserName == userName).FirstOrDefault(); if (userLogin != null) { userLogin.LastLoginDateTime = RockDateTime.Now; personId = userLogin.PersonId; rockContext.SaveChanges(); } } else { var impersonationToken = userName.Substring(8); personId = new PersonService(rockContext).GetByImpersonationToken(impersonationToken, false, null)?.Id; } } if (personId == null) { return; } var relatedDataBuilder = new System.Text.StringBuilder(); int?relatedEntityTypeId = null; int?relatedEntityId = null; if (impersonated) { var impersonatedByUser = HttpContext.Current?.Session?["ImpersonatedByUser"] as UserLogin; relatedEntityTypeId = EntityTypeCache.GetId <Rock.Model.Person>(); relatedEntityId = impersonatedByUser?.PersonId; if (impersonatedByUser != null) { relatedDataBuilder.Append($" impersonated by { impersonatedByUser.Person.FullName }"); } } if (HttpContext.Current != null && HttpContext.Current.Request != null) { string cleanUrl = PersonToken.ObfuscateRockMagicToken(HttpContext.Current.Request.UrlProxySafe().AbsoluteUri); // obfuscate the URL specified in the returnurl, just in case it contains any sensitive information (like a rckipid) Regex returnurlRegEx = new Regex(@"returnurl=([^&]*)"); cleanUrl = returnurlRegEx.Replace(cleanUrl, "returnurl=XXXXXXXXXXXXXXXXXXXXXXXXXXXX"); string clientIPAddress; try { clientIPAddress = Rock.Web.UI.RockPage.GetClientIpAddress(); } catch { // if we get an exception getting the IP Address, just ignore it clientIPAddress = ""; } relatedDataBuilder.AppendFormat( " to <span class='field-value'>{0}</span>, from <span class='field-value'>{1}</span>", cleanUrl, clientIPAddress); } var historyChangeList = new History.HistoryChangeList(); var historyChange = historyChangeList.AddChange(History.HistoryVerb.Login, History.HistoryChangeType.Record, userName); if (relatedDataBuilder.Length > 0) { historyChange.SetRelatedData(relatedDataBuilder.ToString(), null, null); } var historyList = HistoryService.GetChanges(typeof(Rock.Model.Person), Rock.SystemGuid.Category.HISTORY_PERSON_ACTIVITY.AsGuid(), personId.Value, historyChangeList, null, null, null, null, null); if (historyList.Any()) { Task.Run(async() => { // Wait 1 second to allow all post save actions to complete await Task.Delay(1000); try { using (var rockContext = new RockContext()) { rockContext.BulkInsert(historyList); } } catch (SystemException ex) { ExceptionLogService.LogException(ex, null); } }); } }
/// <summary> /// This method is called in the /// <see cref="M:Rock.Data.Model`1.PreSaveChanges(Rock.Data.DbContext,System.Data.Entity.Infrastructure.DbEntityEntry,System.Data.Entity.EntityState)" /> /// method. Use it to populate <see cref="P:Rock.Data.Model`1.HistoryItems" /> if needed. /// These history items are queued to be written into the database post save (so that they /// are only written if the save actually occurs). /// </summary> /// <param name="dbContext">The database context.</param> /// <param name="entry">The entry.</param> /// <param name="state">The state.</param> protected override void BuildHistoryItems(Data.DbContext dbContext, DbEntityEntry entry, EntityState state) { // Sometimes, especially if the model is being deleted, some properties might not be // populated, but we can query to try to get their original value. We need to use a new // rock context to get the actual value from the DB var rockContext = new RockContext(); var service = new FinancialPersonSavedAccountService(rockContext); var originalModel = service.Queryable("PersonAlias, FinancialPaymentDetail") .FirstOrDefault(fpsa => fpsa.Id == Id); // Use the original value for the person alias or the new value if that is not set var personId = (originalModel?.PersonAlias ?? PersonAlias)?.PersonId; if (!personId.HasValue) { // If this model is new, it won't have any virtual properties hydrated or an original // record in the database if (PersonAliasId.HasValue) { var personAliasService = new PersonAliasService(rockContext); var personAlias = personAliasService.Get(PersonAliasId.Value); personId = personAlias?.PersonId; } // We can't log history if we don't know who the saved account belongs to if (!personId.HasValue) { return; } } History.HistoryVerb verb; switch (state) { case EntityState.Added: verb = History.HistoryVerb.Add; break; case EntityState.Deleted: verb = History.HistoryVerb.Delete; break; default: // As of now, there is no requirement to log other events return; } var historyChangeList = new History.HistoryChangeList(); historyChangeList.AddChange(verb, History.HistoryChangeType.Record, "Financial Person Saved Account"); HistoryItems = HistoryService.GetChanges( typeof(Person), Rock.SystemGuid.Category.HISTORY_PERSON.AsGuid(), personId.Value, historyChangeList, GetNameForHistory(originalModel?.FinancialPaymentDetail ?? FinancialPaymentDetail), typeof(FinancialPersonSavedAccount), Id, dbContext.GetCurrentPersonAlias()?.Id, dbContext.SourceOfChange); }
/// <summary> /// This method is called in the /// <see cref="M:Rock.Data.Model`1.PreSaveChanges(Rock.Data.DbContext,System.Data.Entity.Infrastructure.DbEntityEntry,System.Data.Entity.EntityState)" /> /// method. Use it to populate <see cref="P:Rock.Data.Model`1.HistoryItems" /> if needed. /// These history items are queued to be written into the database post save (so that they /// are only written if the save actually occurs). /// </summary> /// <param name="dbContext">The database context.</param> /// <param name="entry">The entry.</param> /// <param name="state">The state.</param> protected override void BuildHistoryItems(Data.DbContext dbContext, DbEntityEntry entry, EntityState state) { /* * 12/18/2019 BJW * * We want to log the history of attribute values within a person matrix. Most of this logging occurs from * the attribute value model. However, when a matrix item (row in the table) is deleted, the pre-save event * of the attribute values is not called. Therefore, the delete event needs to be logged here. Additionally, * when the matrix item is added, the history is much cleaner when added here so that all the values of the * row are consolidated to one history item. Modified state is not possible to log here because the * matrix item is not actually modified when its attributes change. * * Task: https://app.asana.com/0/1120115219297347/1136643182208595/f */ if (state != EntityState.Deleted && state != EntityState.Added) { return; } var rockContext = new RockContext(); var matrixId = AttributeMatrixId != default ? AttributeMatrixId : entry.OriginalValues["AttributeMatrixId"].ToStringSafe().AsIntegerOrNull(); var matrix = AttributeMatrix; if (matrix == null && matrixId.HasValue) { var matrixService = new AttributeMatrixService(rockContext); matrix = matrixService.Queryable().AsNoTracking().FirstOrDefault(am => am.Id == matrixId); } if (matrix == null) { return; } // The root attribute matrix attribute value is linked to the matrix by the guid as the attribute value var matrixGuidString = matrix.Guid.ToString(); var personEntityTypeId = EntityTypeCache.Get(typeof(Person)).Id; var attributeValueService = new AttributeValueService(rockContext); var rootAttributeValue = attributeValueService.Queryable().AsNoTracking().FirstOrDefault(av => av.Value.Equals(matrixGuidString, System.StringComparison.OrdinalIgnoreCase) && av.Attribute.EntityTypeId == personEntityTypeId ); if (rootAttributeValue?.EntityId == null) { return; } var rootAttributeCache = AttributeCache.Get(rootAttributeValue.AttributeId); if (rootAttributeCache == null) { return; } // Evaluate the history changes var historyChangeList = new History.HistoryChangeList(); if (AttributeValues == null || !AttributeValues.Any()) { this.LoadAttributes(); } var isDelete = state == EntityState.Deleted; foreach (var attributeValue in AttributeValues.Values) { var attributeCache = AttributeCache.Get(attributeValue.AttributeId); var formattedOldValue = isDelete ? GetHistoryFormattedValue(attributeValue.Value, attributeCache) : string.Empty; var formattedNewValue = isDelete ? string.Empty : GetHistoryFormattedValue(attributeValue.Value, attributeCache); History.EvaluateChange(historyChangeList, attributeCache.Name, formattedOldValue, formattedNewValue, attributeCache.FieldType.Field.IsSensitive()); } if (!historyChangeList.Any()) { historyChangeList.AddChange( isDelete ? History.HistoryVerb.Delete : History.HistoryVerb.Add, History.HistoryChangeType.Record, $"{rootAttributeCache.Name} Item"); } HistoryItems = HistoryService.GetChanges( typeof(Person), SystemGuid.Category.HISTORY_PERSON_DEMOGRAPHIC_CHANGES.AsGuid(), rootAttributeValue.EntityId.Value, historyChangeList, rootAttributeCache.Name, typeof(Attribute), rootAttributeCache.Id, dbContext.GetCurrentPersonAlias()?.Id, dbContext.SourceOfChange); }
/// <summary> /// This method is called in the /// <see cref="M:Rock.Data.Model`1.PreSaveChanges(Rock.Data.DbContext,System.Data.Entity.Infrastructure.DbEntityEntry,System.Data.Entity.EntityState)" /> /// method. Use it to populate <see cref="P:Rock.Data.Model`1.HistoryItems" /> if needed. /// These history items are queued to be written into the database post save (so that they /// are only written if the save actually occurs). /// </summary> /// <param name="dbContext">The database context.</param> /// <param name="entry">The entry.</param> /// <param name="state">The state.</param> protected override void BuildHistoryItems(Data.DbContext dbContext, DbEntityEntry entry, EntityState state) { // Sometimes, especially if the model is being deleted, some properties might not be // populated, but we can query to try to get their original value. We need to use a new // rock context to get the actual value from the DB var rockContext = new RockContext(); var service = new PersonSearchKeyService(rockContext); var originalModel = service.Queryable("PersonAlias") .FirstOrDefault(fpsa => fpsa.Id == Id); // Use the original value for the person alias or the new value if that is not set var personId = (originalModel?.PersonAlias ?? PersonAlias)?.PersonId; if (!personId.HasValue) { // If this model is new, it won't have any virtual properties hydrated or an original // record in the database if (PersonAliasId.HasValue) { var personAliasService = new PersonAliasService(rockContext); var personAlias = personAliasService.Get(PersonAliasId.Value); personId = personAlias?.PersonId; } // We can't log history if we don't know who the saved account belongs to if (!personId.HasValue) { return; } } History.HistoryVerb verb; switch (state) { case EntityState.Added: verb = History.HistoryVerb.Add; break; case EntityState.Deleted: verb = History.HistoryVerb.Delete; break; case EntityState.Modified: verb = History.HistoryVerb.Modify; break; default: // As of now, there is no requirement to log other events return; } var caption = verb == History.HistoryVerb.Modify ? "Person Search Key" : GetCaptionForHistory(originalModel?.SearchValue ?? SearchValue, originalModel?.SearchTypeValueId ?? SearchTypeValueId); var historyChangeList = new History.HistoryChangeList(); if (verb != History.HistoryVerb.Modify) { historyChangeList.AddChange(verb, History.HistoryChangeType.Record, "Person Search Key"); } else { History.EvaluateChange(historyChangeList, $"SearchValue", entry.OriginalValues["SearchValue"].ToStringSafe(), SearchValue, false); var originalSearchType = DefinedValueCache.Get(entry.OriginalValues["SearchTypeValueId"].ToStringSafe().AsInteger()); var currentSearchType = DefinedValueCache.Get(SearchTypeValueId); History.EvaluateChange(historyChangeList, $"SearchType", originalSearchType?.Value, currentSearchType?.Value, false); } HistoryItems = HistoryService.GetChanges( typeof(Person), Rock.SystemGuid.Category.HISTORY_PERSON.AsGuid(), personId.Value, historyChangeList, caption, typeof(PersonSearchKey), Id, dbContext.GetCurrentPersonAlias()?.Id, dbContext.SourceOfChange); }
/// <summary> /// This method is called in the /// <see cref="M:Rock.Data.Model`1.PreSaveChanges(Rock.Data.DbContext,System.Data.Entity.Infrastructure.DbEntityEntry,System.Data.Entity.EntityState)" /> /// method. Use it to populate <see cref="P:Rock.Data.Model`1.HistoryItems" /> if needed. /// These history items are queued to be written into the database post save (so that they /// are only written if the save actually occurs). /// </summary> /// <param name="dbContext">The database context.</param> /// <param name="entry">The entry.</param> /// <param name="state">The state.</param> protected override void BuildHistoryItems(Data.DbContext dbContext, DbEntityEntry entry, EntityState state) { var attributeCache = AttributeCache.Get(AttributeId); if (attributeCache?.EntityTypeId == null) { return; } var entityTypeId = attributeCache.EntityTypeId.Value; var entityId = EntityId; if (!entityId.HasValue && (entry.State == EntityState.Modified || entry.State == EntityState.Deleted)) { entityId = entry.OriginalValues["EntityId"].ToStringSafe().AsIntegerOrNull(); } var caption = attributeCache.Name; // Check to see if this attribute is for a person or group, and if so, save to history table var personEntityTypeId = EntityTypeCache.Get(typeof(Person)).Id; var entityTypesToSaveToHistoryTable = new List <int> { personEntityTypeId, EntityTypeCache.Get(typeof(Group)).Id }; var saveToHistoryTable = entityTypesToSaveToHistoryTable.Contains(entityTypeId); // If the value is not directly linked to a person or group, it still may be linked through an attribute matrix. // Matrix attribute changes are only logged here for modify. Add and delete are handled in the AttributeMatrixItem. if (!saveToHistoryTable && state == EntityState.Modified && IsLikelyWithinMatrix()) { var rootMatrixAttributeValue = GetRootMatrixAttributeValue(); if (rootMatrixAttributeValue == null) { return; } var rootMatrixAttributeCache = AttributeCache.Get(rootMatrixAttributeValue.AttributeId); if (rootMatrixAttributeCache == null || !rootMatrixAttributeCache.EntityTypeId.HasValue) { return; } saveToHistoryTable = entityTypesToSaveToHistoryTable.Contains(rootMatrixAttributeCache.EntityTypeId.Value); if (saveToHistoryTable) { // Use the values from the root matrix attribute since this is the attribute that ties the // values to a person or group and are thus more meaningful entityTypeId = rootMatrixAttributeCache.EntityTypeId.Value; entityId = rootMatrixAttributeValue.EntityId; caption = rootMatrixAttributeCache.Name; } } if (!saveToHistoryTable || !entityId.HasValue) { return; } // We have determined to write to the History table. Now determine what changed. var oldValue = GetHistoryOldValue(entry); var newValue = GetHistoryNewValue(state); if (oldValue == newValue) { return; } // Evaluate the history change var formattedOldValue = GetHistoryFormattedValue(oldValue, attributeCache); var formattedNewValue = GetHistoryFormattedValue(newValue, attributeCache); var historyChangeList = new History.HistoryChangeList(); History.EvaluateChange(historyChangeList, attributeCache.Name, formattedOldValue, formattedNewValue, attributeCache.FieldType.Field.IsSensitive()); if (!historyChangeList.Any()) { return; } var categoryGuid = entityTypeId == personEntityTypeId? SystemGuid.Category.HISTORY_PERSON_DEMOGRAPHIC_CHANGES.AsGuid() : SystemGuid.Category.HISTORY_GROUP_CHANGES.AsGuid(); HistoryItems = HistoryService.GetChanges( entityTypeId == personEntityTypeId ? typeof(Person) : typeof(Group), categoryGuid, entityId.Value, historyChangeList, caption, typeof(Attribute), AttributeId, dbContext.GetCurrentPersonAlias()?.Id, dbContext.SourceOfChange); }
/// <summary> /// Called after the save operation has been executed /// </summary> /// <remarks> /// This method is only called if <see cref="M:Rock.Data.EntitySaveHook`1.PreSave" /> returns /// without error. /// </remarks> protected override void PostSave() { var rockContext = ( RockContext )this.RockContext; if (HistoryChanges != null) { foreach (var historyItem in HistoryChanges) { int personId = historyItem.PersonId > 0 ? historyItem.PersonId : Entity.PersonId; // if GroupId is 0, it is probably a Group that wasn't saved yet, so get the GroupId from historyItem.Group.Id instead if (historyItem.GroupId == 0) { historyItem.GroupId = historyItem.Group?.Id; } var changes = HistoryService.GetChanges( typeof(Person), Rock.SystemGuid.Category.HISTORY_PERSON_GROUP_MEMBERSHIP.AsGuid(), personId, historyItem.PersonHistoryChangeList, historyItem.Caption, typeof(Group), historyItem.GroupId, Entity.ModifiedByPersonAliasId, rockContext.SourceOfChange); if (changes.Any()) { Task.Run(async() => { // Wait 1 second to allow all post save actions to complete await Task.Delay(1000); try { using (var insertRockContext = new RockContext()) { insertRockContext.BulkInsert(changes); } } catch (SystemException ex) { ExceptionLogService.LogException(ex, null); } }); } var groupMemberChanges = HistoryService.GetChanges( typeof(GroupMember), Rock.SystemGuid.Category.HISTORY_GROUP_CHANGES.AsGuid(), Entity.Id, historyItem.GroupMemberHistoryChangeList, historyItem.Caption, typeof(Group), historyItem.GroupId, Entity.ModifiedByPersonAliasId, rockContext.SourceOfChange); if (groupMemberChanges.Any()) { Task.Run(async() => { // Wait 1 second to allow all post save actions to complete await Task.Delay(1000); try { using (var insertRockContext = new RockContext()) { insertRockContext.BulkInsert(groupMemberChanges); } } catch (SystemException ex) { ExceptionLogService.LogException(ex, null); } }); } } } base.PostSave(); // if this is a GroupMember record on a Family, ensure that AgeClassification, PrimaryFamily, // GivingLeadId, and GroupSalution is updated // NOTE: This is also done on Person.PostSaveChanges in case Birthdate changes var groupTypeFamilyRoleIds = GroupTypeCache.GetFamilyGroupType()?.Roles?.Select(a => a.Id).ToList(); if (groupTypeFamilyRoleIds?.Any() == true) { if (groupTypeFamilyRoleIds.Contains(Entity.GroupRoleId)) { PersonService.UpdatePersonAgeClassification(Entity.PersonId, rockContext); PersonService.UpdatePrimaryFamily(Entity.PersonId, rockContext); PersonService.UpdateGivingLeaderId(Entity.PersonId, rockContext); GroupService.UpdateGroupSalutations(Entity.GroupId, rockContext); if (_preSaveChangesOldGroupId.HasValue && _preSaveChangesOldGroupId.Value != Entity.GroupId) { // if person was moved to a different family, the old family will need its GroupSalutations updated GroupService.UpdateGroupSalutations(_preSaveChangesOldGroupId.Value, rockContext); } } } if (State == EntityContextState.Added || State == EntityContextState.Modified) { if (Entity.Group != null && Entity.Person != null) { if (Entity.Group?.IsSecurityRoleOrSecurityGroupType() == true) { /* 09/27/2021 MDP * * If this GroupMember record results in making this Person having a higher AccountProtectionProfile level, * update the Person's AccountProtectionProfile. * Note: If this GroupMember record could result in making this Person having a *lower* AccountProtectionProfile level, * don't lower the AccountProtectionProfile here, because other rules have to be considered before * lowering the AccountProtectionProfile level. So we'll let the RockCleanup job take care of making sure the * AccountProtectionProfile is updated after factoring in all the rules. * */ if (Entity.Group.ElevatedSecurityLevel >= Utility.Enums.ElevatedSecurityLevel.Extreme && Entity.Person.AccountProtectionProfile < Utility.Enums.AccountProtectionProfile.Extreme) { Entity.Person.AccountProtectionProfile = Utility.Enums.AccountProtectionProfile.Extreme; rockContext.SaveChanges(); } else if (Entity.Group.ElevatedSecurityLevel >= Utility.Enums.ElevatedSecurityLevel.High && Entity.Person.AccountProtectionProfile < Utility.Enums.AccountProtectionProfile.High) { Entity.Person.AccountProtectionProfile = Utility.Enums.AccountProtectionProfile.High; rockContext.SaveChanges(); } } } } }