diff --git a/build/build-bootstrap.ps1 b/build/build-bootstrap.ps1 index 82c789ff22..9a31a57acf 100644 --- a/build/build-bootstrap.ps1 +++ b/build/build-bootstrap.ps1 @@ -34,6 +34,7 @@ if (-not (test-path $nuget)) { Write-Host "Download NuGet..." + [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 Invoke-WebRequest $source -OutFile $nuget if (-not $?) { throw "Failed to download NuGet." } } diff --git a/src/Umbraco.Core/Cache/MemberCacheRefresher.cs b/src/Umbraco.Core/Cache/MemberCacheRefresher.cs index 68c89c0bc7..a9a648acff 100644 --- a/src/Umbraco.Core/Cache/MemberCacheRefresher.cs +++ b/src/Umbraco.Core/Cache/MemberCacheRefresher.cs @@ -1,21 +1,37 @@ -using System; +//using Newtonsoft.Json; +using System; using Umbraco.Core; using Umbraco.Core.Cache; using Umbraco.Core.Models; -using Umbraco.Core.Persistence.Repositories; using Umbraco.Core.Persistence.Repositories.Implement; +using Umbraco.Core.Serialization; using Umbraco.Core.Services; namespace Umbraco.Web.Cache { - public sealed class MemberCacheRefresher : TypedCacheRefresherBase + public sealed class MemberCacheRefresher : PayloadCacheRefresherBase { private readonly IIdKeyMap _idKeyMap; + private readonly LegacyMemberCacheRefresher _legacyMemberRefresher; - public MemberCacheRefresher(AppCaches appCaches, IIdKeyMap idKeyMap) - : base(appCaches) + public MemberCacheRefresher(AppCaches appCaches, IJsonSerializer serializer, IIdKeyMap idKeyMap) + : base(appCaches, serializer) { _idKeyMap = idKeyMap; + _legacyMemberRefresher = new LegacyMemberCacheRefresher(this, appCaches); + } + + public class JsonPayload + { + //[JsonConstructor] + public JsonPayload(int id, string username) + { + Id = id; + Username = username; + } + + public int Id { get; } + public string Username { get; } } #region Define @@ -32,38 +48,45 @@ namespace Umbraco.Web.Cache #region Refresher + public override void Refresh(JsonPayload[] payloads) + { + ClearCache(payloads); + base.Refresh(payloads); + } + public override void Refresh(int id) { - ClearCache(id); + ClearCache(new JsonPayload(id, null)); base.Refresh(id); } public override void Remove(int id) { - ClearCache(id); + ClearCache(new JsonPayload(id, null)); base.Remove(id); } - public override void Refresh(IMember instance) - { - ClearCache(instance.Id); - base.Refresh(instance); - } + [Obsolete("This is no longer used and will be removed from the codebase in the future")] + public void Refresh(IMember instance) => _legacyMemberRefresher.Refresh(instance); - public override void Remove(IMember instance) - { - ClearCache(instance.Id); - base.Remove(instance); - } + [Obsolete("This is no longer used and will be removed from the codebase in the future")] + public void Remove(IMember instance) => _legacyMemberRefresher.Remove(instance); - private void ClearCache(int id) + private void ClearCache(params JsonPayload[] payloads) { - _idKeyMap.ClearCache(id); AppCaches.ClearPartialViewCache(); - var memberCache = AppCaches.IsolatedCaches.Get(); - if (memberCache) - memberCache.Result.Clear(RepositoryCacheKeys.GetKey(id)); + + foreach (var p in payloads) + { + _idKeyMap.ClearCache(p.Id); + if (memberCache) + { + memberCache.Result.Clear(RepositoryCacheKeys.GetKey(p.Id)); + memberCache.Result.Clear(RepositoryCacheKeys.GetKey(p.Username)); + } + } + } #endregion @@ -76,5 +99,38 @@ namespace Umbraco.Web.Cache } #endregion + + #region Backwards Compat + + // TODO: this is here purely for backwards compat but should be removed in netcore + private class LegacyMemberCacheRefresher : TypedCacheRefresherBase + { + private readonly MemberCacheRefresher _parent; + + public LegacyMemberCacheRefresher(MemberCacheRefresher parent, AppCaches appCaches) : base(appCaches) + { + _parent = parent; + } + + public override Guid RefresherUniqueId => _parent.RefresherUniqueId; + + public override string Name => _parent.Name; + + protected override MemberCacheRefresher This => _parent; + + public override void Refresh(IMember instance) + { + _parent.ClearCache(new JsonPayload(instance.Id, instance.Username)); + base.Refresh(instance.Id); + } + + public override void Remove(IMember instance) + { + _parent.ClearCache(new JsonPayload(instance.Id, instance.Username)); + base.Remove(instance); + } + } + + #endregion } } diff --git a/src/Umbraco.Core/Services/IMembershipMemberService.cs b/src/Umbraco.Core/Services/IMembershipMemberService.cs index d3ca954de8..839d8e3af3 100644 --- a/src/Umbraco.Core/Services/IMembershipMemberService.cs +++ b/src/Umbraco.Core/Services/IMembershipMemberService.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using Umbraco.Core.Models; using Umbraco.Core.Models.Membership; using Umbraco.Core.Persistence.Querying; @@ -107,6 +108,18 @@ namespace Umbraco.Core.Services /// or to Delete void Delete(T membershipUser); + /// + /// Sets the last login date for the member if they are found by username + /// + /// + /// + /// + /// This is a specialized method because whenever a member logs in, the membership provider requires us to set the 'online' which requires + /// updating their login date. This operation must be fast and cannot use database locks which is fine if we are only executing a single query + /// for this data since there won't be any other data contention issues. + /// + void SetLastLogin(string username, DateTime date); + /// /// Saves an /// diff --git a/src/Umbraco.Examine.Lucene/UmbracoContentIndex.cs b/src/Umbraco.Examine.Lucene/UmbracoContentIndex.cs index 70b44340bd..9f846bc07c 100644 --- a/src/Umbraco.Examine.Lucene/UmbracoContentIndex.cs +++ b/src/Umbraco.Examine.Lucene/UmbracoContentIndex.cs @@ -68,7 +68,7 @@ namespace Umbraco.Examine /// protected override void PerformIndexItems(IEnumerable values, Action onComplete) { - //We don't want to re-enumerate this list, but we need to split it into 2x enumerables: invalid and valid items. + // We don't want to re-enumerate this list, but we need to split it into 2x enumerables: invalid and valid items. // The Invalid items will be deleted, these are items that have invalid paths (i.e. moved to the recycle bin, etc...) // Then we'll index the Value group all together. // We return 0 or 1 here so we can order the results and do the invalid first and then the valid. @@ -86,7 +86,7 @@ namespace Umbraco.Examine || !validator.ValidateProtectedContent(path, v.Category)) ? 0 : 1; - }); + }).ToList(); var hasDeletes = false; var hasUpdates = false; @@ -105,7 +105,7 @@ namespace Umbraco.Examine { hasUpdates = true; //these are the valid ones, so just index them all at once - base.PerformIndexItems(group, onComplete); + base.PerformIndexItems(group.ToList(), onComplete); } } diff --git a/src/Umbraco.Infrastructure/Cache/DistributedCacheExtensions.cs b/src/Umbraco.Infrastructure/Cache/DistributedCacheExtensions.cs index 7cfac1b9a0..ef968e8fa6 100644 --- a/src/Umbraco.Infrastructure/Cache/DistributedCacheExtensions.cs +++ b/src/Umbraco.Infrastructure/Cache/DistributedCacheExtensions.cs @@ -128,15 +128,16 @@ namespace Umbraco.Web.Cache public static void RefreshMemberCache(this DistributedCache dc, params IMember[] members) { - dc.Refresh(MemberCacheRefresher.UniqueId, x => x.Id, members); + if (members.Length == 0) return; + dc.RefreshByPayload(MemberCacheRefresher.UniqueId, members.Select(x => new MemberCacheRefresher.JsonPayload(x.Id, x.Username))); } public static void RemoveMemberCache(this DistributedCache dc, params IMember[] members) { - dc.Remove(MemberCacheRefresher.UniqueId, x => x.Id, members); + if (members.Length == 0) return; + dc.RefreshByPayload(MemberCacheRefresher.UniqueId, members.Select(x => new MemberCacheRefresher.JsonPayload(x.Id, x.Username))); } - #endregion #region MemberGroupCache diff --git a/src/Umbraco.Infrastructure/Compose/NotificationsComponent.cs b/src/Umbraco.Infrastructure/Compose/NotificationsComponent.cs index b39e5c03e9..8ef4d30191 100644 --- a/src/Umbraco.Infrastructure/Compose/NotificationsComponent.cs +++ b/src/Umbraco.Infrastructure/Compose/NotificationsComponent.cs @@ -5,11 +5,9 @@ using System.Linq; using Umbraco.Core; using Umbraco.Core.Composing; using Umbraco.Core.Configuration; -using Umbraco.Core.Configuration.UmbracoSettings; using Umbraco.Core.Logging; using Umbraco.Core.Models; using Umbraco.Core.Models.Entities; -using Umbraco.Core.Models.Identity; using Umbraco.Core.Models.Membership; using Umbraco.Core.Services; using Umbraco.Core.Services.Implement; @@ -33,42 +31,78 @@ namespace Umbraco.Web.Compose public void Initialize() { //Send notifications for the send to publish action - ContentService.SentToPublish += (sender, args) => _notifier.Notify(_actions.GetAction(), args.Entity); - + ContentService.SentToPublish += ContentService_SentToPublish; //Send notifications for the published action - ContentService.Published += (sender, args) => _notifier.Notify(_actions.GetAction(), args.PublishedEntities.ToArray()); - + ContentService.Published += ContentService_Published; //Send notifications for the saved action - ContentService.Sorted += (sender, args) => ContentServiceSorted(_notifier, sender, args, _actions); - + ContentService.Sorted += ContentService_Sorted; //Send notifications for the update and created actions - ContentService.Saved += (sender, args) => ContentServiceSaved(_notifier, sender, args, _actions); - + ContentService.Saved += ContentService_Saved; //Send notifications for the unpublish action - ContentService.Unpublished += (sender, args) => _notifier.Notify(_actions.GetAction(), args.PublishedEntities.ToArray()); - + ContentService.Unpublished += ContentService_Unpublished; //Send notifications for the move/move to recycle bin and restore actions - ContentService.Moved += (sender, args) => ContentServiceMoved(_notifier, sender, args, _actions); - + ContentService.Moved += ContentService_Moved; //Send notifications for the delete action when content is moved to the recycle bin - ContentService.Trashed += (sender, args) => _notifier.Notify(_actions.GetAction(), args.MoveInfoCollection.Select(m => m.Entity).ToArray()); - + ContentService.Trashed += ContentService_Trashed; //Send notifications for the copy action - ContentService.Copied += (sender, args) => _notifier.Notify(_actions.GetAction(), args.Original); - + ContentService.Copied += ContentService_Copied; //Send notifications for the rollback action - ContentService.RolledBack += (sender, args) => _notifier.Notify(_actions.GetAction(), args.Entity); - + ContentService.RolledBack += ContentService_RolledBack; //Send notifications for the public access changed action - PublicAccessService.Saved += (sender, args) => PublicAccessServiceSaved(_notifier, sender, args, _contentService, _actions); + PublicAccessService.Saved += PublicAccessService_Saved; - UserService.UserGroupPermissionsAssigned += (sender, args) => UserServiceUserGroupPermissionsAssigned(_notifier, sender, args, _contentService, _actions); + UserService.UserGroupPermissionsAssigned += UserService_UserGroupPermissionsAssigned; } public void Terminate() - { } + { + ContentService.SentToPublish -= ContentService_SentToPublish; + ContentService.Published -= ContentService_Published; + ContentService.Sorted -= ContentService_Sorted; + ContentService.Saved -= ContentService_Saved; + ContentService.Unpublished -= ContentService_Unpublished; + ContentService.Moved -= ContentService_Moved; + ContentService.Trashed -= ContentService_Trashed; + ContentService.Copied -= ContentService_Copied; + ContentService.RolledBack -= ContentService_RolledBack; + PublicAccessService.Saved -= PublicAccessService_Saved; + UserService.UserGroupPermissionsAssigned -= UserService_UserGroupPermissionsAssigned; + } - private void ContentServiceSorted(Notifier notifier, IContentService sender, Core.Events.SaveEventArgs args, ActionCollection actions) + private void UserService_UserGroupPermissionsAssigned(IUserService sender, Core.Events.SaveEventArgs args) + => UserServiceUserGroupPermissionsAssigned(args, _contentService); + + private void PublicAccessService_Saved(IPublicAccessService sender, Core.Events.SaveEventArgs args) + => PublicAccessServiceSaved(args, _contentService); + + private void ContentService_RolledBack(IContentService sender, Core.Events.RollbackEventArgs args) + => _notifier.Notify(_actions.GetAction(), args.Entity); + + private void ContentService_Copied(IContentService sender, Core.Events.CopyEventArgs args) + => _notifier.Notify(_actions.GetAction(), args.Original); + + private void ContentService_Trashed(IContentService sender, Core.Events.MoveEventArgs args) + => _notifier.Notify(_actions.GetAction(), args.MoveInfoCollection.Select(m => m.Entity).ToArray()); + + private void ContentService_Moved(IContentService sender, Core.Events.MoveEventArgs args) + => ContentServiceMoved(args); + + private void ContentService_Unpublished(IContentService sender, Core.Events.PublishEventArgs args) + => _notifier.Notify(_actions.GetAction(), args.PublishedEntities.ToArray()); + + private void ContentService_Saved(IContentService sender, Core.Events.ContentSavedEventArgs args) + => ContentServiceSaved(args); + + private void ContentService_Sorted(IContentService sender, Core.Events.SaveEventArgs args) + => ContentServiceSorted(sender, args); + + private void ContentService_Published(IContentService sender, Core.Events.ContentPublishedEventArgs args) + => _notifier.Notify(_actions.GetAction(), args.PublishedEntities.ToArray()); + + private void ContentService_SentToPublish(IContentService sender, Core.Events.SendToPublishEventArgs args) + => _notifier.Notify(_actions.GetAction(), args.Entity); + + private void ContentServiceSorted(IContentService sender, Core.Events.SaveEventArgs args) { var parentId = args.SavedEntities.Select(x => x.ParentId).Distinct().ToList(); if (parentId.Count != 1) return; // this shouldn't happen, for sorting all entities will have the same parent id @@ -80,10 +114,10 @@ namespace Umbraco.Web.Compose var parent = sender.GetById(parentId[0]); if (parent == null) return; // this shouldn't happen - notifier.Notify(actions.GetAction(), new[] { parent }); + _notifier.Notify(_actions.GetAction(), new[] { parent }); } - private void ContentServiceSaved(Notifier notifier, IContentService sender, Core.Events.SaveEventArgs args, ActionCollection actions) + private void ContentServiceSaved(Core.Events.SaveEventArgs args) { var newEntities = new List(); var updatedEntities = new List(); @@ -103,21 +137,21 @@ namespace Umbraco.Web.Compose updatedEntities.Add(entity); } } - notifier.Notify(actions.GetAction(), newEntities.ToArray()); - notifier.Notify(actions.GetAction(), updatedEntities.ToArray()); + _notifier.Notify(_actions.GetAction(), newEntities.ToArray()); + _notifier.Notify(_actions.GetAction(), updatedEntities.ToArray()); } - private void UserServiceUserGroupPermissionsAssigned(Notifier notifier, IUserService sender, Core.Events.SaveEventArgs args, IContentService contentService, ActionCollection actions) + private void UserServiceUserGroupPermissionsAssigned(Core.Events.SaveEventArgs args, IContentService contentService) { var entities = contentService.GetByIds(args.SavedEntities.Select(e => e.EntityId)).ToArray(); - if(entities.Any() == false) + if (entities.Any() == false) { return; } - notifier.Notify(actions.GetAction(), entities); + _notifier.Notify(_actions.GetAction(), entities); } - private void ContentServiceMoved(Notifier notifier, IContentService sender, Core.Events.MoveEventArgs args, ActionCollection actions) + private void ContentServiceMoved(Core.Events.MoveEventArgs args) { // notify about the move for all moved items _notifier.Notify(_actions.GetAction(), args.MoveInfoCollection.Select(m => m.Entity).ToArray()); @@ -127,20 +161,20 @@ namespace Umbraco.Web.Compose .Where(m => m.OriginalPath.Contains(Constants.System.RecycleBinContentString)) .Select(m => m.Entity) .ToArray(); - if(restoredEntities.Any()) + if (restoredEntities.Any()) { _notifier.Notify(_actions.GetAction(), restoredEntities); } } - private void PublicAccessServiceSaved(Notifier notifier, IPublicAccessService sender, Core.Events.SaveEventArgs args, IContentService contentService, ActionCollection actions) + private void PublicAccessServiceSaved(Core.Events.SaveEventArgs args, IContentService contentService) { var entities = contentService.GetByIds(args.SavedEntities.Select(e => e.ProtectedNodeId)).ToArray(); - if(entities.Any() == false) + if (entities.Any() == false) { return; } - notifier.Notify(actions.GetAction(), entities); + _notifier.Notify(_actions.GetAction(), entities); } /// diff --git a/src/Umbraco.Infrastructure/Compose/PublicAccessComponent.cs b/src/Umbraco.Infrastructure/Compose/PublicAccessComponent.cs index 37bcfb1ceb..6a77739c64 100644 --- a/src/Umbraco.Infrastructure/Compose/PublicAccessComponent.cs +++ b/src/Umbraco.Infrastructure/Compose/PublicAccessComponent.cs @@ -16,13 +16,15 @@ namespace Umbraco.Web.Compose public void Initialize() { - MemberGroupService.Saved += (s, e) => MemberGroupService_Saved(s, e, _publicAccessService); + MemberGroupService.Saved += MemberGroupService_Saved; } public void Terminate() - { } + { + MemberGroupService.Saved -= MemberGroupService_Saved; + } - static void MemberGroupService_Saved(IMemberGroupService sender, Core.Events.SaveEventArgs e, IPublicAccessService publicAccessService) + void MemberGroupService_Saved(IMemberGroupService sender, Core.Events.SaveEventArgs e) { foreach (var grp in e.SavedEntities) { @@ -32,7 +34,7 @@ namespace Umbraco.Web.Compose && grp.AdditionalData["previousName"].ToString().IsNullOrWhiteSpace() == false && grp.AdditionalData["previousName"].ToString() != grp.Name) { - publicAccessService.RenameMemberGroupRoleRules(grp.AdditionalData["previousName"].ToString(), grp.Name); + _publicAccessService.RenameMemberGroupRoleRules(grp.AdditionalData["previousName"].ToString(), grp.Name); } } } diff --git a/src/Umbraco.Infrastructure/Compose/RelateOnCopyComponent.cs b/src/Umbraco.Infrastructure/Compose/RelateOnCopyComponent.cs index 56a97e4cba..3418dfcfc0 100644 --- a/src/Umbraco.Infrastructure/Compose/RelateOnCopyComponent.cs +++ b/src/Umbraco.Infrastructure/Compose/RelateOnCopyComponent.cs @@ -23,7 +23,9 @@ namespace Umbraco.Core.Compose } public void Terminate() - { } + { + ContentService.Copied -= ContentServiceCopied; + } private void ContentServiceCopied(IContentService sender, Events.CopyEventArgs e) { diff --git a/src/Umbraco.Infrastructure/Compose/RelateOnTrashComponent.cs b/src/Umbraco.Infrastructure/Compose/RelateOnTrashComponent.cs index c81aa2fd7d..aa92972e9c 100644 --- a/src/Umbraco.Infrastructure/Compose/RelateOnTrashComponent.cs +++ b/src/Umbraco.Infrastructure/Compose/RelateOnTrashComponent.cs @@ -24,47 +24,52 @@ namespace Umbraco.Core.Compose public void Initialize() { - ContentService.Moved += (sender, args) => ContentService_Moved(sender, args, _relationService); - ContentService.Trashed += (sender, args) => ContentService_Trashed(sender, args, _relationService, _entityService, _textService, _auditService); - MediaService.Moved += (sender, args) => MediaService_Moved(sender, args, _relationService); - MediaService.Trashed += (sender, args) => MediaService_Trashed(sender, args, _relationService, _entityService, _textService, _auditService); + ContentService.Moved += ContentService_Moved; + ContentService.Trashed += ContentService_Trashed; + MediaService.Moved += MediaService_Moved; + MediaService.Trashed += MediaService_Trashed; } public void Terminate() - { } + { + ContentService.Moved -= ContentService_Moved; + ContentService.Trashed -= ContentService_Trashed; + MediaService.Moved -= MediaService_Moved; + MediaService.Trashed -= MediaService_Trashed; + } - private static void ContentService_Moved(IContentService sender, MoveEventArgs e, IRelationService relationService) + private void ContentService_Moved(IContentService sender, MoveEventArgs e) { foreach (var item in e.MoveInfoCollection.Where(x => x.OriginalPath.Contains(Constants.System.RecycleBinContentString))) { const string relationTypeAlias = Constants.Conventions.RelationTypes.RelateParentDocumentOnDeleteAlias; - var relations = relationService.GetByChildId(item.Entity.Id); + var relations = _relationService.GetByChildId(item.Entity.Id); foreach (var relation in relations.Where(x => x.RelationType.Alias.InvariantEquals(relationTypeAlias))) { - relationService.Delete(relation); + _relationService.Delete(relation); } } } - private static void MediaService_Moved(IMediaService sender, MoveEventArgs e, IRelationService relationService) + private void MediaService_Moved(IMediaService sender, MoveEventArgs e) { foreach (var item in e.MoveInfoCollection.Where(x => x.OriginalPath.Contains(Constants.System.RecycleBinMediaString))) { const string relationTypeAlias = Constants.Conventions.RelationTypes.RelateParentMediaFolderOnDeleteAlias; - var relations = relationService.GetByChildId(item.Entity.Id); + var relations = _relationService.GetByChildId(item.Entity.Id); foreach (var relation in relations.Where(x => x.RelationType.Alias.InvariantEquals(relationTypeAlias))) { - relationService.Delete(relation); + _relationService.Delete(relation); } } } - private static void ContentService_Trashed(IContentService sender, MoveEventArgs e, IRelationService relationService, IEntityService entityService, ILocalizedTextService textService, IAuditService auditService) + private void ContentService_Trashed(IContentService sender, MoveEventArgs e) { const string relationTypeAlias = Constants.Conventions.RelationTypes.RelateParentDocumentOnDeleteAlias; - var relationType = relationService.GetRelationTypeByAlias(relationTypeAlias); + var relationType = _relationService.GetRelationTypeByAlias(relationTypeAlias); // check that the relation-type exists, if not, then recreate it if (relationType == null) @@ -73,7 +78,7 @@ namespace Umbraco.Core.Compose const string relationTypeName = Constants.Conventions.RelationTypes.RelateParentDocumentOnDeleteName; relationType = new RelationType(relationTypeName, relationTypeAlias, false, documentObjectType, documentObjectType); - relationService.Save(relationType); + _relationService.Save(relationType); } foreach (var item in e.MoveInfoCollection) @@ -86,34 +91,34 @@ namespace Umbraco.Core.Compose //before we can create this relation, we need to ensure that the original parent still exists which //may not be the case if the encompassing transaction also deleted it when this item was moved to the bin - if (entityService.Exists(originalParentId)) + if (_entityService.Exists(originalParentId)) { // Add a relation for the item being deleted, so that we can know the original parent for if we need to restore later var relation = new Relation(originalParentId, item.Entity.Id, relationType); - relationService.Save(relation); + _relationService.Save(relation); - auditService.Add(AuditType.Delete, + _auditService.Add(AuditType.Delete, item.Entity.WriterId, item.Entity.Id, ObjectTypes.GetName(UmbracoObjectTypes.Document), - string.Format(textService.Localize( + string.Format(_textService.Localize( "recycleBin/contentTrashed"), item.Entity.Id, originalParentId)); } } } - private static void MediaService_Trashed(IMediaService sender, MoveEventArgs e, IRelationService relationService, IEntityService entityService, ILocalizedTextService textService, IAuditService auditService) + private void MediaService_Trashed(IMediaService sender, MoveEventArgs e) { const string relationTypeAlias = Constants.Conventions.RelationTypes.RelateParentMediaFolderOnDeleteAlias; - var relationType = relationService.GetRelationTypeByAlias(relationTypeAlias); + var relationType = _relationService.GetRelationTypeByAlias(relationTypeAlias); // check that the relation-type exists, if not, then recreate it if (relationType == null) { var documentObjectType = Constants.ObjectTypes.Document; const string relationTypeName = Constants.Conventions.RelationTypes.RelateParentMediaFolderOnDeleteName; relationType = new RelationType(relationTypeName, relationTypeAlias, false, documentObjectType, documentObjectType); - relationService.Save(relationType); + _relationService.Save(relationType); } foreach (var item in e.MoveInfoCollection) { @@ -123,16 +128,16 @@ namespace Umbraco.Core.Compose : Constants.System.Root; //before we can create this relation, we need to ensure that the original parent still exists which //may not be the case if the encompassing transaction also deleted it when this item was moved to the bin - if (entityService.Exists(originalParentId)) + if (_entityService.Exists(originalParentId)) { // Add a relation for the item being deleted, so that we can know the original parent for if we need to restore later var relation = new Relation(originalParentId, item.Entity.Id, relationType); - relationService.Save(relation); - auditService.Add(AuditType.Delete, + _relationService.Save(relation); + _auditService.Add(AuditType.Delete, item.Entity.CreatorId, item.Entity.Id, ObjectTypes.GetName(UmbracoObjectTypes.Media), - string.Format(textService.Localize( + string.Format(_textService.Localize( "recycleBin/mediaTrashed"), item.Entity.Id, originalParentId)); } diff --git a/src/Umbraco.Infrastructure/Examine/ContentValueSetBuilder.cs b/src/Umbraco.Infrastructure/Examine/ContentValueSetBuilder.cs index 01e6d993b4..f03fcca181 100644 --- a/src/Umbraco.Infrastructure/Examine/ContentValueSetBuilder.cs +++ b/src/Umbraco.Infrastructure/Examine/ContentValueSetBuilder.cs @@ -53,6 +53,11 @@ namespace Umbraco.Examine scope.Complete(); } + return GetValueSetsEnumerable(content, creatorIds, writerIds); + } + + private IEnumerable GetValueSetsEnumerable(IContent[] content, Dictionary creatorIds, Dictionary writerIds) + { // TODO: There is a lot of boxing going on here and ultimately all values will be boxed by Lucene anyways // but I wonder if there's a way to reduce the boxing that we have to do or if it will matter in the end since // Lucene will do it no matter what? One idea was to create a `FieldValue` struct which would contain `object`, `object[]`, `ValueType` and `ValueType[]` diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/IMemberRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/IMemberRepository.cs index 245c024356..c737c2bf66 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/IMemberRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/IMemberRepository.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using Umbraco.Core.Models; using Umbraco.Core.Persistence.Querying; @@ -6,6 +7,8 @@ namespace Umbraco.Core.Persistence.Repositories { public interface IMemberRepository : IContentRepository { + IMember GetByUsername(string username); + /// /// Finds members in a given role /// @@ -35,5 +38,17 @@ namespace Umbraco.Core.Persistence.Repositories /// /// int GetCountByQuery(IQuery query); + + /// + /// Sets a members last login date based on their username + /// + /// + /// + /// + /// This is a specialized method because whenever a member logs in, the membership provider requires us to set the 'online' which requires + /// updating their login date. This operation must be fast and cannot use database locks which is fine if we are only executing a single query + /// for this data since there won't be any other data contention issues. + /// + void SetLastLogin(string username, DateTime date); } } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MemberRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MemberRepository.cs index 64266f9df8..8c3c2b3b11 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MemberRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MemberRepository.cs @@ -26,6 +26,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement private readonly ITagRepository _tagRepository; private readonly IPasswordHasher _passwordHasher; private readonly IMemberGroupRepository _memberGroupRepository; + private readonly IRepositoryCachePolicy _memberByUsernameCachePolicy; public MemberRepository(IScopeAccessor scopeAccessor, AppCaches cache, ILogger logger, IMemberTypeRepository memberTypeRepository, IMemberGroupRepository memberGroupRepository, ITagRepository tagRepository, ILanguageRepository languageRepository, IRelationRepository relationRepository, IRelationTypeRepository relationTypeRepository, @@ -39,6 +40,8 @@ namespace Umbraco.Core.Persistence.Repositories.Implement _tagRepository = tagRepository ?? throw new ArgumentNullException(nameof(tagRepository)); _passwordHasher = passwordHasher; _memberGroupRepository = memberGroupRepository; + + _memberByUsernameCachePolicy = new DefaultRepositoryCachePolicy(GlobalIsolatedCache, ScopeAccessor, DefaultOptions); } protected override MemberRepository This => this; @@ -383,12 +386,27 @@ namespace Umbraco.Core.Persistence.Repositories.Implement if (changedCols.Count > 0) Database.Update(dto, changedCols); - // replace the property data - var deletePropertyDataSql = SqlContext.Sql().Delete().Where(x => x.VersionId == member.VersionId); - Database.Execute(deletePropertyDataSql); + // Replace the property data + // Lookup the data to update with a UPDLOCK (using ForUpdate()) this is because we have another method that doesn't take an explicit WriteLock + // in SetLastLogin which is called very often and we want to avoid the lock timeout for the explicit lock table but we still need to ensure atomic + // operations between that method and this one. + + var propDataSql = SqlContext.Sql().Select("*").From().Where(x => x.VersionId == member.VersionId).ForUpdate(); + var existingPropData = Database.Fetch(propDataSql).ToDictionary(x => x.PropertyTypeId); var propertyDataDtos = PropertyFactory.BuildDtos(member.ContentType.Variations, member.VersionId, 0, entity.Properties, LanguageRepository, out _, out _); foreach (var propertyDataDto in propertyDataDtos) - Database.Insert(propertyDataDto); + { + // Check if this already exists and update, else insert a new one + if (existingPropData.TryGetValue(propertyDataDto.PropertyTypeId, out var propData)) + { + propertyDataDto.Id = propData.Id; + Database.Update(propertyDataDto); + } + else + { + Database.Insert(propertyDataDto); + } + } SetEntityTags(entity, _tagRepository); @@ -506,6 +524,56 @@ namespace Umbraco.Core.Persistence.Repositories.Implement return Database.ExecuteScalar(fullSql); } + /// + public void SetLastLogin(string username, DateTime date) + { + // Important - these queries are designed to execute without an exclusive WriteLock taken in our distributed lock + // table. However due to the data that we are updating which relies on version data we cannot update this data + // without taking some locks, otherwise we'll end up with strange situations because when a member is updated, that operation + // deletes and re-inserts all property data. So if there are concurrent transactions, one deleting and re-inserting and another trying + // to update there can be problems. This is only an issue for cmsPropertyData, not umbracoContentVersion because that table just + // maintains a single row and it isn't deleted/re-inserted. + // So the important part here is the ForUpdate() call on the select to fetch the property data to update. + + // Update the cms property value for the member + + var sqlSelectTemplateProperty = SqlContext.Templates.Get("Umbraco.Core.MemberRepository.SetLastLogin1", s => s + .Select(x => x.Id) + .From() + .InnerJoin().On((l, r) => l.Id == r.PropertyTypeId) + .InnerJoin().On((l, r) => l.Id == r.VersionId) + .InnerJoin().On((l, r) => l.NodeId == r.NodeId) + .InnerJoin().On((l, r) => l.NodeId == r.NodeId) + .Where(x => x.NodeObjectType == SqlTemplate.Arg("nodeObjectType")) + .Where(x => x.Alias == SqlTemplate.Arg("propertyTypeAlias")) + .Where(x => x.LoginName == SqlTemplate.Arg("username")) + .ForUpdate()); + var sqlSelectProperty = sqlSelectTemplateProperty.Sql(Constants.ObjectTypes.Member, Constants.Conventions.Member.LastLoginDate, username); + + var update = Sql() + .Update(u => u + .Set(x => x.DateValue, date)) + .WhereIn(x => x.Id, sqlSelectProperty); + + Database.Execute(update); + + // Update the umbracoContentVersion value for the member + + var sqlSelectTemplateVersion = SqlContext.Templates.Get("Umbraco.Core.MemberRepository.SetLastLogin2", s => s + .Select(x => x.Id) + .From() + .InnerJoin().On((l, r) => l.NodeId == r.NodeId) + .InnerJoin().On((l, r) => l.NodeId == r.NodeId) + .Where(x => x.NodeObjectType == SqlTemplate.Arg("nodeObjectType")) + .Where(x => x.LoginName == SqlTemplate.Arg("username"))); + var sqlSelectVersion = sqlSelectTemplateVersion.Sql(Constants.ObjectTypes.Member, username); + + Database.Execute(Sql() + .Update(u => u + .Set(x => x.VersionDate, date)) + .WhereIn(x => x.Id, sqlSelectVersion)); + } + /// /// Gets paged member results. /// @@ -529,20 +597,6 @@ namespace Umbraco.Core.Persistence.Repositories.Implement ordering); } - private string _pagedResultsByQueryWhere; - - private string GetPagedResultsByQueryWhere() - { - if (_pagedResultsByQueryWhere == null) - _pagedResultsByQueryWhere = " AND (" - + $"({SqlSyntax.GetQuotedTableName("umbracoNode")}.{SqlSyntax.GetQuotedColumnName("text")} LIKE @0)" - + " OR " - + $"({SqlSyntax.GetQuotedTableName("cmsMember")}.{SqlSyntax.GetQuotedColumnName("LoginName")} LIKE @0)" - + ")"; - - return _pagedResultsByQueryWhere; - } - protected override string ApplySystemOrdering(ref Sql sql, Ordering ordering) { if (ordering.OrderBy.InvariantEquals("email")) @@ -632,5 +686,22 @@ namespace Umbraco.Core.Persistence.Repositories.Implement member.ResetDirtyProperties(false); return member; } + + public IMember GetByUsername(string username) + { + return _memberByUsernameCachePolicy.Get(username, PerformGetByUsername, PerformGetAllByUsername); + } + + private IMember PerformGetByUsername(string username) + { + var query = Query().Where(x => x.Username.Equals(username)); + return PerformGetByQuery(query).FirstOrDefault(); + } + + private IEnumerable PerformGetAllByUsername(params string[] usernames) + { + var query = Query().WhereIn(x => x.Username, usernames); + return PerformGetByQuery(query); + } } } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/SimpleGetRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/SimpleGetRepository.cs index f7e59820c3..d327fba67f 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/SimpleGetRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/SimpleGetRepository.cs @@ -11,6 +11,8 @@ using Umbraco.Core.Scoping; namespace Umbraco.Core.Persistence.Repositories.Implement { + // TODO: Obsolete this, change all implementations of this like in Dictionary to just use custom Cache policies like in the member repository. + /// /// Simple abstract ReadOnly repository used to simply have PerformGet and PeformGetAll with an underlying cache /// diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/UserRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/UserRepository.cs index 2452a3ba30..cd03c7cfce 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/UserRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/UserRepository.cs @@ -153,10 +153,7 @@ ORDER BY colName"; } public Guid CreateLoginSession(int userId, string requestingIpAddress, bool cleanStaleSessions = true) - { - // TODO: I know this doesn't follow the normal repository conventions which would require us to create a UserSessionRepository - //and also business logic models for these objects but that's just so overkill for what we are doing - //and now that everything is properly in a transaction (Scope) there doesn't seem to be much reason for using that anymore + { var now = DateTime.UtcNow; var dto = new UserLoginDto { @@ -186,13 +183,14 @@ ORDER BY colName"; // that query is going to run a *lot*, make it a template var t = SqlContext.Templates.Get("Umbraco.Core.UserRepository.ValidateLoginSession", s => s .Select() + .SelectTop(1) .From() .Where(x => x.SessionId == SqlTemplate.Arg("sessionId")) .ForUpdate()); var sql = t.Sql(sessionId); - var found = Database.Query(sql).FirstOrDefault(); + var found = Database.FirstOrDefault(sql); if (found == null || found.UserId != userId || found.LoggedOutUtc.HasValue) return false; diff --git a/src/Umbraco.Infrastructure/PropertyEditors/PropertyEditorsComponent.cs b/src/Umbraco.Infrastructure/PropertyEditors/PropertyEditorsComponent.cs index de071d1a1e..cd7b7a1f39 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/PropertyEditorsComponent.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/PropertyEditorsComponent.cs @@ -1,7 +1,11 @@ -using System.Linq; +using System; +using System.Collections.Generic; +using System.Linq; using Umbraco.Core.Composing; +using Umbraco.Core.Events; using Umbraco.Core.Models; using Umbraco.Core.PropertyEditors; +using Umbraco.Core.Services; using Umbraco.Core.Services.Implement; namespace Umbraco.Web.PropertyEditors @@ -9,6 +13,7 @@ namespace Umbraco.Web.PropertyEditors public sealed class PropertyEditorsComponent : IComponent { private readonly PropertyEditorCollection _propertyEditors; + private readonly List _terminate = new List(); public PropertyEditorsComponent(PropertyEditorCollection propertyEditors) { @@ -27,32 +32,48 @@ namespace Umbraco.Web.PropertyEditors } public void Terminate() - { } - - private static void Initialize(FileUploadPropertyEditor fileUpload) { - MediaService.Saving += fileUpload.MediaServiceSaving; - ContentService.Copied += fileUpload.ContentServiceCopied; - - MediaService.Deleted += (sender, args) - => args.MediaFilesToDelete.AddRange(fileUpload.ServiceDeleted(args.DeletedEntities.Cast())); - ContentService.Deleted += (sender, args) - => args.MediaFilesToDelete.AddRange(fileUpload.ServiceDeleted(args.DeletedEntities.Cast())); - MemberService.Deleted += (sender, args) - => args.MediaFilesToDelete.AddRange(fileUpload.ServiceDeleted(args.DeletedEntities.Cast())); + foreach (var t in _terminate) t(); } - private static void Initialize(ImageCropperPropertyEditor imageCropper) + private void Initialize(FileUploadPropertyEditor fileUpload) + { + MediaService.Saving += fileUpload.MediaServiceSaving; + _terminate.Add(() => MediaService.Saving -= fileUpload.MediaServiceSaving); + ContentService.Copied += fileUpload.ContentServiceCopied; + _terminate.Add(() => ContentService.Copied -= fileUpload.ContentServiceCopied); + + void mediaServiceDeleted(IMediaService sender, DeleteEventArgs args) => args.MediaFilesToDelete.AddRange(fileUpload.ServiceDeleted(args.DeletedEntities.Cast())); + MediaService.Deleted += mediaServiceDeleted; + _terminate.Add(() => MediaService.Deleted -= mediaServiceDeleted); + + void contentServiceDeleted(IContentService sender, DeleteEventArgs args) => args.MediaFilesToDelete.AddRange(fileUpload.ServiceDeleted(args.DeletedEntities.Cast())); + ContentService.Deleted += contentServiceDeleted; + _terminate.Add(() => ContentService.Deleted -= contentServiceDeleted); + + void memberServiceDeleted(IMemberService sender, DeleteEventArgs args) => args.MediaFilesToDelete.AddRange(fileUpload.ServiceDeleted(args.DeletedEntities.Cast())); + MemberService.Deleted += memberServiceDeleted; + _terminate.Add(() => MemberService.Deleted -= memberServiceDeleted); + } + + private void Initialize(ImageCropperPropertyEditor imageCropper) { MediaService.Saving += imageCropper.MediaServiceSaving; + _terminate.Add(() => MediaService.Saving -= imageCropper.MediaServiceSaving); ContentService.Copied += imageCropper.ContentServiceCopied; + _terminate.Add(() => ContentService.Copied -= imageCropper.ContentServiceCopied); - MediaService.Deleted += (sender, args) - => args.MediaFilesToDelete.AddRange(imageCropper.ServiceDeleted(args.DeletedEntities.Cast())); - ContentService.Deleted += (sender, args) - => args.MediaFilesToDelete.AddRange(imageCropper.ServiceDeleted(args.DeletedEntities.Cast())); - MemberService.Deleted += (sender, args) - => args.MediaFilesToDelete.AddRange(imageCropper.ServiceDeleted(args.DeletedEntities.Cast())); + void mediaServiceDeleted(IMediaService sender, DeleteEventArgs args) => args.MediaFilesToDelete.AddRange(imageCropper.ServiceDeleted(args.DeletedEntities.Cast())); + MediaService.Deleted += mediaServiceDeleted; + _terminate.Add(() => MediaService.Deleted -= mediaServiceDeleted); + + void contentServiceDeleted(IContentService sender, DeleteEventArgs args) => args.MediaFilesToDelete.AddRange(imageCropper.ServiceDeleted(args.DeletedEntities.Cast())); + ContentService.Deleted += contentServiceDeleted; + _terminate.Add(() => ContentService.Deleted -= contentServiceDeleted); + + void memberServiceDeleted(IMemberService sender, DeleteEventArgs args) => args.MediaFilesToDelete.AddRange(imageCropper.ServiceDeleted(args.DeletedEntities.Cast())); + MemberService.Deleted += memberServiceDeleted; + _terminate.Add(() => MemberService.Deleted -= memberServiceDeleted); } } } diff --git a/src/Umbraco.Infrastructure/Routing/RedirectTrackingComponent.cs b/src/Umbraco.Infrastructure/Routing/RedirectTrackingComponent.cs index f9256b3692..7fecba7c78 100644 --- a/src/Umbraco.Infrastructure/Routing/RedirectTrackingComponent.cs +++ b/src/Umbraco.Infrastructure/Routing/RedirectTrackingComponent.cs @@ -57,7 +57,12 @@ namespace Umbraco.Web.Routing } public void Terminate() - { } + { + ContentService.Publishing -= ContentService_Publishing; + ContentService.Published -= ContentService_Published; + ContentService.Moving -= ContentService_Moving; + ContentService.Moved -= ContentService_Moved; + } private void ContentService_Publishing(IContentService sender, PublishEventArgs args) { diff --git a/src/Umbraco.Infrastructure/Scheduling/SimpleTask.cs b/src/Umbraco.Infrastructure/Scheduling/SimpleTask.cs new file mode 100644 index 0000000000..a8603915b0 --- /dev/null +++ b/src/Umbraco.Infrastructure/Scheduling/SimpleTask.cs @@ -0,0 +1,32 @@ +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace Umbraco.Web.Scheduling +{ + /// + /// A simple task that executes a delegate synchronously + /// + internal class SimpleTask : IBackgroundTask + { + private readonly Action _action; + + public SimpleTask(Action action) + { + _action = action; + } + + public bool IsAsync => false; + + public void Run() => _action(); + + public Task RunAsync(CancellationToken token) + { + throw new NotImplementedException(); + } + + public void Dispose() + { + } + } +} diff --git a/src/Umbraco.Infrastructure/Search/ExamineComponent.cs b/src/Umbraco.Infrastructure/Search/ExamineComponent.cs index 037981d8b4..b173b7c398 100644 --- a/src/Umbraco.Infrastructure/Search/ExamineComponent.cs +++ b/src/Umbraco.Infrastructure/Search/ExamineComponent.cs @@ -5,6 +5,7 @@ using System.Linq; using Examine; using Umbraco.Core; using Umbraco.Core.Cache; +using Umbraco.Core.Hosting; using Umbraco.Core.Logging; using Umbraco.Core.Models; using Umbraco.Core.Scoping; @@ -13,9 +14,12 @@ using Umbraco.Core.Services.Changes; using Umbraco.Core.Sync; using Umbraco.Web.Cache; using Umbraco.Examine; +using Umbraco.Web.Scheduling; namespace Umbraco.Web.Search { + + public sealed class ExamineComponent : Umbraco.Core.Composing.IComponent { private readonly IExamineManager _examineManager; @@ -30,7 +34,7 @@ namespace Umbraco.Web.Search private readonly IMainDom _mainDom; private readonly IProfilingLogger _logger; private readonly IUmbracoIndexesCreator _indexCreator; - + private readonly BackgroundTaskRunner _indexItemTaskRunner; // the default enlist priority is 100 // enlist with a lower priority to ensure that anything "default" runs after us @@ -45,7 +49,8 @@ namespace Umbraco.Web.Search IPublishedContentValueSetBuilder publishedContentValueSetBuilder, IValueSetBuilder mediaValueSetBuilder, IValueSetBuilder memberValueSetBuilder, - BackgroundIndexRebuilder backgroundIndexRebuilder) + BackgroundIndexRebuilder backgroundIndexRebuilder, + IApplicationShutdownRegistry applicationShutdownRegistry) { _services = services; _scopeProvider = scopeProvider; @@ -58,6 +63,7 @@ namespace Umbraco.Web.Search _mainDom = mainDom; _logger = profilingLogger; _indexCreator = indexCreator; + _indexItemTaskRunner = new BackgroundTaskRunner(_logger, applicationShutdownRegistry); } public void Initialize() @@ -104,7 +110,13 @@ namespace Umbraco.Web.Search } public void Terminate() - { } + { + ContentCacheRefresher.CacheUpdated -= ContentCacheRefresherUpdated; + ContentTypeCacheRefresher.CacheUpdated -= ContentTypeCacheRefresherUpdated; + MediaCacheRefresher.CacheUpdated -= MediaCacheRefresherUpdated; + MemberCacheRefresher.CacheUpdated -= MemberCacheRefresherUpdated; + LanguageCacheRefresher.CacheUpdated -= LanguageCacheRefresherUpdated; + } #region Cache refresher updated event handlers @@ -557,12 +569,18 @@ namespace Umbraco.Web.Search } } + /// + /// An action that will execute at the end of the Scope being completed + /// private abstract class DeferedAction { public virtual void Execute() { } } + /// + /// Re-indexes an item on a background thread + /// private class DeferedReIndexForContent : DeferedAction { private readonly ExamineComponent _examineComponent; @@ -583,21 +601,32 @@ namespace Umbraco.Web.Search public static void Execute(ExamineComponent examineComponent, IContent content, bool isPublished) { - foreach (var index in examineComponent._examineManager.Indexes.OfType() - //filter the indexers - .Where(x => isPublished || !x.PublishedValuesOnly) - .Where(x => x.EnableDefaultEventHandler)) + // perform the ValueSet lookup on a background thread + examineComponent._indexItemTaskRunner.Add(new SimpleTask(() => { - //for content we have a different builder for published vs unpublished - var builder = index.PublishedValuesOnly - ? examineComponent._publishedContentValueSetBuilder - : (IValueSetBuilder)examineComponent._contentValueSetBuilder; + // for content we have a different builder for published vs unpublished + // we don't want to build more value sets than is needed so we'll lazily build 2 one for published one for non-published + var builders = new Dictionary>> + { + [true] = new Lazy>(() => examineComponent._publishedContentValueSetBuilder.GetValueSets(content).ToList()), + [false] = new Lazy>(() => examineComponent._contentValueSetBuilder.GetValueSets(content).ToList()) + }; - index.IndexItems(builder.GetValueSets(content)); - } + foreach (var index in examineComponent._examineManager.Indexes.OfType() + //filter the indexers + .Where(x => isPublished || !x.PublishedValuesOnly) + .Where(x => x.EnableDefaultEventHandler)) + { + var valueSet = builders[index.PublishedValuesOnly].Value; + index.IndexItems(valueSet); + } + })); } } + /// + /// Re-indexes an item on a background thread + /// private class DeferedReIndexForMedia : DeferedAction { private readonly ExamineComponent _examineComponent; @@ -618,18 +647,25 @@ namespace Umbraco.Web.Search public static void Execute(ExamineComponent examineComponent, IMedia media, bool isPublished) { - var valueSet = examineComponent._mediaValueSetBuilder.GetValueSets(media).ToList(); - - foreach (var index in examineComponent._examineManager.Indexes.OfType() - //filter the indexers - .Where(x => isPublished || !x.PublishedValuesOnly) - .Where(x => x.EnableDefaultEventHandler)) + // perform the ValueSet lookup on a background thread + examineComponent._indexItemTaskRunner.Add(new SimpleTask(() => { - index.IndexItems(valueSet); - } + var valueSet = examineComponent._mediaValueSetBuilder.GetValueSets(media).ToList(); + + foreach (var index in examineComponent._examineManager.Indexes.OfType() + //filter the indexers + .Where(x => isPublished || !x.PublishedValuesOnly) + .Where(x => x.EnableDefaultEventHandler)) + { + index.IndexItems(valueSet); + } + })); } } + /// + /// Re-indexes an item on a background thread + /// private class DeferedReIndexForMember : DeferedAction { private readonly ExamineComponent _examineComponent; @@ -648,13 +684,17 @@ namespace Umbraco.Web.Search public static void Execute(ExamineComponent examineComponent, IMember member) { - var valueSet = examineComponent._memberValueSetBuilder.GetValueSets(member).ToList(); - foreach (var index in examineComponent._examineManager.Indexes.OfType() - //filter the indexers - .Where(x => x.EnableDefaultEventHandler)) + // perform the ValueSet lookup on a background thread + examineComponent._indexItemTaskRunner.Add(new SimpleTask(() => { - index.IndexItems(valueSet); - } + var valueSet = examineComponent._memberValueSetBuilder.GetValueSets(member).ToList(); + foreach (var index in examineComponent._examineManager.Indexes.OfType() + //filter the indexers + .Where(x => x.EnableDefaultEventHandler)) + { + index.IndexItems(valueSet); + } + })); } } diff --git a/src/Umbraco.Infrastructure/Services/Implement/MemberService.cs b/src/Umbraco.Infrastructure/Services/Implement/MemberService.cs index 7ee6065210..13e8f3fd87 100644 --- a/src/Umbraco.Infrastructure/Services/Implement/MemberService.cs +++ b/src/Umbraco.Infrastructure/Services/Implement/MemberService.cs @@ -435,15 +435,10 @@ namespace Umbraco.Core.Services.Implement /// public IMember GetByUsername(string username) { - // TODO: Somewhere in here, whether at this level or the repository level, we need to add - // a caching mechanism since this method is used by all the membership providers and could be - // called quite a bit when dealing with members. - using (var scope = ScopeProvider.CreateScope(autoComplete: true)) { - scope.ReadLock(Constants.Locks.MemberTree); - var query = Query().Where(x => x.Username.Equals(username)); - return _memberRepository.Get(query).FirstOrDefault(); + scope.ReadLock(Constants.Locks.MemberTree); + return _memberRepository.GetByUsername(username); } } @@ -793,12 +788,17 @@ namespace Umbraco.Core.Services.Implement #region Save - /// - /// Saves an - /// - /// to Save - /// Optional parameter to raise events. - /// Default is True otherwise set to False to not raise events + /// + public void SetLastLogin(string username, DateTime date) + { + using (var scope = ScopeProvider.CreateScope()) + { + _memberRepository.SetLastLogin(username, date); + scope.Complete(); + } + } + + /// public void Save(IMember member, bool raiseEvents = true) { //trimming username and email to make sure we have no trailing space @@ -834,12 +834,7 @@ namespace Umbraco.Core.Services.Implement } } - /// - /// 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 members, bool raiseEvents = true) { var membersA = members.ToArray(); diff --git a/src/Umbraco.Infrastructure/Services/Implement/UserService.cs b/src/Umbraco.Infrastructure/Services/Implement/UserService.cs index 24f1af5843..49a806499a 100644 --- a/src/Umbraco.Infrastructure/Services/Implement/UserService.cs +++ b/src/Umbraco.Infrastructure/Services/Implement/UserService.cs @@ -252,6 +252,13 @@ namespace Umbraco.Core.Services.Implement } } + // explicit implementation because we don't need it now but due to the way that the members membership provider is put together + // this method must exist in this service as an implementation (legacy) + void IMembershipMemberService.SetLastLogin(string username, DateTime date) + { + throw new NotSupportedException("This method is not implemented or supported for users"); + } + /// /// Saves an /// diff --git a/src/Umbraco.ModelsBuilder.Embedded/Compose/ModelsBuilderComponent.cs b/src/Umbraco.ModelsBuilder.Embedded/Compose/ModelsBuilderComponent.cs index 32cfd3057e..18c622911e 100644 --- a/src/Umbraco.ModelsBuilder.Embedded/Compose/ModelsBuilderComponent.cs +++ b/src/Umbraco.ModelsBuilder.Embedded/Compose/ModelsBuilderComponent.cs @@ -53,32 +53,38 @@ namespace Umbraco.ModelsBuilder.Embedded.Compose } public void Terminate() - { } + { + ServerVariablesParser.Parsing -= ServerVariablesParser_Parsing; + ContentModelBinder.ModelBindingException -= ContentModelBinder_ModelBindingException; + FileService.SavingTemplate -= FileService_SavingTemplate; + } private void InstallServerVars() { // register our url - for the backoffice api - ServerVariablesParser.Parsing += (sender, serverVars) => - { - if (!serverVars.ContainsKey("umbracoUrls")) - throw new ArgumentException("Missing umbracoUrls."); - var umbracoUrlsObject = serverVars["umbracoUrls"]; - if (umbracoUrlsObject == null) - throw new ArgumentException("Null umbracoUrls"); - if (!(umbracoUrlsObject is Dictionary umbracoUrls)) - throw new ArgumentException("Invalid umbracoUrls"); + ServerVariablesParser.Parsing += ServerVariablesParser_Parsing; + } - if (!serverVars.ContainsKey("umbracoPlugins")) - throw new ArgumentException("Missing umbracoPlugins."); - if (!(serverVars["umbracoPlugins"] is Dictionary umbracoPlugins)) - throw new ArgumentException("Invalid umbracoPlugins"); + private void ServerVariablesParser_Parsing(object sender, Dictionary serverVars) + { + if (!serverVars.ContainsKey("umbracoUrls")) + throw new ArgumentException("Missing umbracoUrls."); + var umbracoUrlsObject = serverVars["umbracoUrls"]; + if (umbracoUrlsObject == null) + throw new ArgumentException("Null umbracoUrls"); + if (!(umbracoUrlsObject is Dictionary umbracoUrls)) + throw new ArgumentException("Invalid umbracoUrls"); - if (HttpContext.Current == null) throw new InvalidOperationException("HttpContext is null"); - var urlHelper = new UrlHelper(new RequestContext(new HttpContextWrapper(HttpContext.Current), new RouteData())); + if (!serverVars.ContainsKey("umbracoPlugins")) + throw new ArgumentException("Missing umbracoPlugins."); + if (!(serverVars["umbracoPlugins"] is Dictionary umbracoPlugins)) + throw new ArgumentException("Invalid umbracoPlugins"); - umbracoUrls["modelsBuilderBaseUrl"] = urlHelper.GetUmbracoApiServiceBaseUrl(controller => controller.BuildModels()); - umbracoPlugins["modelsBuilder"] = GetModelsBuilderSettings(); - }; + if (HttpContext.Current == null) throw new InvalidOperationException("HttpContext is null"); + var urlHelper = new UrlHelper(new RequestContext(new HttpContextWrapper(HttpContext.Current), new RouteData())); + + umbracoUrls["modelsBuilderBaseUrl"] = urlHelper.GetUmbracoApiServiceBaseUrl(controller => controller.BuildModels()); + umbracoPlugins["modelsBuilder"] = GetModelsBuilderSettings(); } private Dictionary GetModelsBuilderSettings() diff --git a/src/Umbraco.Tests/Persistence/Repositories/UserRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/UserRepositoryTest.cs index 8938a69579..e0844ac57b 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/UserRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/UserRepositoryTest.cs @@ -14,6 +14,11 @@ using Umbraco.Tests.Testing; using Umbraco.Core.PropertyEditors; using System; using Umbraco.Core.Configuration; +using Umbraco.Core.Persistence; +using Umbraco.Core.Persistence.Dtos; +using Umbraco.Core.Persistence.Mappers; +using Umbraco.Tests.Common.Builders; +using Umbraco.Tests.Common.Builders.Extensions; namespace Umbraco.Tests.Persistence.Repositories { @@ -77,6 +82,104 @@ namespace Umbraco.Tests.Persistence.Repositories return new UserGroupRepository(accessor, AppCaches.Disabled, Logger, ShortStringHelper); } + [Test] + public void Validate_Login_Session() + { + // Arrange + var provider = TestObjects.GetScopeProvider(Logger); + var user = MockedUser.CreateUser(); + using (var scope = provider.CreateScope(autoComplete: true)) + { + var repository = CreateRepository(provider); + repository.Save(user); + } + + using (var scope = provider.CreateScope(autoComplete: true)) + { + var repository = CreateRepository(provider); + var sessionId = repository.CreateLoginSession(user.Id, "1.2.3.4"); + + // manually update this record to be in the past + scope.Database.Execute(SqlContext.Sql() + .Update(u => u.Set(x => x.LoggedOutUtc, DateTime.UtcNow.AddDays(-100))) + .Where(x => x.SessionId == sessionId)); + + var isValid = repository.ValidateLoginSession(user.Id, sessionId); + Assert.IsFalse(isValid); + + // create a new one + sessionId = repository.CreateLoginSession(user.Id, "1.2.3.4"); + isValid = repository.ValidateLoginSession(user.Id, sessionId); + Assert.IsTrue(isValid); + } + } + + [Test] + public void Can_Perform_Add_On_UserRepository() + { + // Arrange + var provider = TestObjects.GetScopeProvider(Logger); + using (var scope = provider.CreateScope(autoComplete: true)) + { + var repository = CreateRepository(provider); + + var user = MockedUser.CreateUser(); + + // Act + repository.Save(user); + + + // Assert + Assert.That(user.HasIdentity, Is.True); + } + } + + [Test] + public void Can_Perform_Multiple_Adds_On_UserRepository() + { + // Arrange + var provider = TestObjects.GetScopeProvider(Logger); + using (var scope = provider.CreateScope(autoComplete: true)) + { + var repository = CreateRepository(provider); + + var user1 = MockedUser.CreateUser("1"); + var use2 = MockedUser.CreateUser("2"); + + // Act + repository.Save(user1); + + repository.Save(use2); + + + // Assert + Assert.That(user1.HasIdentity, Is.True); + Assert.That(use2.HasIdentity, Is.True); + } + } + + [Test] + public void Can_Verify_Fresh_Entity_Is_Not_Dirty() + { + // Arrange + var provider = TestObjects.GetScopeProvider(Logger); + using (var scope = provider.CreateScope(autoComplete: true)) + { + var repository = CreateRepository(provider); + + var user = MockedUser.CreateUser(); + repository.Save(user); + + + // Act + var resolved = repository.Get((int)user.Id); + bool dirty = ((User)resolved).IsDirty(); + + // Assert + Assert.That(dirty, Is.False); + } + } + [Test] public void Can_Perform_Update_On_UserRepository() { @@ -85,7 +188,7 @@ namespace Umbraco.Tests.Persistence.Repositories // Arrange var provider = TestObjects.GetScopeProvider(Logger); - using (var scope = provider.CreateScope()) + using (var scope = provider.CreateScope(autoComplete: true)) { var userRepository = CreateRepository(provider); var contentRepository = CreateContentRepository(provider, out var contentTypeRepo); @@ -139,6 +242,268 @@ namespace Umbraco.Tests.Persistence.Repositories } } + [Test] + public void Can_Perform_Delete_On_UserRepository() + { + // Arrange + var provider = TestObjects.GetScopeProvider(Logger); + using (var scope = provider.CreateScope(autoComplete: true)) + { + var repository = CreateRepository(provider); + + var user = MockedUser.CreateUser(); + + // Act + repository.Save(user); + + var id = user.Id; + + var repository2 = new UserRepository((IScopeAccessor) provider, AppCaches.Disabled, Logger, Mock.Of(),TestObjects.GetGlobalSettings(), Mock.Of()); + + repository2.Delete(user); + + + var resolved = repository2.Get((int) id); + + // Assert + Assert.That(resolved, Is.Null); + } + } + + [Test] + public void Can_Perform_Get_On_UserRepository() + { + // Arrange + var provider = TestObjects.GetScopeProvider(Logger); + using (var scope = provider.CreateScope(autoComplete: true)) + { + var repository = CreateRepository(provider); + var userGroupRepository = CreateUserGroupRepository(provider); + + var user = CreateAndCommitUserWithGroup(repository, userGroupRepository); + + // Act + var updatedItem = repository.Get(user.Id); + + // FIXME: this test cannot work, user has 2 sections but the way it's created, + // they don't show, so the comparison with updatedItem fails - fix! + + // Assert + AssertPropertyValues(updatedItem, user); + } + } + + [Test] + public void Can_Perform_GetByQuery_On_UserRepository() + { + // Arrange + var provider = TestObjects.GetScopeProvider(Logger); + using (var scope = provider.CreateScope(autoComplete: true)) + { + var repository = CreateRepository(provider); + + CreateAndCommitMultipleUsers(repository); + + // Act + var query = scope.SqlContext.Query().Where(x => x.Username == "TestUser1"); + var result = repository.Get(query); + + // Assert + Assert.That(result.Count(), Is.GreaterThanOrEqualTo(1)); + } + } + + [Test] + public void Can_Perform_GetAll_By_Param_Ids_On_UserRepository() + { + // Arrange + var provider = TestObjects.GetScopeProvider(Logger); + using (var scope = provider.CreateScope(autoComplete: true)) + { + var repository = CreateRepository(provider); + + var users = CreateAndCommitMultipleUsers(repository); + + // Act + var result = repository.GetMany((int) users[0].Id, (int) users[1].Id); + + // Assert + Assert.That(result, Is.Not.Null); + Assert.That(result.Any(), Is.True); + Assert.That(result.Count(), Is.EqualTo(2)); + } + } + + [Test] + public void Can_Perform_GetAll_On_UserRepository() + { + // Arrange + var provider = TestObjects.GetScopeProvider(Logger); + using (var scope = provider.CreateScope(autoComplete: true)) + { + var repository = CreateRepository(provider); + + CreateAndCommitMultipleUsers(repository); + + // Act + var result = repository.GetMany(); + + // Assert + Assert.That(result, Is.Not.Null); + Assert.That(result.Any(), Is.True); + Assert.That(result.Count(), Is.GreaterThanOrEqualTo(3)); + } + } + + [Test] + public void Can_Perform_Exists_On_UserRepository() + { + // Arrange + var provider = TestObjects.GetScopeProvider(Logger); + using (var scope = provider.CreateScope(autoComplete: true)) + { + var repository = CreateRepository(provider); + + var users = CreateAndCommitMultipleUsers(repository); + + // Act + var exists = repository.Exists(users[0].Id); + + // Assert + Assert.That(exists, Is.True); + } + } + + [Test] + public void Can_Perform_Count_On_UserRepository() + { + // Arrange + var provider = TestObjects.GetScopeProvider(Logger); + using (var scope = provider.CreateScope(autoComplete: true)) + { + var repository = CreateRepository(provider); + + var users = CreateAndCommitMultipleUsers(repository); + + // Act + var query = scope.SqlContext.Query().Where(x => x.Username == "TestUser1" || x.Username == "TestUser2"); + var result = repository.Count(query); + + // Assert + Assert.AreEqual(2, result); + } + } + + [Test] + public void Can_Get_Paged_Results_By_Query_And_Filter_And_Groups() + { + var provider = TestObjects.GetScopeProvider(Logger); + using (var scope = provider.CreateScope(autoComplete: true)) + { + var repository = CreateRepository(provider); + + var users = CreateAndCommitMultipleUsers(repository); + var query = provider.SqlContext.Query().Where(x => x.Username == "TestUser1" || x.Username == "TestUser2"); + + try + { + scope.Database.AsUmbracoDatabase().EnableSqlTrace = true; + scope.Database.AsUmbracoDatabase().EnableSqlCount = true; + + // Act + var result = repository.GetPagedResultsByQuery(query, 0, 10, out var totalRecs, user => user.Id, Direction.Ascending, + excludeUserGroups: new[] { Constants.Security.TranslatorGroupAlias }, + filter: provider.SqlContext.Query().Where(x => x.Id > -1)); + + // Assert + Assert.AreEqual(2, totalRecs); + } + finally + { + scope.Database.AsUmbracoDatabase().EnableSqlTrace = false; + scope.Database.AsUmbracoDatabase().EnableSqlCount = false; + } + } + + } + + [Test] + public void Can_Get_Paged_Results_With_Filter_And_Groups() + { + var provider = TestObjects.GetScopeProvider(Logger); + using (var scope = provider.CreateScope(autoComplete: true)) + { + var repository = CreateRepository(provider); + + var users = CreateAndCommitMultipleUsers(repository); + + try + { + scope.Database.AsUmbracoDatabase().EnableSqlTrace = true; + scope.Database.AsUmbracoDatabase().EnableSqlCount = true; + + // Act + var result = repository.GetPagedResultsByQuery(null, 0, 10, out var totalRecs, user => user.Id, Direction.Ascending, + includeUserGroups: new[] { Constants.Security.AdminGroupAlias, Constants.Security.SensitiveDataGroupAlias }, + excludeUserGroups: new[] { Constants.Security.TranslatorGroupAlias }, + filter: provider.SqlContext.Query().Where(x => x.Id == -1)); + + // Assert + Assert.AreEqual(1, totalRecs); + } + finally + { + scope.Database.AsUmbracoDatabase().EnableSqlTrace = false; + scope.Database.AsUmbracoDatabase().EnableSqlCount = false; + } + } + } + + [Test] + public void Can_Invalidate_SecurityStamp_On_Username_Change() + { + // Arrange + var provider = TestObjects.GetScopeProvider(Logger); + using (var scope = provider.CreateScope(autoComplete: true)) + { + var repository = CreateRepository(provider); + var userGroupRepository = CreateUserGroupRepository(provider); + + var user = CreateAndCommitUserWithGroup(repository, userGroupRepository); + var originalSecurityStamp = user.SecurityStamp; + + // Ensure when user generated a security stamp is present + Assert.That(user.SecurityStamp, Is.Not.Null); + Assert.That(user.SecurityStamp, Is.Not.Empty); + + // Update username + user.Username = user.Username + "UPDATED"; + repository.Save(user); + + // Get the user + var updatedUser = repository.Get(user.Id); + + // Ensure the Security Stamp is invalidated & no longer the same + Assert.AreNotEqual(originalSecurityStamp, updatedUser.SecurityStamp); + } + } + + private void AssertPropertyValues(IUser updatedItem, IUser originalUser) + { + Assert.That(updatedItem.Id, Is.EqualTo(originalUser.Id)); + Assert.That(updatedItem.Name, Is.EqualTo(originalUser.Name)); + Assert.That(updatedItem.Language, Is.EqualTo(originalUser.Language)); + Assert.That(updatedItem.IsApproved, Is.EqualTo(originalUser.IsApproved)); + Assert.That(updatedItem.RawPasswordValue, Is.EqualTo(originalUser.RawPasswordValue)); + Assert.That(updatedItem.IsLockedOut, Is.EqualTo(originalUser.IsLockedOut)); + Assert.IsTrue(updatedItem.StartContentIds.UnsortedSequenceEqual(originalUser.StartContentIds)); + Assert.IsTrue(updatedItem.StartMediaIds.UnsortedSequenceEqual(originalUser.StartMediaIds)); + Assert.That(updatedItem.Email, Is.EqualTo(originalUser.Email)); + Assert.That(updatedItem.Username, Is.EqualTo(originalUser.Username)); + Assert.That(updatedItem.AllowedSections.Count(), Is.EqualTo(originalUser.AllowedSections.Count())); + foreach (var allowedSection in originalUser.AllowedSections) + Assert.IsTrue(updatedItem.AllowedSections.Contains(allowedSection)); + } private static User CreateAndCommitUserWithGroup(IUserRepository repository, IUserGroupRepository userGroupRepository) { @@ -153,6 +518,16 @@ namespace Umbraco.Tests.Persistence.Repositories return user; } + private IUser[] CreateAndCommitMultipleUsers(IUserRepository repository) + { + var user1 = new UserBuilder().WithoutIdentity().WithSuffix("1").Build(); + var user2 = new UserBuilder().WithoutIdentity().WithSuffix("2").Build(); + var user3 = new UserBuilder().WithoutIdentity().WithSuffix("3").Build(); + repository.Save(user1); + repository.Save(user2); + repository.Save(user3); + return new IUser[] { user1, user2, user3 }; + } } } diff --git a/src/Umbraco.Tests/Services/MemberServiceTests.cs b/src/Umbraco.Tests/Services/MemberServiceTests.cs index e7adbdacbf..79fbfb28ed 100644 --- a/src/Umbraco.Tests/Services/MemberServiceTests.cs +++ b/src/Umbraco.Tests/Services/MemberServiceTests.cs @@ -38,6 +38,60 @@ namespace Umbraco.Tests.Services base.SetUp(); } + [Test] + public void Can_Update_Member_Property_Value() + { + IMemberType memberType = MockedContentTypes.CreateSimpleMemberType(); + ServiceContext.MemberTypeService.Save(memberType); + IMember member = MockedMember.CreateSimpleMember(memberType, "hello", "helloworld@test123.com", "hello", "hello"); + member.SetValue("title", "title of mine"); + ServiceContext.MemberService.Save(member); + + // re-get + member = ServiceContext.MemberService.GetById(member.Id); + member.SetValue("title", "another title of mine"); + ServiceContext.MemberService.Save(member); + + // re-get + member = ServiceContext.MemberService.GetById(member.Id); + Assert.AreEqual("another title of mine", member.GetValue("title")); + } + + [Test] + public void Can_Get_By_Username() + { + var memberType = ServiceContext.MemberTypeService.Get("member"); + IMember member = new Member("xname", "xemail", "xusername", "xrawpassword", memberType, true); + ServiceContext.MemberService.Save(member); + + var member2 = ServiceContext.MemberService.GetByUsername(member.Username); + + Assert.IsNotNull(member2); + Assert.AreEqual(member.Email, member2.Email); + } + + [Test] + public void Can_Set_Last_Login_Date() + { + var now = DateTime.Now; + var memberType = ServiceContext.MemberTypeService.Get("member"); + IMember member = new Member("xname", "xemail", "xusername", "xrawpassword", memberType, true) + { + LastLoginDate = now, + UpdateDate = now + }; + ServiceContext.MemberService.Save(member); + + var newDate = now.AddDays(10); + ServiceContext.MemberService.SetLastLogin(member.Username, newDate); + + //re-get + member = ServiceContext.MemberService.GetById(member.Id); + + Assert.That(member.LastLoginDate, Is.EqualTo(newDate).Within(1).Seconds); + Assert.That(member.UpdateDate, Is.EqualTo(newDate).Within(1).Seconds); + } + [Test] public void Can_Create_Member_With_Properties() { diff --git a/src/Umbraco.Web/Compose/AuditEventsComponent.cs b/src/Umbraco.Web/Compose/AuditEventsComponent.cs index bd2520aa90..2a3eb884a8 100644 --- a/src/Umbraco.Web/Compose/AuditEventsComponent.cs +++ b/src/Umbraco.Web/Compose/AuditEventsComponent.cs @@ -48,7 +48,19 @@ namespace Umbraco.Core.Compose } public void Terminate() - { } + { + UserService.SavedUserGroup -= OnSavedUserGroupWithUsers; + + UserService.SavedUser -= OnSavedUser; + UserService.DeletedUser -= OnDeletedUser; + UserService.UserGroupPermissionsAssigned -= UserGroupPermissionAssigned; + + MemberService.Saved -= OnSavedMember; + MemberService.Deleted -= OnDeletedMember; + MemberService.AssignedRoles -= OnAssignedRoles; + MemberService.RemovedRoles -= OnRemovedRoles; + MemberService.Exported -= OnMemberExported; + } public static IUser UnknownUser(IGlobalSettings globalSettings) => new User(globalSettings) { Id = Constants.Security.UnknownUserId, Name = Constants.Security.UnknownUserName, Email = "" }; diff --git a/src/Umbraco.Web/Compose/BackOfficeUserAuditEventsComponent.cs b/src/Umbraco.Web/Compose/BackOfficeUserAuditEventsComponent.cs index 84fb0e6bb8..445b1ccf83 100644 --- a/src/Umbraco.Web/Compose/BackOfficeUserAuditEventsComponent.cs +++ b/src/Umbraco.Web/Compose/BackOfficeUserAuditEventsComponent.cs @@ -38,7 +38,19 @@ namespace Umbraco.Web.Compose } public void Terminate() - { } + { + //BackOfficeUserManager.AccountLocked -= ; + //BackOfficeUserManager.AccountUnlocked -= ; + BackOfficeUserManager.ForgotPasswordRequested -= OnForgotPasswordRequest; + BackOfficeUserManager.ForgotPasswordChangedSuccess -= OnForgotPasswordChange; + BackOfficeUserManager.LoginFailed -= OnLoginFailed; + //BackOfficeUserManager.LoginRequiresVerification -= ; + BackOfficeUserManager.LoginSuccess -= OnLoginSuccess; + BackOfficeUserManager.LogoutSuccess -= OnLogoutSuccess; + BackOfficeUserManager.PasswordChanged -= OnPasswordChanged; + BackOfficeUserManager.PasswordReset -= OnPasswordReset; + //BackOfficeUserManager.ResetAccessFailedCount -= ; + } private IUser GetPerformingUser(int userId) { diff --git a/src/Umbraco.Web/Logging/WebProfilerComponent.cs b/src/Umbraco.Web/Logging/WebProfilerComponent.cs index 2959e12ad7..1cb2142199 100755 --- a/src/Umbraco.Web/Logging/WebProfilerComponent.cs +++ b/src/Umbraco.Web/Logging/WebProfilerComponent.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Web; using Umbraco.Core.Composing; using Umbraco.Core.Logging; @@ -9,6 +10,7 @@ namespace Umbraco.Web.Logging { private readonly WebProfiler _profiler; private readonly bool _profile; + private readonly List _terminate = new List(); public WebProfilerComponent(IProfiler profiler, ILogger logger) { @@ -35,15 +37,23 @@ namespace Umbraco.Web.Logging } public void Terminate() - { } + { + UmbracoApplicationBase.ApplicationInit -= InitializeApplication; + foreach (var t in _terminate) t(); + } private void InitializeApplication(object sender, EventArgs args) { if (!(sender is HttpApplication app)) return; // for *each* application (this will run more than once) - app.BeginRequest += (s, a) => _profiler.UmbracoApplicationBeginRequest(s, a); - app.EndRequest += (s, a) => _profiler.UmbracoApplicationEndRequest(s, a); + void beginRequest(object s, EventArgs a) => _profiler.UmbracoApplicationBeginRequest(s, a); + app.BeginRequest += beginRequest; + _terminate.Add(() => app.BeginRequest -= beginRequest); + + void endRequest(object s, EventArgs a) => _profiler.UmbracoApplicationEndRequest(s, a); + app.EndRequest += endRequest; + _terminate.Add(() => app.EndRequest -= endRequest); } } } diff --git a/src/Umbraco.Web/Security/MembershipHelper.cs b/src/Umbraco.Web/Security/MembershipHelper.cs index f13be1c6ea..8ca5af397b 100644 --- a/src/Umbraco.Web/Security/MembershipHelper.cs +++ b/src/Umbraco.Web/Security/MembershipHelper.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Text; using System.Web.Security; using Umbraco.Core; using Umbraco.Core.Logging; @@ -300,8 +299,9 @@ namespace Umbraco.Web.Security { return false; } - //Set member online - var member = provider.GetUser(username, true); + // Get the member, do not set to online - this is done implicitly as part of ValidateUser which is consistent with + // how the .NET framework SqlMembershipProvider works. Passing in true will just cause more unnecessary SQL queries/locks. + var member = provider.GetUser(username, false); if (member == null) { //this should not happen @@ -770,29 +770,12 @@ namespace Umbraco.Web.Security /// private IMember GetCurrentPersistedMember() { - return _appCaches.RequestCache.GetCacheItem( - GetCacheKey("GetCurrentPersistedMember"), () => - { - var provider = _membershipProvider; + var provider = _membershipProvider; var username = provider.GetCurrentUserName(); - var member = _memberService.GetByUsername(username); - return member; - }); - } - - private static string GetCacheKey(string key, params object[] additional) - { - var sb = new StringBuilder(); - sb.Append(typeof(MembershipHelper).Name); - sb.Append("-"); - sb.Append(key); - foreach (var s in additional) - { - sb.Append("-"); - sb.Append(s); - } - return sb.ToString(); + // The result of this is cached by the MemberRepository + var member = _memberService.GetByUsername(username); + return member; } /// diff --git a/src/Umbraco.Web/Security/Providers/UmbracoMembershipProvider.cs b/src/Umbraco.Web/Security/Providers/UmbracoMembershipProvider.cs index b099110911..d059113e0a 100644 --- a/src/Umbraco.Web/Security/Providers/UmbracoMembershipProvider.cs +++ b/src/Umbraco.Web/Security/Providers/UmbracoMembershipProvider.cs @@ -12,7 +12,6 @@ using Umbraco.Core.Logging; using Umbraco.Core.Models.Membership; using Umbraco.Net; using Umbraco.Core.Persistence.Querying; -using Umbraco.Core.Security; using Umbraco.Core.Services; using Umbraco.Web.Composing; @@ -321,15 +320,16 @@ namespace Umbraco.Web.Security.Providers if (userIsOnline) { - member.LastLoginDate = DateTime.Now; - member.UpdateDate = DateTime.Now; - //don't raise events for this! It just sets the member dates, if we do raise events this will - // cause all distributed cache to execute - which will clear out some caches we don't want. - // http://issues.umbraco.org/issue/U4-3451 - // when upgrading from 7.2 to 7.3 trying to save will throw if (_umbracoVersion.Current >= new Version(7, 3, 0, 0)) - MemberService.Save(member, false); + { + var now = DateTime.Now; + // update the database data directly instead of a full member save which requires DB locks + MemberService.SetLastLogin(username, now); + member.LastLoginDate = now; + member.UpdateDate = now; + } + } return ConvertToMembershipUser(member); @@ -521,6 +521,8 @@ namespace Umbraco.Web.Security.Providers var authenticated = PasswordSecurity.VerifyPassword(password, member.RawPasswordValue); + var requiresFullSave = false; + if (authenticated == false) { // TODO: Increment login attempts - lock if too many. @@ -540,6 +542,8 @@ namespace Umbraco.Web.Security.Providers { Current.Logger.Info("Login attempt failed for username {Username} from IP address {IpAddress}", username, _ipResolver.GetCurrentRequestIpAddress()); } + + requiresFullSave = true; } else { @@ -547,6 +551,7 @@ namespace Umbraco.Web.Security.Providers { //we have successfully logged in, reset the AccessFailedCount member.FailedPasswordAttempts = 0; + requiresFullSave = true; } member.LastLoginDate = DateTime.Now; @@ -554,15 +559,23 @@ namespace Umbraco.Web.Security.Providers Current.Logger.Info("Login attempt succeeded for username {Username} from IP address {IpAddress}", username, _ipResolver.GetCurrentRequestIpAddress()); } - //don't raise events for this! It just sets the member dates, if we do raise events this will + // don't raise events for this! It just sets the member dates, if we do raise events this will // cause all distributed cache to execute - which will clear out some caches we don't want. // http://issues.umbraco.org/issue/U4-3451 // TODO: In v8 we aren't going to have an overload to disable events, so we'll need to make a different method // for this type of thing (i.e. UpdateLastLogin or similar). - // when upgrading from 7.2 to 7.3 trying to save will throw - if (_umbracoVersion.Current >= new Version(7, 3, 0, 0)) - MemberService.Save(member, false); + if (requiresFullSave) + { + // when upgrading from 7.2 to 7.3 trying to save will throw + if (_umbracoVersion.Current >= new Version(7, 3, 0, 0)) + MemberService.Save(member, false); + } + else + { + // set the last login date without full save (fast, no locks) + MemberService.SetLastLogin(member.Username, member.LastLoginDate); + } return new ValidateUserResult { diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index 6b612a4304..294253417f 100644 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -156,7 +156,6 @@ - @@ -234,6 +233,7 @@ +