From 0a10d3176d1f0b5695dcdd246e649e9f316236db Mon Sep 17 00:00:00 2001 From: Emma Garland Date: Sat, 20 Feb 2021 19:16:31 +0000 Subject: [PATCH 001/188] Initial changes to password model --- .../Models/ChangingPasswordModel.cs | 27 +++++- .../Models/ContentEditing/UserSave.cs | 3 +- src/Umbraco.Core/Models/IMemberUserAdapter.cs | 10 ++ src/Umbraco.Core/Models/MemberUserAdapter.cs | 96 +++++++++++++++++++ .../Security/IUmbracoUserManager.cs | 9 +- .../Security/UmbracoUserManager.cs | 14 +-- .../Controllers/MemberControllerUnitTests.cs | 68 +++++++------ .../Controllers/CurrentUserController.cs | 65 +++++++------ .../Controllers/MemberController.cs | 60 ++++++++++-- .../Controllers/UsersController.cs | 44 +++++---- .../UmbracoBuilderExtensions.cs | 2 + .../Security/IPasswordChanger.cs | 16 ++++ .../Security/PasswordChanger.cs | 93 ++++++++++-------- 13 files changed, 360 insertions(+), 147 deletions(-) create mode 100644 src/Umbraco.Core/Models/IMemberUserAdapter.cs create mode 100644 src/Umbraco.Core/Models/MemberUserAdapter.cs create mode 100644 src/Umbraco.Web.BackOffice/Security/IPasswordChanger.cs diff --git a/src/Umbraco.Core/Models/ChangingPasswordModel.cs b/src/Umbraco.Core/Models/ChangingPasswordModel.cs index 3816044c0a..c668475275 100644 --- a/src/Umbraco.Core/Models/ChangingPasswordModel.cs +++ b/src/Umbraco.Core/Models/ChangingPasswordModel.cs @@ -1,6 +1,6 @@ -using System.Runtime.Serialization; +using System.Runtime.Serialization; -namespace Umbraco.Web.Models +namespace Umbraco.Core.Models { /// /// A model representing the data required to set a member/user password depending on the provider installed. @@ -20,9 +20,30 @@ namespace Umbraco.Web.Models public string OldPassword { get; set; } /// - /// The id of the user - required to allow changing password without the entire UserSave model + /// The ID of the current user/member requesting the password change + /// For users, required to allow changing password without the entire UserSave model /// [DataMember(Name = "id")] public int Id { get; set; } + + /// + /// The username of the user/member who is changing the password + /// + public string CurrentUsername { get; set; } + + /// + /// The ID of the user/member whose password is being changed + /// + public int SavingUserId { get; set; } + + /// + /// The username of the user/memeber whose password is being changed + /// + public string SavingUsername { get; set; } + + /// + /// True if the current user has access to change the password for the member/user + /// + public bool CurrentUserHasSectionAccess { get; set; } } } diff --git a/src/Umbraco.Core/Models/ContentEditing/UserSave.cs b/src/Umbraco.Core/Models/ContentEditing/UserSave.cs index 2533ebb105..427340700d 100644 --- a/src/Umbraco.Core/Models/ContentEditing/UserSave.cs +++ b/src/Umbraco.Core/Models/ContentEditing/UserSave.cs @@ -1,7 +1,8 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Linq; using System.Runtime.Serialization; +using Umbraco.Core.Models; namespace Umbraco.Web.Models.ContentEditing { diff --git a/src/Umbraco.Core/Models/IMemberUserAdapter.cs b/src/Umbraco.Core/Models/IMemberUserAdapter.cs new file mode 100644 index 0000000000..9238ae7da3 --- /dev/null +++ b/src/Umbraco.Core/Models/IMemberUserAdapter.cs @@ -0,0 +1,10 @@ +using System.Collections.Generic; +using Umbraco.Core.Models.Membership; + +namespace Umbraco.Core.Models +{ + public interface IMemberUserAdapter : IUser + { + IEnumerable AllowedSections { get; } + } +} diff --git a/src/Umbraco.Core/Models/MemberUserAdapter.cs b/src/Umbraco.Core/Models/MemberUserAdapter.cs new file mode 100644 index 0000000000..3d5ee7a33a --- /dev/null +++ b/src/Umbraco.Core/Models/MemberUserAdapter.cs @@ -0,0 +1,96 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using Umbraco.Core.Models.Membership; + +namespace Umbraco.Core.Models +{ + public class MemberUserAdapter : IMemberUserAdapter + { + private readonly IMember _member; + + /// + /// Initializes a new instance of the class. + /// Member adaptor to use existing user change password functionality and other shared functions + /// + /// The member to adapt + public MemberUserAdapter(IMember member) + { + _member = member; + } + + // This is the only reason we currently need this adaptor + public IEnumerable AllowedSections => new List() + { + Constants.Applications.Users + }; + + + public UserState UserState { get; } + public string Name { get; set; } + public int SessionTimeout { get; set; } + public int[] StartContentIds { get; set; } + public int[] StartMediaIds { get; set; } + public string Language { get; set; } + public DateTime? InvitedDate { get; set; } + public IEnumerable Groups { get; } + public void RemoveGroup(string @group) => throw new NotImplementedException(); + + public void ClearGroups() => throw new NotImplementedException(); + + public void AddGroup(IReadOnlyUserGroup @group) => throw new NotImplementedException(); + + public IProfile ProfileData { get; } + public string Avatar { get; set; } + public string TourData { get; set; } + public T FromUserCache(string cacheKey) where T : class => throw new NotImplementedException(); + + public void ToUserCache(string cacheKey, T vals) where T : class => throw new NotImplementedException(); + + public object DeepClone() => throw new NotImplementedException(); + + public int Id { get; set; } + public Guid Key { get; set; } + public DateTime CreateDate { get; set; } + public DateTime UpdateDate { get; set; } + public DateTime? DeleteDate { get; set; } + public bool HasIdentity { get; } + public void ResetIdentity() => throw new NotImplementedException(); + + public string Username { get; set; } + public string Email { get; set; } + public DateTime? EmailConfirmedDate { get; set; } + public string RawPasswordValue { get; set; } + public string PasswordConfiguration { get; set; } + public string Comments { get; set; } + public bool IsApproved { get; set; } + public bool IsLockedOut { get; set; } + public DateTime LastLoginDate { get; set; } + public DateTime LastPasswordChangeDate { get; set; } + public DateTime LastLockoutDate { get; set; } + public int FailedPasswordAttempts { get; set; } + public string SecurityStamp { get; set; } + public bool IsDirty() => throw new NotImplementedException(); + + public bool IsPropertyDirty(string propName) => throw new NotImplementedException(); + + public IEnumerable GetDirtyProperties() => throw new NotImplementedException(); + + public void ResetDirtyProperties() => throw new NotImplementedException(); + + public void DisableChangeTracking() => throw new NotImplementedException(); + + public void EnableChangeTracking() => throw new NotImplementedException(); + + public event PropertyChangedEventHandler PropertyChanged; + public bool WasDirty() => throw new NotImplementedException(); + + public bool WasPropertyDirty(string propertyName) => throw new NotImplementedException(); + + public void ResetWereDirtyProperties() => throw new NotImplementedException(); + + public void ResetDirtyProperties(bool rememberDirty) => throw new NotImplementedException(); + + public IEnumerable GetWereDirtyProperties() => throw new NotImplementedException(); + } +} diff --git a/src/Umbraco.Infrastructure/Security/IUmbracoUserManager.cs b/src/Umbraco.Infrastructure/Security/IUmbracoUserManager.cs index 9e44855a4a..587b7cd8c5 100644 --- a/src/Umbraco.Infrastructure/Security/IUmbracoUserManager.cs +++ b/src/Umbraco.Infrastructure/Security/IUmbracoUserManager.cs @@ -263,14 +263,7 @@ namespace Umbraco.Infrastructure.Security /// /// A generated password string GeneratePassword(); - - /// - /// Hashes a password for a null user based on the default password hasher - /// - /// The password to hash - /// The hashed password - string HashPassword(string password); - + /// /// Used to validate the password without an identity user /// Validation code is based on the default ValidatePasswordAsync code diff --git a/src/Umbraco.Infrastructure/Security/UmbracoUserManager.cs b/src/Umbraco.Infrastructure/Security/UmbracoUserManager.cs index 04560fe45b..528c24ed46 100644 --- a/src/Umbraco.Infrastructure/Security/UmbracoUserManager.cs +++ b/src/Umbraco.Infrastructure/Security/UmbracoUserManager.cs @@ -100,19 +100,7 @@ namespace Umbraco.Infrastructure.Security string password = _passwordGenerator.GeneratePassword(); return password; } - - /// - /// Generates a hashed password based on the default password hasher - /// No existing identity user is required and this does not validate the password - /// - /// The password to hash - /// The hashed password - public string HashPassword(string password) - { - string hashedPassword = PasswordHasher.HashPassword(null, password); - return hashedPassword; - } - + /// /// Used to validate the password without an identity user /// Validation code is based on the default ValidatePasswordAsync code diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Controllers/MemberControllerUnitTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Controllers/MemberControllerUnitTests.cs index c69d1365ed..d1c0633472 100644 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Controllers/MemberControllerUnitTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Controllers/MemberControllerUnitTests.cs @@ -20,8 +20,8 @@ using Umbraco.Core.Events; using Umbraco.Core.Mapping; using Umbraco.Core.Models; using Umbraco.Core.Models.ContentEditing; +using Umbraco.Core.Models.Membership; using Umbraco.Core.PropertyEditors; -using Umbraco.Core.PropertyEditors.Validators; using Umbraco.Core.Security; using Umbraco.Core.Serialization; using Umbraco.Core.Services; @@ -33,6 +33,7 @@ using Umbraco.Tests.UnitTests.Umbraco.Core.ShortStringHelper; using Umbraco.Web; using Umbraco.Web.BackOffice.Controllers; using Umbraco.Web.BackOffice.Mapping; +using Umbraco.Web.BackOffice.Security; using Umbraco.Web.Common.ActionsResults; using Umbraco.Web.ContentApps; using Umbraco.Web.Models; @@ -69,11 +70,12 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Web.BackOffice.Controllers IMemberTypeService memberTypeService, IMemberGroupService memberGroupService, IDataTypeService dataTypeService, - IBackOfficeSecurityAccessor backOfficeSecurityAccessor) + IBackOfficeSecurityAccessor backOfficeSecurityAccessor, + IPasswordChanger passwordChanger) { // arrange Member member = SetupMemberTestData(out MemberSave fakeMemberData, out MemberDisplay memberDisplay, ContentSaveAction.SaveNew); - MemberController sut = CreateSut(memberService, memberTypeService, memberGroupService, umbracoMembersUserManager, dataTypeService, backOfficeSecurityAccessor); + MemberController sut = CreateSut(memberService, memberTypeService, memberGroupService, umbracoMembersUserManager, dataTypeService, backOfficeSecurityAccessor, passwordChanger); sut.ModelState.AddModelError("key", "Invalid model state"); Mock.Get(umbracoMembersUserManager) @@ -105,7 +107,8 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Web.BackOffice.Controllers IMemberGroupService memberGroupService, IDataTypeService dataTypeService, IBackOfficeSecurityAccessor backOfficeSecurityAccessor, - IBackOfficeSecurity backOfficeSecurity) + IBackOfficeSecurity backOfficeSecurity, + IPasswordChanger passwordChanger) { // arrange Member member = SetupMemberTestData(out MemberSave fakeMemberData, out MemberDisplay memberDisplay, ContentSaveAction.SaveNew); @@ -123,7 +126,7 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Web.BackOffice.Controllers .Returns(() => member); Mock.Get(memberService).Setup(x => x.GetByUsername(It.IsAny())).Returns(() => member); - MemberController sut = CreateSut(memberService, memberTypeService, memberGroupService, umbracoMembersUserManager, dataTypeService, backOfficeSecurityAccessor); + MemberController sut = CreateSut(memberService, memberTypeService, memberGroupService, umbracoMembersUserManager, dataTypeService, backOfficeSecurityAccessor, passwordChanger); // act ActionResult result = await sut.PostSave(fakeMemberData); @@ -143,7 +146,8 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Web.BackOffice.Controllers IMemberGroupService memberGroupService, IDataTypeService dataTypeService, IBackOfficeSecurityAccessor backOfficeSecurityAccessor, - IBackOfficeSecurity backOfficeSecurity) + IBackOfficeSecurity backOfficeSecurity, + IPasswordChanger passwordChanger) { // arrange Member member = SetupMemberTestData(out MemberSave fakeMemberData, out MemberDisplay memberDisplay, ContentSaveAction.SaveNew); @@ -161,7 +165,7 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Web.BackOffice.Controllers .Returns(() => member); Mock.Get(memberService).Setup(x => x.GetByUsername(It.IsAny())).Returns(() => member); - MemberController sut = CreateSut(memberService, memberTypeService, memberGroupService, umbracoMembersUserManager, dataTypeService, backOfficeSecurityAccessor); + MemberController sut = CreateSut(memberService, memberTypeService, memberGroupService, umbracoMembersUserManager, dataTypeService, backOfficeSecurityAccessor, passwordChanger); // act ActionResult result = await sut.PostSave(fakeMemberData); @@ -182,7 +186,8 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Web.BackOffice.Controllers IMemberGroupService memberGroupService, IDataTypeService dataTypeService, IBackOfficeSecurityAccessor backOfficeSecurityAccessor, - IBackOfficeSecurity backOfficeSecurity) + IBackOfficeSecurity backOfficeSecurity, + IPasswordChanger passwordChanger) { // arrange Member member = SetupMemberTestData(out MemberSave fakeMemberData, out MemberDisplay memberDisplay, ContentSaveAction.Save); @@ -194,9 +199,9 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Web.BackOffice.Controllers .ReturnsAsync(() => IdentityResult.Success); string password = "fakepassword9aw89rnyco3938cyr^%&*()i8Y"; - Mock.Get(umbracoMembersUserManager) - .Setup(x => x.HashPassword(It.IsAny())) - .Returns(password); + Mock.Get(passwordChanger) + .Setup(x => x.ChangePasswordWithIdentityAsync(It.IsAny(), umbracoMembersUserManager)) + .ReturnsAsync(() => new Attempt()); Mock.Get(umbracoMembersUserManager) .Setup(x => x.UpdateAsync(It.IsAny())) .ReturnsAsync(() => IdentityResult.Success); @@ -208,7 +213,7 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Web.BackOffice.Controllers .Returns(() => null) .Returns(() => member); - MemberController sut = CreateSut(memberService, memberTypeService, memberGroupService, umbracoMembersUserManager, dataTypeService, backOfficeSecurityAccessor); + MemberController sut = CreateSut(memberService, memberTypeService, memberGroupService, umbracoMembersUserManager, dataTypeService, backOfficeSecurityAccessor, passwordChanger); // act ActionResult result = await sut.PostSave(fakeMemberData); @@ -228,7 +233,8 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Web.BackOffice.Controllers IMemberGroupService memberGroupService, IDataTypeService dataTypeService, IBackOfficeSecurityAccessor backOfficeSecurityAccessor, - IBackOfficeSecurity backOfficeSecurity) + IBackOfficeSecurity backOfficeSecurity, + IPasswordChanger passwordChanger) { // arrange Member member = SetupMemberTestData(out MemberSave fakeMemberData, out MemberDisplay memberDisplay, ContentSaveAction.SaveNew); @@ -245,7 +251,7 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Web.BackOffice.Controllers x => x.GetByEmail(It.IsAny())) .Returns(() => member); - MemberController sut = CreateSut(memberService, memberTypeService, memberGroupService, umbracoMembersUserManager, dataTypeService, backOfficeSecurityAccessor); + MemberController sut = CreateSut(memberService, memberTypeService, memberGroupService, umbracoMembersUserManager, dataTypeService, backOfficeSecurityAccessor, passwordChanger); string reason = "Validation failed"; // act @@ -267,7 +273,8 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Web.BackOffice.Controllers IMemberGroupService memberGroupService, IDataTypeService dataTypeService, IBackOfficeSecurityAccessor backOfficeSecurityAccessor, - IBackOfficeSecurity backOfficeSecurity) + IBackOfficeSecurity backOfficeSecurity, + IPasswordChanger passwordChanger) { // arrange string password = "fakepassword9aw89rnyco3938cyr^%&*()i8Y"; @@ -277,16 +284,16 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Web.BackOffice.Controllers { roleName }; - var membersIdentityUser = new MembersIdentityUser(); + var membersIdentityUser = new MembersIdentityUser(123); Mock.Get(umbracoMembersUserManager) .Setup(x => x.FindByIdAsync(It.IsAny())) .ReturnsAsync(() => membersIdentityUser); Mock.Get(umbracoMembersUserManager) .Setup(x => x.ValidatePasswordAsync(It.IsAny())) .ReturnsAsync(() => IdentityResult.Success); - Mock.Get(umbracoMembersUserManager) - .Setup(x => x.HashPassword(It.IsAny())) - .Returns(password); + Mock.Get(passwordChanger) + .Setup(x => x.ChangePasswordWithIdentityAsync(It.IsAny(), umbracoMembersUserManager)) + .ReturnsAsync(() => Attempt.Succeed(new PasswordChangedModel())); Mock.Get(umbracoMembersUserManager) .Setup(x => x.UpdateAsync(It.IsAny())) .ReturnsAsync(() => IdentityResult.Success); @@ -299,7 +306,7 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Web.BackOffice.Controllers .Returns(() => null) .Returns(() => member); Mock.Get(memberService).Setup(x => x.GetByUsername(It.IsAny())).Returns(() => member); - MemberController sut = CreateSut(memberService, memberTypeService, memberGroupService, umbracoMembersUserManager, dataTypeService, backOfficeSecurityAccessor); + MemberController sut = CreateSut(memberService, memberTypeService, memberGroupService, umbracoMembersUserManager, dataTypeService, backOfficeSecurityAccessor, passwordChanger); // act ActionResult result = await sut.PostSave(fakeMemberData); @@ -309,8 +316,8 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Web.BackOffice.Controllers Assert.IsNotNull(result.Value); Mock.Get(umbracoMembersUserManager) .Verify(u => u.GetRolesAsync(membersIdentityUser)); - Mock.Get(umbracoMembersUserManager) - .Verify(u => u.AddToRolesAsync(membersIdentityUser, new[] { roleName })); + Mock.Get(umbracoMembersUserManager) + .Verify(u => u.AddToRolesAsync(membersIdentityUser, new[] { roleName })); Mock.Get(memberService) .Verify(m => m.Save(It.IsAny(), true)); AssertMemberDisplayPropertiesAreEqual(memberDisplay, result.Value); @@ -325,17 +332,18 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Web.BackOffice.Controllers /// Members user manager /// Data type service /// Back office security accessor + /// Password changer class /// A member controller for the tests private MemberController CreateSut( IMemberService memberService, IMemberTypeService memberTypeService, IMemberGroupService memberGroupService, - IMemberManager membersUserManager, + IUmbracoUserManager membersUserManager, IDataTypeService dataTypeService, - IBackOfficeSecurityAccessor backOfficeSecurityAccessor) + IBackOfficeSecurityAccessor backOfficeSecurityAccessor, + IPasswordChanger mockPasswordChanger) { var mockShortStringHelper = new MockShortStringHelper(); - var textService = new Mock(); var contentTypeBaseServiceProvider = new Mock(); contentTypeBaseServiceProvider.Setup(x => x.GetContentTypeOf(It.IsAny())).Returns(new ContentType(mockShortStringHelper, 123)); @@ -410,13 +418,13 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Web.BackOffice.Controllers _mapper, memberService, memberTypeService, - membersUserManager, + (IMemberManager) membersUserManager, dataTypeService, backOfficeSecurityAccessor, - new ConfigurationEditorJsonSerializer()); + new ConfigurationEditorJsonSerializer(), + mockPasswordChanger); } - /// /// Setup all standard member data for test /// @@ -428,10 +436,10 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Web.BackOffice.Controllers // arrange MemberType memberType = MemberTypeBuilder.CreateSimpleMemberType(); Member member = MemberBuilder.CreateSimpleMember(memberType, "Test Member", "test@example.com", "123", "test"); - int memberId = 123; + var memberId = 123; member.Id = memberId; - //TODO: replace with builder for MemberSave and MemberDisplay + // TODO: replace with builder for MemberSave and MemberDisplay fakeMemberData = new MemberSave() { Id = memberId, diff --git a/src/Umbraco.Web.BackOffice/Controllers/CurrentUserController.cs b/src/Umbraco.Web.BackOffice/Controllers/CurrentUserController.cs index 70b44db586..3383a7578a 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/CurrentUserController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/CurrentUserController.cs @@ -17,6 +17,7 @@ using Umbraco.Core.IO; using Umbraco.Core.Mapping; using Umbraco.Core.Media; using Umbraco.Core.Models; +using Umbraco.Core.Models.Membership; using Umbraco.Core.Security; using Umbraco.Core.Services; using Umbraco.Core.Strings; @@ -42,7 +43,7 @@ namespace Umbraco.Web.BackOffice.Controllers private readonly ContentSettings _contentSettings; private readonly IHostingEnvironment _hostingEnvironment; private readonly IImageUrlGenerator _imageUrlGenerator; - private readonly IBackOfficeSecurityAccessor _backofficeSecurityAccessor; + private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor; private readonly IUserService _userService; private readonly UmbracoMapper _umbracoMapper; private readonly IBackOfficeUserManager _backOfficeUserManager; @@ -50,6 +51,7 @@ namespace Umbraco.Web.BackOffice.Controllers private readonly ILocalizedTextService _localizedTextService; private readonly AppCaches _appCaches; private readonly IShortStringHelper _shortStringHelper; + private readonly IPasswordChanger _passwordChanger; public CurrentUserController( IMediaFileSystem mediaFileSystem, @@ -63,13 +65,14 @@ namespace Umbraco.Web.BackOffice.Controllers ILoggerFactory loggerFactory, ILocalizedTextService localizedTextService, AppCaches appCaches, - IShortStringHelper shortStringHelper) + IShortStringHelper shortStringHelper, + IPasswordChanger passwordChanger) { _mediaFileSystem = mediaFileSystem; _contentSettings = contentSettings.Value; _hostingEnvironment = hostingEnvironment; _imageUrlGenerator = imageUrlGenerator; - _backofficeSecurityAccessor = backofficeSecurityAccessor; + _backOfficeSecurityAccessor = backofficeSecurityAccessor; _userService = userService; _umbracoMapper = umbracoMapper; _backOfficeUserManager = backOfficeUserManager; @@ -77,6 +80,7 @@ namespace Umbraco.Web.BackOffice.Controllers _localizedTextService = localizedTextService; _appCaches = appCaches; _shortStringHelper = shortStringHelper; + _passwordChanger = passwordChanger; } @@ -89,7 +93,7 @@ namespace Umbraco.Web.BackOffice.Controllers public Dictionary GetPermissions(int[] nodeIds) { var permissions = _userService - .GetPermissions(_backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser, nodeIds); + .GetPermissions(_backOfficeSecurityAccessor.BackOfficeSecurity.CurrentUser, nodeIds); var permissionsDictionary = new Dictionary(); foreach (var nodeId in nodeIds) @@ -110,7 +114,7 @@ namespace Umbraco.Web.BackOffice.Controllers [HttpGet] public bool HasPermission(string permissionToCheck, int nodeId) { - var p = _userService.GetPermissions(_backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser, nodeId).GetAllPermissions(); + var p = _userService.GetPermissions(_backOfficeSecurityAccessor.BackOfficeSecurity.CurrentUser, nodeId).GetAllPermissions(); if (p.Contains(permissionToCheck.ToString(CultureInfo.InvariantCulture))) { return true; @@ -129,15 +133,15 @@ namespace Umbraco.Web.BackOffice.Controllers if (status == null) throw new ArgumentNullException(nameof(status)); List userTours; - if (_backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser.TourData.IsNullOrWhiteSpace()) + if (_backOfficeSecurityAccessor.BackOfficeSecurity.CurrentUser.TourData.IsNullOrWhiteSpace()) { userTours = new List { status }; - _backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser.TourData = JsonConvert.SerializeObject(userTours); - _userService.Save(_backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser); + _backOfficeSecurityAccessor.BackOfficeSecurity.CurrentUser.TourData = JsonConvert.SerializeObject(userTours); + _userService.Save(_backOfficeSecurityAccessor.BackOfficeSecurity.CurrentUser); return userTours; } - userTours = JsonConvert.DeserializeObject>(_backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser.TourData).ToList(); + userTours = JsonConvert.DeserializeObject>(_backOfficeSecurityAccessor.BackOfficeSecurity.CurrentUser.TourData).ToList(); var found = userTours.FirstOrDefault(x => x.Alias == status.Alias); if (found != null) { @@ -145,8 +149,8 @@ namespace Umbraco.Web.BackOffice.Controllers userTours.Remove(found); } userTours.Add(status); - _backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser.TourData = JsonConvert.SerializeObject(userTours); - _userService.Save(_backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser); + _backOfficeSecurityAccessor.BackOfficeSecurity.CurrentUser.TourData = JsonConvert.SerializeObject(userTours); + _userService.Save(_backOfficeSecurityAccessor.BackOfficeSecurity.CurrentUser); return userTours; } @@ -156,10 +160,10 @@ namespace Umbraco.Web.BackOffice.Controllers /// public IEnumerable GetUserTours() { - if (_backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser.TourData.IsNullOrWhiteSpace()) + if (_backOfficeSecurityAccessor.BackOfficeSecurity.CurrentUser.TourData.IsNullOrWhiteSpace()) return Enumerable.Empty(); - var userTours = JsonConvert.DeserializeObject>(_backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser.TourData); + var userTours = JsonConvert.DeserializeObject>(_backOfficeSecurityAccessor.BackOfficeSecurity.CurrentUser.TourData); return userTours; } @@ -175,7 +179,7 @@ namespace Umbraco.Web.BackOffice.Controllers [AllowAnonymous] public async Task> PostSetInvitedUserPassword([FromBody]string newPassword) { - var user = await _backOfficeUserManager.FindByIdAsync(_backofficeSecurityAccessor.BackOfficeSecurity.GetUserId().ResultOr(0).ToString()); + var user = await _backOfficeUserManager.FindByIdAsync(_backOfficeSecurityAccessor.BackOfficeSecurity.GetUserId().ResultOr(0).ToString()); if (user == null) throw new InvalidOperationException("Could not find user"); var result = await _backOfficeUserManager.AddPasswordAsync(user, newPassword); @@ -190,13 +194,13 @@ namespace Umbraco.Web.BackOffice.Controllers } //They've successfully set their password, we can now update their user account to be approved - _backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser.IsApproved = true; + _backOfficeSecurityAccessor.BackOfficeSecurity.CurrentUser.IsApproved = true; //They've successfully set their password, and will now get fully logged into the back office, so the lastlogindate is set so the backoffice shows they have logged in - _backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser.LastLoginDate = DateTime.UtcNow; - _userService.Save(_backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser); + _backOfficeSecurityAccessor.BackOfficeSecurity.CurrentUser.LastLoginDate = DateTime.UtcNow; + _userService.Save(_backOfficeSecurityAccessor.BackOfficeSecurity.CurrentUser); //now we can return their full object since they are now really logged into the back office - var userDisplay = _umbracoMapper.Map(_backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser); + var userDisplay = _umbracoMapper.Map(_backOfficeSecurityAccessor.BackOfficeSecurity.CurrentUser); userDisplay.SecondsUntilTimeout = HttpContext.User.GetRemainingAuthSeconds(); return userDisplay; @@ -206,31 +210,38 @@ namespace Umbraco.Web.BackOffice.Controllers public IActionResult PostSetAvatar(IList file) { //borrow the logic from the user controller - return UsersController.PostSetAvatarInternal(file, _userService, _appCaches.RuntimeCache, _mediaFileSystem, _shortStringHelper, _contentSettings, _hostingEnvironment, _imageUrlGenerator, _backofficeSecurityAccessor.BackOfficeSecurity.GetUserId().ResultOr(0)); + return UsersController.PostSetAvatarInternal(file, _userService, _appCaches.RuntimeCache, _mediaFileSystem, _shortStringHelper, _contentSettings, _hostingEnvironment, _imageUrlGenerator, _backOfficeSecurityAccessor.BackOfficeSecurity.GetUserId().ResultOr(0)); } /// /// Changes the users password /// - /// + /// The changing password model /// /// If the password is being reset it will return the newly reset password, otherwise will return an empty value /// - public async Task>> PostChangePassword(ChangingPasswordModel data) + public async Task>> PostChangePassword(ChangingPasswordModel changingPasswordModel) { - // TODO: Why don't we inject this? Then we can just inject a logger - var passwordChanger = new PasswordChanger(_loggerFactory.CreateLogger()); - var passwordChangeResult = await passwordChanger.ChangePasswordWithIdentityAsync(_backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser, _backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser, data, _backOfficeUserManager); + IUser currentUser = _backOfficeSecurityAccessor.BackOfficeSecurity.CurrentUser; + changingPasswordModel.CurrentUserHasSectionAccess = currentUser.HasSectionAccess(Constants.Applications.Users); + + // the current user has access to change their password + changingPasswordModel.CurrentUserHasSectionAccess = true; + changingPasswordModel.CurrentUsername = currentUser.Username; + changingPasswordModel.SavingUsername = currentUser.Username; + changingPasswordModel.SavingUserId = currentUser.Id; + + Attempt passwordChangeResult = await _passwordChanger.ChangePasswordWithIdentityAsync(changingPasswordModel, _backOfficeUserManager); if (passwordChangeResult.Success) { - //even if we weren't resetting this, it is the correct value (null), otherwise if we were resetting then it will contain the new pword + // even if we weren't resetting this, it is the correct value (null), otherwise if we were resetting then it will contain the new pword var result = new ModelWithNotifications(passwordChangeResult.Result.ResetPassword); result.AddSuccessNotification(_localizedTextService.Localize("user/password"), _localizedTextService.Localize("user/passwordChanged")); return result; } - foreach (var memberName in passwordChangeResult.Result.ChangeError.MemberNames) + foreach (string memberName in passwordChangeResult.Result.ChangeError.MemberNames) { ModelState.AddModelError(memberName, passwordChangeResult.Result.ChangeError.ErrorMessage); } @@ -243,7 +254,7 @@ namespace Umbraco.Web.BackOffice.Controllers [ValidateAngularAntiForgeryToken] public async Task> GetCurrentUserLinkedLogins() { - var identityUser = await _backOfficeUserManager.FindByIdAsync(_backofficeSecurityAccessor.BackOfficeSecurity.GetUserId().ResultOr(0).ToString()); + var identityUser = await _backOfficeUserManager.FindByIdAsync(_backOfficeSecurityAccessor.BackOfficeSecurity.GetUserId().ResultOr(0).ToString()); // deduplicate in case there are duplicates (there shouldn't be now since we have a unique constraint on the external logins // but there didn't used to be) diff --git a/src/Umbraco.Web.BackOffice/Controllers/MemberController.cs b/src/Umbraco.Web.BackOffice/Controllers/MemberController.cs index cf5681d578..8831f7fe00 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/MemberController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/MemberController.cs @@ -28,11 +28,13 @@ using Umbraco.Infrastructure.Security; using Umbraco.Infrastructure.Services.Implement; using Umbraco.Web.BackOffice.Filters; using Umbraco.Web.BackOffice.ModelBinders; +using Umbraco.Web.BackOffice.Security; using Umbraco.Web.Common.ActionsResults; using Umbraco.Web.Common.Attributes; using Umbraco.Web.Common.Authorization; using Umbraco.Web.Common.Filters; using Umbraco.Web.ContentApps; +using Umbraco.Web.Models; using Umbraco.Web.Models.ContentEditing; namespace Umbraco.Web.BackOffice.Controllers @@ -56,6 +58,7 @@ namespace Umbraco.Web.BackOffice.Controllers private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor; private readonly IJsonSerializer _jsonSerializer; private readonly IShortStringHelper _shortStringHelper; + private readonly IPasswordChanger _passwordChanger; /// /// Initializes a new instance of the class. @@ -73,6 +76,7 @@ namespace Umbraco.Web.BackOffice.Controllers /// The data-type service /// The back office security accessor /// The JSON serializer + /// The password changer public MemberController( ICultureDictionary cultureDictionary, ILoggerFactory loggerFactory, @@ -86,7 +90,8 @@ namespace Umbraco.Web.BackOffice.Controllers IMemberManager memberManager, IDataTypeService dataTypeService, IBackOfficeSecurityAccessor backOfficeSecurityAccessor, - IJsonSerializer jsonSerializer) + IJsonSerializer jsonSerializer, + IPasswordChanger passwordChanger) : base(cultureDictionary, loggerFactory, shortStringHelper, eventMessages, localizedTextService, jsonSerializer) { _propertyEditors = propertyEditors; @@ -99,6 +104,7 @@ namespace Umbraco.Web.BackOffice.Controllers _backOfficeSecurityAccessor = backOfficeSecurityAccessor; _jsonSerializer = jsonSerializer; _shortStringHelper = shortStringHelper; + _passwordChanger = passwordChanger; } /// @@ -390,7 +396,7 @@ namespace Umbraco.Web.BackOffice.Controllers } //TODO: do we need to resave the key? - //contentItem.PersistedContent.Key = contentItem.Key; + // contentItem.PersistedContent.Key = contentItem.Key; // now the member has been saved via identity, resave the member with mapped content properties _memberService.Save(member); @@ -450,7 +456,7 @@ namespace Umbraco.Web.BackOffice.Controllers MembersIdentityUser identityMember = await _memberManager.FindByIdAsync(contentItem.Id.ToString()); if (identityMember == null) { - return new ValidationErrorResult("Member was not found"); + return new ValidationErrorResult("Identity member was not found"); } if (contentItem.Password != null) @@ -461,14 +467,52 @@ namespace Umbraco.Web.BackOffice.Controllers return new ValidationErrorResult(validatePassword.Errors.ToErrorMessage()); } - string newPassword = _memberManager.HashPassword(contentItem.Password.NewPassword); - identityMember.PasswordHash = newPassword; - contentItem.PersistedContent.RawPasswordValue = identityMember.PasswordHash; - if (identityMember.LastPasswordChangeDateUtc != null) + Attempt intId = identityMember.Id.TryConvertTo(); + if (intId.Success == false) { - contentItem.PersistedContent.LastPasswordChangeDate = DateTime.UtcNow; + return new ValidationErrorResult("Member ID was not valid"); + } + + IMember foundMember = _memberService.GetById(intId.Result); + if (foundMember == null) + { + return new ValidationErrorResult("Member was not found"); + } + + IUser currentUser = _backOfficeSecurityAccessor.BackOfficeSecurity.CurrentUser; + var changingPasswordModel = new ChangingPasswordModel + { + Id = intId.Result, + OldPassword = contentItem.Password.OldPassword, + NewPassword = contentItem.Password.NewPassword, + CurrentUsername = currentUser.Username, + SavingUserId = foundMember.Id, + SavingUsername = foundMember.Username, + CurrentUserHasSectionAccess = currentUser.HasSectionAccess(Constants.Applications.Members) + }; + + Attempt passwordChangeResult = await _passwordChanger.ChangePasswordWithIdentityAsync(changingPasswordModel, _memberManager); + + if (passwordChangeResult.Success) + { + contentItem.PersistedContent.RawPasswordValue = passwordChangeResult.Result.ResetPassword; + if (identityMember.LastPasswordChangeDateUtc != null) + { + contentItem.PersistedContent.LastPasswordChangeDate = DateTime.UtcNow; + } + identityMember.LastPasswordChangeDateUtc = contentItem.PersistedContent.LastPasswordChangeDate; } + + if (passwordChangeResult.Result.ChangeError?.MemberNames != null) + { + foreach (string memberName in passwordChangeResult.Result.ChangeError?.MemberNames) + { + ModelState.AddModelError(memberName, passwordChangeResult.Result.ChangeError?.ErrorMessage ?? string.Empty); + } + } + + return new ValidationErrorResult(new SimpleValidationModel(ModelState.ToErrorDictionary())); } IdentityResult updatedResult = await _memberManager.UpdateAsync(identityMember); diff --git a/src/Umbraco.Web.BackOffice/Controllers/UsersController.cs b/src/Umbraco.Web.BackOffice/Controllers/UsersController.cs index d8e4b83ac9..93d1f236a6 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/UsersController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/UsersController.cs @@ -23,6 +23,7 @@ using Umbraco.Core.Mail; using Umbraco.Core.Mapping; using Umbraco.Core.Media; using Umbraco.Core.Models; +using Umbraco.Core.Models.Identity; using Umbraco.Core.Models.Membership; using Umbraco.Core.Persistence; using Umbraco.Core.Security; @@ -56,7 +57,7 @@ namespace Umbraco.Web.BackOffice.Controllers private readonly IImageUrlGenerator _imageUrlGenerator; private readonly SecuritySettings _securitySettings; private readonly IEmailSender _emailSender; - private readonly IBackOfficeSecurityAccessor _backofficeSecurityAccessor; + private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor; private readonly AppCaches _appCaches; private readonly IShortStringHelper _shortStringHelper; private readonly IUserService _userService; @@ -68,6 +69,7 @@ namespace Umbraco.Web.BackOffice.Controllers private readonly LinkGenerator _linkGenerator; private readonly IBackOfficeExternalLoginProviders _externalLogins; private readonly UserEditorAuthorizationHelper _userEditorAuthorizationHelper; + private readonly IPasswordChanger _passwordChanger; private readonly ILogger _logger; public UsersController( @@ -89,7 +91,8 @@ namespace Umbraco.Web.BackOffice.Controllers ILoggerFactory loggerFactory, LinkGenerator linkGenerator, IBackOfficeExternalLoginProviders externalLogins, - UserEditorAuthorizationHelper userEditorAuthorizationHelper) + UserEditorAuthorizationHelper userEditorAuthorizationHelper, + IPasswordChanger passwordChanger) { _mediaFileSystem = mediaFileSystem; _contentSettings = contentSettings.Value; @@ -98,7 +101,7 @@ namespace Umbraco.Web.BackOffice.Controllers _imageUrlGenerator = imageUrlGenerator; _securitySettings = securitySettings.Value; _emailSender = emailSender; - _backofficeSecurityAccessor = backofficeSecurityAccessor; + _backOfficeSecurityAccessor = backofficeSecurityAccessor; _appCaches = appCaches; _shortStringHelper = shortStringHelper; _userService = userService; @@ -110,6 +113,7 @@ namespace Umbraco.Web.BackOffice.Controllers _linkGenerator = linkGenerator; _externalLogins = externalLogins; _userEditorAuthorizationHelper = userEditorAuthorizationHelper; + _passwordChanger = passwordChanger; _logger = _loggerFactory.CreateLogger(); } @@ -119,7 +123,7 @@ namespace Umbraco.Web.BackOffice.Controllers /// public ActionResult GetCurrentUserAvatarUrls() { - var urls = _backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser.GetUserAvatarUrls(_appCaches.RuntimeCache, _mediaFileSystem, _imageUrlGenerator); + var urls = _backOfficeSecurityAccessor.BackOfficeSecurity.CurrentUser.GetUserAvatarUrls(_appCaches.RuntimeCache, _mediaFileSystem, _imageUrlGenerator); if (urls == null) return new ValidationErrorResult("Could not access Gravatar endpoint"); @@ -285,7 +289,7 @@ namespace Umbraco.Web.BackOffice.Controllers var hideDisabledUsers = _securitySettings.HideDisabledUsersInBackOffice; var excludeUserGroups = new string[0]; - var isAdmin = _backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser.IsAdmin(); + var isAdmin = _backOfficeSecurityAccessor.BackOfficeSecurity.CurrentUser.IsAdmin(); if (isAdmin == false) { //this user is not an admin so in that case we need to exclude all admin users @@ -294,7 +298,7 @@ namespace Umbraco.Web.BackOffice.Controllers var filterQuery = _sqlContext.Query(); - if (!_backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser.IsSuper()) + if (!_backOfficeSecurityAccessor.BackOfficeSecurity.CurrentUser.IsSuper()) { // only super can see super - but don't use IsSuper, cannot be mapped to SQL //filterQuery.Where(x => !x.IsSuper()); @@ -359,7 +363,7 @@ namespace Umbraco.Web.BackOffice.Controllers } //Perform authorization here to see if the current user can actually save this user with the info being requested - var canSaveUser = _userEditorAuthorizationHelper.IsAuthorized(_backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser, null, null, null, userSave.UserGroups); + var canSaveUser = _userEditorAuthorizationHelper.IsAuthorized(_backOfficeSecurityAccessor.BackOfficeSecurity.CurrentUser, null, null, null, userSave.UserGroups); if (canSaveUser == false) { return Unauthorized(canSaveUser.Result); @@ -448,7 +452,7 @@ namespace Umbraco.Web.BackOffice.Controllers } //Perform authorization here to see if the current user can actually save this user with the info being requested - var canSaveUser = _userEditorAuthorizationHelper.IsAuthorized(_backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser, user, null, null, userSave.UserGroups); + var canSaveUser = _userEditorAuthorizationHelper.IsAuthorized(_backOfficeSecurityAccessor.BackOfficeSecurity.CurrentUser, user, null, null, userSave.UserGroups); if (canSaveUser == false) { return new ValidationErrorResult(canSaveUser.Result, StatusCodes.Status401Unauthorized); @@ -511,7 +515,7 @@ namespace Umbraco.Web.BackOffice.Controllers { //send the email - await SendUserInviteEmailAsync(display, _backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser.Name, _backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser.Email, user, userSave.Message); + await SendUserInviteEmailAsync(display, _backOfficeSecurityAccessor.BackOfficeSecurity.CurrentUser.Name, _backOfficeSecurityAccessor.BackOfficeSecurity.CurrentUser.Email, user, userSave.Message); } @@ -605,7 +609,7 @@ namespace Umbraco.Web.BackOffice.Controllers return NotFound(); //Perform authorization here to see if the current user can actually save this user with the info being requested - var canSaveUser = _userEditorAuthorizationHelper.IsAuthorized(_backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser, found, userSave.StartContentIds, userSave.StartMediaIds, userSave.UserGroups); + var canSaveUser = _userEditorAuthorizationHelper.IsAuthorized(_backOfficeSecurityAccessor.BackOfficeSecurity.CurrentUser, found, userSave.StartContentIds, userSave.StartMediaIds, userSave.UserGroups); if (canSaveUser == false) { return Unauthorized(canSaveUser.Result); @@ -665,7 +669,7 @@ namespace Umbraco.Web.BackOffice.Controllers var display = _umbracoMapper.Map(user); // determine if the user has changed their own language; - var currentUser = _backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser; + var currentUser = _backOfficeSecurityAccessor.BackOfficeSecurity.CurrentUser; var userHasChangedOwnLanguage = user.Id == currentUser.Id && currentUser.Language != user.Language; @@ -691,21 +695,25 @@ namespace Umbraco.Web.BackOffice.Controllers return new ValidationErrorResult(new SimpleValidationModel(ModelState.ToErrorDictionary())); } - var intId = changingPasswordModel.Id.TryConvertTo(); + Attempt intId = changingPasswordModel.Id.TryConvertTo(); if (intId.Success == false) { return NotFound(); } - var found = _userService.GetUserById(intId.Result); + IUser found = _userService.GetUserById(intId.Result); if (found == null) { return NotFound(); } - // TODO: Why don't we inject this? Then we can just inject a logger - var passwordChanger = new PasswordChanger(_loggerFactory.CreateLogger()); - var passwordChangeResult = await passwordChanger.ChangePasswordWithIdentityAsync(_backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser, found, changingPasswordModel, _userManager); + IUser currentUser = _backOfficeSecurityAccessor.BackOfficeSecurity.CurrentUser; + changingPasswordModel.CurrentUserHasSectionAccess = currentUser.HasSectionAccess(Constants.Applications.Users); + changingPasswordModel.CurrentUsername = currentUser.Username; + changingPasswordModel.SavingUserId = found.Id; + changingPasswordModel.SavingUsername = found.Username; + + Attempt passwordChangeResult = await _passwordChanger.ChangePasswordWithIdentityAsync(changingPasswordModel, _userManager); if (passwordChangeResult.Success) { @@ -714,7 +722,7 @@ namespace Umbraco.Web.BackOffice.Controllers return result; } - foreach (var memberName in passwordChangeResult.Result.ChangeError.MemberNames) + foreach (string memberName in passwordChangeResult.Result.ChangeError.MemberNames) { ModelState.AddModelError(memberName, passwordChangeResult.Result.ChangeError.ErrorMessage); } @@ -730,7 +738,7 @@ namespace Umbraco.Web.BackOffice.Controllers [Authorize(Policy = AuthorizationPolicies.AdminUserEditsRequireAdmin)] public IActionResult PostDisableUsers([FromQuery]int[] userIds) { - var tryGetCurrentUserId = _backofficeSecurityAccessor.BackOfficeSecurity.GetUserId(); + var tryGetCurrentUserId = _backOfficeSecurityAccessor.BackOfficeSecurity.GetUserId(); if (tryGetCurrentUserId && userIds.Contains(tryGetCurrentUserId.Result)) { return ValidationErrorResult.CreateNotificationValidationErrorResult("The current user cannot disable itself"); diff --git a/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilderExtensions.cs b/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilderExtensions.cs index 32fb491695..fc0edbb2cb 100644 --- a/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilderExtensions.cs +++ b/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilderExtensions.cs @@ -6,6 +6,7 @@ using Microsoft.Extensions.Logging; using Umbraco.Core.DependencyInjection; using Umbraco.Core.Hosting; using Umbraco.Core.IO; +using Umbraco.Core.Models.Identity; using Umbraco.Core.Services; using Umbraco.Extensions; using Umbraco.Infrastructure.DependencyInjection; @@ -81,6 +82,7 @@ namespace Umbraco.Web.BackOffice.DependencyInjection builder.Services.AddUnique(); builder.Services.AddUnique(); builder.Services.AddUnique(); + builder.Services.AddUnique, PasswordChanger>(); return builder; } diff --git a/src/Umbraco.Web.BackOffice/Security/IPasswordChanger.cs b/src/Umbraco.Web.BackOffice/Security/IPasswordChanger.cs new file mode 100644 index 0000000000..7292fde344 --- /dev/null +++ b/src/Umbraco.Web.BackOffice/Security/IPasswordChanger.cs @@ -0,0 +1,16 @@ +using System.Threading.Tasks; +using Umbraco.Core; +using Umbraco.Core.Models; +using Umbraco.Core.Models.Identity; +using Umbraco.Infrastructure.Security; +using Umbraco.Web.Models; + +namespace Umbraco.Web.BackOffice.Security +{ + public interface IPasswordChanger where TUser : UmbracoIdentityUser + { + public Task> ChangePasswordWithIdentityAsync( + ChangingPasswordModel passwordModel, + IUmbracoUserManager userMgr); + } +} diff --git a/src/Umbraco.Web.BackOffice/Security/PasswordChanger.cs b/src/Umbraco.Web.BackOffice/Security/PasswordChanger.cs index 6144e7b63f..e9df385a18 100644 --- a/src/Umbraco.Web.BackOffice/Security/PasswordChanger.cs +++ b/src/Umbraco.Web.BackOffice/Security/PasswordChanger.cs @@ -1,22 +1,31 @@ using System; using System.ComponentModel.DataAnnotations; using System.Threading.Tasks; +using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.Logging; using Umbraco.Core; using Umbraco.Core.Models; -using Umbraco.Core.Security; -using Umbraco.Extensions; +using Umbraco.Core.Models.Identity; +using Umbraco.Core.Models.Membership; using Umbraco.Infrastructure.Security; using Umbraco.Web.Models; -using IUser = Umbraco.Core.Models.Membership.IUser; namespace Umbraco.Web.BackOffice.Security { - internal class PasswordChanger + /// + /// Changes the password for an identity user + /// + internal class PasswordChanger : IPasswordChanger + where TUser : UmbracoIdentityUser { - private readonly ILogger _logger; + private readonly ILogger> _logger; - public PasswordChanger(ILogger logger) + /// + /// Initializes a new instance of the class. + /// Password changing functionality + /// + /// Logger for this class + public PasswordChanger(ILogger> logger) { _logger = logger; } @@ -24,55 +33,60 @@ namespace Umbraco.Web.BackOffice.Security /// /// Changes the password for a user based on the many different rules and config options /// - /// The user performing the password save action - /// The user who's password is being changed - /// - /// - /// + /// The changing password model + /// The identity manager to use to update the password + /// Create an adapter to pass through everything - adapting the member into a user for this functionality + /// The outcome of the password changed model public async Task> ChangePasswordWithIdentityAsync( - IUser currentUser, - IUser savingUser, - ChangingPasswordModel passwordModel, - IBackOfficeUserManager userMgr) + ChangingPasswordModel changingPasswordModel, + IUmbracoUserManager userMgr) { - if (passwordModel == null) throw new ArgumentNullException(nameof(passwordModel)); - if (userMgr == null) throw new ArgumentNullException(nameof(userMgr)); + if (changingPasswordModel == null) + { + throw new ArgumentNullException(nameof(changingPasswordModel)); + } - if (passwordModel.NewPassword.IsNullOrWhiteSpace()) + if (userMgr == null) + { + throw new ArgumentNullException(nameof(userMgr)); + } + + if (changingPasswordModel.NewPassword.IsNullOrWhiteSpace()) { return Attempt.Fail(new PasswordChangedModel { ChangeError = new ValidationResult("Cannot set an empty password", new[] { "value" }) }); } - var backOfficeIdentityUser = await userMgr.FindByIdAsync(savingUser.Id.ToString()); - if (backOfficeIdentityUser == null) + TUser identityUser = await userMgr.FindByIdAsync(changingPasswordModel.SavingUserId.ToString()); + if (identityUser == null) { - //this really shouldn't ever happen... but just in case + // this really shouldn't ever happen... but just in case return Attempt.Fail(new PasswordChangedModel { ChangeError = new ValidationResult("Password could not be verified", new[] { "oldPassword" }) }); } - //Are we just changing another user's password? - if (passwordModel.OldPassword.IsNullOrWhiteSpace()) + // Are we just changing another user's password? + if (changingPasswordModel.OldPassword.IsNullOrWhiteSpace()) { - //if it's the current user, the current user cannot reset their own password - if (currentUser.Username == savingUser.Username) + // if it's the current user, the current user cannot reset their own password + // For members, this should not happen + if (changingPasswordModel.CurrentUsername == changingPasswordModel.SavingUsername) { return Attempt.Fail(new PasswordChangedModel { ChangeError = new ValidationResult("Password reset is not allowed", new[] { "value" }) }); } - //if the current user has access to reset/manually change the password - if (currentUser.HasSectionAccess(Umbraco.Core.Constants.Applications.Users) == false) + // if the current user has access to reset/manually change the password + if (changingPasswordModel.CurrentUserHasSectionAccess) { return Attempt.Fail(new PasswordChangedModel { ChangeError = new ValidationResult("The current user is not authorized", new[] { "value" }) }); } - //ok, we should be able to reset it - var resetToken = await userMgr.GeneratePasswordResetTokenAsync(backOfficeIdentityUser); + // ok, we should be able to reset it + string resetToken = await userMgr.GeneratePasswordResetTokenAsync(identityUser); - var resetResult = await userMgr.ChangePasswordWithResetAsync(savingUser.Id.ToString(), resetToken, passwordModel.NewPassword); + IdentityResult resetResult = await userMgr.ChangePasswordWithResetAsync(changingPasswordModel.SavingUserId.ToString(), resetToken, changingPasswordModel.NewPassword); if (resetResult.Succeeded == false) { - var errors = resetResult.Errors.ToErrorMessage(); + string errors = resetResult.Errors.ToErrorMessage(); _logger.LogWarning("Could not reset user password {PasswordErrors}", errors); return Attempt.Fail(new PasswordChangedModel { ChangeError = new ValidationResult(errors, new[] { "value" }) }); } @@ -80,24 +94,25 @@ namespace Umbraco.Web.BackOffice.Security return Attempt.Succeed(new PasswordChangedModel()); } - //is the old password correct? - var validateResult = await userMgr.CheckPasswordAsync(backOfficeIdentityUser, passwordModel.OldPassword); + // is the old password correct? + bool validateResult = await userMgr.CheckPasswordAsync(identityUser, changingPasswordModel.OldPassword); if (validateResult == false) { - //no, fail with an error message for "oldPassword" + // no, fail with an error message for "oldPassword" return Attempt.Fail(new PasswordChangedModel { ChangeError = new ValidationResult("Incorrect password", new[] { "oldPassword" }) }); } - //can we change to the new password? - var changeResult = await userMgr.ChangePasswordAsync(backOfficeIdentityUser, passwordModel.OldPassword, passwordModel.NewPassword); + + // can we change to the new password? + IdentityResult changeResult = await userMgr.ChangePasswordAsync(identityUser, changingPasswordModel.OldPassword, changingPasswordModel.NewPassword); if (changeResult.Succeeded == false) { - //no, fail with error messages for "password" - var errors = changeResult.Errors.ToErrorMessage(); + // no, fail with error messages for "password" + string errors = changeResult.Errors.ToErrorMessage(); _logger.LogWarning("Could not change user password {PasswordErrors}", errors); return Attempt.Fail(new PasswordChangedModel { ChangeError = new ValidationResult(errors, new[] { "password" }) }); } + return Attempt.Succeed(new PasswordChangedModel()); } - } } From f62b7cbf4aaaa6803f35207bbb6eccb525a432a5 Mon Sep 17 00:00:00 2001 From: emmagarland Date: Sat, 20 Feb 2021 19:56:35 +0000 Subject: [PATCH 002/188] Merged from dev --- .../Services/Importing/ImportResources.Designer.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/Importing/ImportResources.Designer.cs b/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/Importing/ImportResources.Designer.cs index f7e0541d95..b7d74985ad 100644 --- a/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/Importing/ImportResources.Designer.cs +++ b/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/Importing/ImportResources.Designer.cs @@ -8,7 +8,7 @@ // //------------------------------------------------------------------------------ -namespace Umbraco.Tests.Services.Importing { +namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services.Importing { using System; @@ -19,7 +19,7 @@ namespace Umbraco.Tests.Services.Importing { // class via a tool like ResGen or Visual Studio. // To add or remove a member, edit your .ResX file then rerun ResGen // with the /str option, or rebuild your VS project. - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")] [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] internal class ImportResources { @@ -39,8 +39,8 @@ namespace Umbraco.Tests.Services.Importing { internal static global::System.Resources.ResourceManager ResourceManager { get { if (object.ReferenceEquals(resourceMan, null)) { - global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Umbraco.Tests.Integration.Umbraco.Infrastructure.Services.Importing.ImportResourc" + - "es", typeof(ImportResources).Assembly); + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services.Importing.ImportRes" + + "ources", typeof(ImportResources).Assembly); resourceMan = temp; } return resourceMan; From c36aaabd0ebe02a7a8fb3650b763a9eedb890810 Mon Sep 17 00:00:00 2001 From: Emma Garland Date: Fri, 26 Feb 2021 12:42:18 +0000 Subject: [PATCH 003/188] Updated to move the logic for whether the password change can occur, into the controller, --- .../Models/ChangingPasswordModel.cs | 20 ----------------- .../Controllers/CurrentUserController.cs | 11 +++++----- .../Controllers/MemberController.cs | 11 +++++----- .../Controllers/UsersController.cs | 14 ++++++++++-- .../Security/PasswordChanger.cs | 22 ++++--------------- 5 files changed, 27 insertions(+), 51 deletions(-) diff --git a/src/Umbraco.Core/Models/ChangingPasswordModel.cs b/src/Umbraco.Core/Models/ChangingPasswordModel.cs index 37dad33eb3..5df6c42c1e 100644 --- a/src/Umbraco.Core/Models/ChangingPasswordModel.cs +++ b/src/Umbraco.Core/Models/ChangingPasswordModel.cs @@ -25,25 +25,5 @@ namespace Umbraco.Cms.Core.Models /// [DataMember(Name = "id")] public int Id { get; set; } - - /// - /// The username of the user/member who is changing the password - /// - public string CurrentUsername { get; set; } - - /// - /// The ID of the user/member whose password is being changed - /// - public int SavingUserId { get; set; } - - /// - /// The username of the user/memeber whose password is being changed - /// - public string SavingUsername { get; set; } - - /// - /// True if the current user has access to change the password for the member/user - /// - public bool CurrentUserHasSectionAccess { get; set; } } } diff --git a/src/Umbraco.Web.BackOffice/Controllers/CurrentUserController.cs b/src/Umbraco.Web.BackOffice/Controllers/CurrentUserController.cs index 77492caccf..c0615e37a1 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/CurrentUserController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/CurrentUserController.cs @@ -223,13 +223,12 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers public async Task>> PostChangePassword(ChangingPasswordModel changingPasswordModel) { IUser currentUser = _backOfficeSecurityAccessor.BackOfficeSecurity.CurrentUser; - changingPasswordModel.CurrentUserHasSectionAccess = currentUser.HasSectionAccess(Constants.Applications.Users); - // the current user has access to change their password - changingPasswordModel.CurrentUserHasSectionAccess = true; - changingPasswordModel.CurrentUsername = currentUser.Username; - changingPasswordModel.SavingUsername = currentUser.Username; - changingPasswordModel.SavingUserId = currentUser.Id; + // if the current user has access to reset/manually change the password + if (currentUser.HasSectionAccess(Constants.Applications.Users) == false) + { + return new ValidationErrorResult("The current user is not authorized"); + } Attempt passwordChangeResult = await _passwordChanger.ChangePasswordWithIdentityAsync(changingPasswordModel, _backOfficeUserManager); diff --git a/src/Umbraco.Web.BackOffice/Controllers/MemberController.cs b/src/Umbraco.Web.BackOffice/Controllers/MemberController.cs index d9b2cca270..b5f81ca3f4 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/MemberController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/MemberController.cs @@ -471,16 +471,17 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers } IUser currentUser = _backOfficeSecurityAccessor.BackOfficeSecurity.CurrentUser; + // if the current user has access to reset/manually change the password + if (currentUser.HasSectionAccess(Constants.Applications.Members) == false) + { + return new ValidationErrorResult("The current user is not authorized"); + } var changingPasswordModel = new ChangingPasswordModel { Id = intId.Result, OldPassword = contentItem.Password.OldPassword, NewPassword = contentItem.Password.NewPassword, - CurrentUsername = currentUser.Username, - SavingUserId = foundMember.Id, - SavingUsername = foundMember.Username, - CurrentUserHasSectionAccess = currentUser.HasSectionAccess(Constants.Applications.Members) - }; + }; Attempt passwordChangeResult = await _passwordChanger.ChangePasswordWithIdentityAsync(changingPasswordModel, _memberManager); diff --git a/src/Umbraco.Web.BackOffice/Controllers/UsersController.cs b/src/Umbraco.Web.BackOffice/Controllers/UsersController.cs index 61103a692d..7ad12ecd65 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/UsersController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/UsersController.cs @@ -708,8 +708,18 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers } IUser currentUser = _backOfficeSecurityAccessor.BackOfficeSecurity.CurrentUser; - changingPasswordModel.CurrentUserHasSectionAccess = currentUser.HasSectionAccess(Constants.Applications.Users); - changingPasswordModel.CurrentUsername = currentUser.Username; + + // if it's the current user, the current user cannot reset their own password + if (currentUser.Username == found.Username) + { + return new ValidationErrorResult("Password reset is not allowed"); + } + + // if the current user has access to reset/manually change the password + if (currentUser.HasSectionAccess(Constants.Applications.Users) == false) + { + return new ValidationErrorResult("The current user is not authorized"); + } Attempt passwordChangeResult = await _passwordChanger.ChangePasswordWithIdentityAsync(changingPasswordModel, _userManager); diff --git a/src/Umbraco.Web.BackOffice/Security/PasswordChanger.cs b/src/Umbraco.Web.BackOffice/Security/PasswordChanger.cs index 90785b9a81..99e8a98a32 100644 --- a/src/Umbraco.Web.BackOffice/Security/PasswordChanger.cs +++ b/src/Umbraco.Web.BackOffice/Security/PasswordChanger.cs @@ -8,8 +8,6 @@ using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.Identity; using Umbraco.Cms.Core.Security; using Umbraco.Extensions; -using Constants = Umbraco.Cms.Core.Constants; -using IUser = Umbraco.Cms.Core.Models.Membership.IUser; namespace Umbraco.Cms.Web.BackOffice.Security { @@ -56,33 +54,21 @@ namespace Umbraco.Cms.Web.BackOffice.Security return Attempt.Fail(new PasswordChangedModel { ChangeError = new ValidationResult("Cannot set an empty password", new[] { "value" }) }); } - TUser identityUser = await userMgr.FindByIdAsync(changingPasswordModel.SavingUserId.ToString()); + var userId = changingPasswordModel.Id.ToString(); + TUser identityUser = await userMgr.FindByIdAsync(userId); if (identityUser == null) { // this really shouldn't ever happen... but just in case return Attempt.Fail(new PasswordChangedModel { ChangeError = new ValidationResult("Password could not be verified", new[] { "oldPassword" }) }); } - // Are we just changing another user's password? + // Are we just changing another user/member's password? if (changingPasswordModel.OldPassword.IsNullOrWhiteSpace()) { - //// if it's the current user, the current user cannot reset their own password - //// For members, this should not happen - //if (changingPasswordModel.CurrentUsername == changingPasswordModel.SavingUsername) - //{ - // return Attempt.Fail(new PasswordChangedModel { ChangeError = new ValidationResult("Password reset is not allowed", new[] { "value" }) }); - //} - - //// if the current user has access to reset/manually change the password - //if (currentUser.HasSectionAccess(Constants.Applications.Users) == false) - //{ - // return Attempt.Fail(new PasswordChangedModel { ChangeError = new ValidationResult("The current user is not authorized", new[] { "value" }) }); - //} - // ok, we should be able to reset it string resetToken = await userMgr.GeneratePasswordResetTokenAsync(identityUser); - IdentityResult resetResult = await userMgr.ChangePasswordWithResetAsync(changingPasswordModel.SavingUserId.ToString(), resetToken, changingPasswordModel.NewPassword); + IdentityResult resetResult = await userMgr.ChangePasswordWithResetAsync(userId, resetToken, changingPasswordModel.NewPassword); if (resetResult.Succeeded == false) { From 525d14ed2569378bb6841f48e666b8e9ea24f5b5 Mon Sep 17 00:00:00 2001 From: Emma Garland Date: Fri, 26 Feb 2021 14:21:23 +0000 Subject: [PATCH 004/188] Updated to set correct properties --- .../Repositories/Implement/MemberRepository.cs | 1 + .../Controllers/MemberControllerUnitTests.cs | 1 + .../Controllers/CurrentUserController.cs | 9 +++------ .../Controllers/MemberController.cs | 1 + .../Controllers/UsersController.cs | 1 + .../DependencyInjection/UmbracoBuilderExtensions.cs | 5 ++++- src/Umbraco.Web.BackOffice/Security/IPasswordChanger.cs | 2 +- src/Umbraco.Web.BackOffice/Security/PasswordChanger.cs | 2 +- src/Umbraco.Web/AspNet/AspNetPasswordHasher.cs | 2 ++ 9 files changed, 15 insertions(+), 9 deletions(-) diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MemberRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MemberRepository.cs index e97add3f5e..406eb08c62 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MemberRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MemberRepository.cs @@ -314,6 +314,7 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement // persist the member dto dto.NodeId = nodeDto.NodeId; + // TODO: password parts of this file need updating // if the password is empty, generate one with the special prefix // this will hash the guid with a salt so should be nicely random if (entity.RawPasswordValue.IsNullOrWhiteSpace()) diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Controllers/MemberControllerUnitTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Controllers/MemberControllerUnitTests.cs index f9f5c0d72a..9e26beea28 100644 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Controllers/MemberControllerUnitTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Controllers/MemberControllerUnitTests.cs @@ -35,6 +35,7 @@ using Umbraco.Cms.Web.BackOffice.Controllers; using Umbraco.Cms.Web.BackOffice.Mapping; using Umbraco.Cms.Web.BackOffice.Security; using Umbraco.Cms.Web.Common.ActionsResults; +using Umbraco.Cms.Web.Common.Security; using IHostingEnvironment = Umbraco.Cms.Core.Hosting.IHostingEnvironment; using MemberMapDefinition = Umbraco.Cms.Web.BackOffice.Mapping.MemberMapDefinition; diff --git a/src/Umbraco.Web.BackOffice/Controllers/CurrentUserController.cs b/src/Umbraco.Web.BackOffice/Controllers/CurrentUserController.cs index c0615e37a1..6492a7b528 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/CurrentUserController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/CurrentUserController.cs @@ -24,10 +24,10 @@ using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Strings; using Umbraco.Cms.Web.BackOffice.Extensions; using Umbraco.Cms.Web.BackOffice.Filters; -using Umbraco.Cms.Web.BackOffice.Security; using Umbraco.Cms.Web.Common.ActionsResults; using Umbraco.Cms.Web.Common.Attributes; using Umbraco.Cms.Web.Common.Authorization; +using Umbraco.Cms.Web.Common.Security; using Umbraco.Extensions; using Constants = Umbraco.Cms.Core.Constants; @@ -223,12 +223,9 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers public async Task>> PostChangePassword(ChangingPasswordModel changingPasswordModel) { IUser currentUser = _backOfficeSecurityAccessor.BackOfficeSecurity.CurrentUser; + changingPasswordModel.Id = currentUser.Id; - // if the current user has access to reset/manually change the password - if (currentUser.HasSectionAccess(Constants.Applications.Users) == false) - { - return new ValidationErrorResult("The current user is not authorized"); - } + // all current users have access to reset/manually change their password Attempt passwordChangeResult = await _passwordChanger.ChangePasswordWithIdentityAsync(changingPasswordModel, _backOfficeUserManager); diff --git a/src/Umbraco.Web.BackOffice/Controllers/MemberController.cs b/src/Umbraco.Web.BackOffice/Controllers/MemberController.cs index b5f81ca3f4..3174727071 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/MemberController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/MemberController.cs @@ -33,6 +33,7 @@ using Umbraco.Cms.Web.Common.ActionsResults; using Umbraco.Cms.Web.Common.Attributes; using Umbraco.Cms.Web.Common.Authorization; using Umbraco.Cms.Web.Common.Filters; +using Umbraco.Cms.Web.Common.Security; using Umbraco.Extensions; namespace Umbraco.Cms.Web.BackOffice.Controllers diff --git a/src/Umbraco.Web.BackOffice/Controllers/UsersController.cs b/src/Umbraco.Web.BackOffice/Controllers/UsersController.cs index 7ad12ecd65..ae8618e020 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/UsersController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/UsersController.cs @@ -39,6 +39,7 @@ using Umbraco.Cms.Web.BackOffice.Security; using Umbraco.Cms.Web.Common.ActionsResults; using Umbraco.Cms.Web.Common.Attributes; using Umbraco.Cms.Web.Common.Authorization; +using Umbraco.Cms.Web.Common.Security; using Umbraco.Extensions; using Constants = Umbraco.Cms.Core.Constants; diff --git a/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilderExtensions.cs b/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilderExtensions.cs index 2c79fd3e2a..53ea801490 100644 --- a/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilderExtensions.cs +++ b/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilderExtensions.cs @@ -8,6 +8,7 @@ using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.Hosting; using Umbraco.Cms.Core.IO; using Umbraco.Cms.Core.Models.Identity; +using Umbraco.Cms.Core.Security; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.WebAssets; using Umbraco.Cms.Infrastructure.DependencyInjection; @@ -21,6 +22,7 @@ using Umbraco.Cms.Web.BackOffice.Security; using Umbraco.Cms.Web.BackOffice.Services; using Umbraco.Cms.Web.BackOffice.Trees; using Umbraco.Cms.Web.Common.Authorization; +using Umbraco.Cms.Web.Common.Security; namespace Umbraco.Extensions { @@ -83,7 +85,8 @@ namespace Umbraco.Extensions builder.Services.AddUnique(); builder.Services.AddUnique(); builder.Services.AddUnique(); - builder.Services.AddUnique, PasswordChanger>(); + builder.Services.AddUnique, PasswordChanger>(); + builder.Services.AddUnique, PasswordChanger>(); return builder; } diff --git a/src/Umbraco.Web.BackOffice/Security/IPasswordChanger.cs b/src/Umbraco.Web.BackOffice/Security/IPasswordChanger.cs index 721ee6b683..d1f90d7bcf 100644 --- a/src/Umbraco.Web.BackOffice/Security/IPasswordChanger.cs +++ b/src/Umbraco.Web.BackOffice/Security/IPasswordChanger.cs @@ -4,7 +4,7 @@ using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.Identity; using Umbraco.Cms.Core.Security; -namespace Umbraco.Cms.Web.BackOffice.Security +namespace Umbraco.Cms.Web.Common.Security { public interface IPasswordChanger where TUser : UmbracoIdentityUser { diff --git a/src/Umbraco.Web.BackOffice/Security/PasswordChanger.cs b/src/Umbraco.Web.BackOffice/Security/PasswordChanger.cs index 99e8a98a32..83f68c8754 100644 --- a/src/Umbraco.Web.BackOffice/Security/PasswordChanger.cs +++ b/src/Umbraco.Web.BackOffice/Security/PasswordChanger.cs @@ -9,7 +9,7 @@ using Umbraco.Cms.Core.Models.Identity; using Umbraco.Cms.Core.Security; using Umbraco.Extensions; -namespace Umbraco.Cms.Web.BackOffice.Security +namespace Umbraco.Cms.Web.Common.Security { /// /// Changes the password for an identity user diff --git a/src/Umbraco.Web/AspNet/AspNetPasswordHasher.cs b/src/Umbraco.Web/AspNet/AspNetPasswordHasher.cs index 7cdeef6e21..e7adae86a6 100644 --- a/src/Umbraco.Web/AspNet/AspNetPasswordHasher.cs +++ b/src/Umbraco.Web/AspNet/AspNetPasswordHasher.cs @@ -1,8 +1,10 @@ +using System; using Microsoft.AspNet.Identity; using IPasswordHasher = Umbraco.Cms.Core.Security.IPasswordHasher; namespace Umbraco.Web { + [Obsolete("Should be removed")] public class AspNetPasswordHasher : Cms.Core.Security.IPasswordHasher { private PasswordHasher _underlyingHasher; From bce27c1fe419b2412cde78c7a2464f13ebe5be5c Mon Sep 17 00:00:00 2001 From: Emma Garland Date: Fri, 26 Feb 2021 14:33:27 +0000 Subject: [PATCH 005/188] Reverted name change --- .../Controllers/CurrentUserController.cs | 40 +++++++++---------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/src/Umbraco.Web.BackOffice/Controllers/CurrentUserController.cs b/src/Umbraco.Web.BackOffice/Controllers/CurrentUserController.cs index 6492a7b528..54a17bc951 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/CurrentUserController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/CurrentUserController.cs @@ -43,7 +43,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers private readonly ContentSettings _contentSettings; private readonly IHostingEnvironment _hostingEnvironment; private readonly IImageUrlGenerator _imageUrlGenerator; - private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor; + private readonly IBackOfficeSecurityAccessor _backofficeSecurityAccessor; private readonly IUserService _userService; private readonly UmbracoMapper _umbracoMapper; private readonly IBackOfficeUserManager _backOfficeUserManager; @@ -72,7 +72,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers _contentSettings = contentSettings.Value; _hostingEnvironment = hostingEnvironment; _imageUrlGenerator = imageUrlGenerator; - _backOfficeSecurityAccessor = backofficeSecurityAccessor; + _backofficeSecurityAccessor = backofficeSecurityAccessor; _userService = userService; _umbracoMapper = umbracoMapper; _backOfficeUserManager = backOfficeUserManager; @@ -93,7 +93,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers public Dictionary GetPermissions(int[] nodeIds) { var permissions = _userService - .GetPermissions(_backOfficeSecurityAccessor.BackOfficeSecurity.CurrentUser, nodeIds); + .GetPermissions(_backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser, nodeIds); var permissionsDictionary = new Dictionary(); foreach (var nodeId in nodeIds) @@ -114,7 +114,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers [HttpGet] public bool HasPermission(string permissionToCheck, int nodeId) { - var p = _userService.GetPermissions(_backOfficeSecurityAccessor.BackOfficeSecurity.CurrentUser, nodeId).GetAllPermissions(); + var p = _userService.GetPermissions(_backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser, nodeId).GetAllPermissions(); if (p.Contains(permissionToCheck.ToString(CultureInfo.InvariantCulture))) { return true; @@ -133,15 +133,15 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers if (status == null) throw new ArgumentNullException(nameof(status)); List userTours; - if (_backOfficeSecurityAccessor.BackOfficeSecurity.CurrentUser.TourData.IsNullOrWhiteSpace()) + if (_backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser.TourData.IsNullOrWhiteSpace()) { userTours = new List { status }; - _backOfficeSecurityAccessor.BackOfficeSecurity.CurrentUser.TourData = JsonConvert.SerializeObject(userTours); - _userService.Save(_backOfficeSecurityAccessor.BackOfficeSecurity.CurrentUser); + _backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser.TourData = JsonConvert.SerializeObject(userTours); + _userService.Save(_backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser); return userTours; } - userTours = JsonConvert.DeserializeObject>(_backOfficeSecurityAccessor.BackOfficeSecurity.CurrentUser.TourData).ToList(); + userTours = JsonConvert.DeserializeObject>(_backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser.TourData).ToList(); var found = userTours.FirstOrDefault(x => x.Alias == status.Alias); if (found != null) { @@ -149,8 +149,8 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers userTours.Remove(found); } userTours.Add(status); - _backOfficeSecurityAccessor.BackOfficeSecurity.CurrentUser.TourData = JsonConvert.SerializeObject(userTours); - _userService.Save(_backOfficeSecurityAccessor.BackOfficeSecurity.CurrentUser); + _backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser.TourData = JsonConvert.SerializeObject(userTours); + _userService.Save(_backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser); return userTours; } @@ -160,10 +160,10 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers /// public IEnumerable GetUserTours() { - if (_backOfficeSecurityAccessor.BackOfficeSecurity.CurrentUser.TourData.IsNullOrWhiteSpace()) + if (_backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser.TourData.IsNullOrWhiteSpace()) return Enumerable.Empty(); - var userTours = JsonConvert.DeserializeObject>(_backOfficeSecurityAccessor.BackOfficeSecurity.CurrentUser.TourData); + var userTours = JsonConvert.DeserializeObject>(_backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser.TourData); return userTours; } @@ -179,7 +179,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers [AllowAnonymous] public async Task> PostSetInvitedUserPassword([FromBody]string newPassword) { - var user = await _backOfficeUserManager.FindByIdAsync(_backOfficeSecurityAccessor.BackOfficeSecurity.GetUserId().ResultOr(0).ToString()); + var user = await _backOfficeUserManager.FindByIdAsync(_backofficeSecurityAccessor.BackOfficeSecurity.GetUserId().ResultOr(0).ToString()); if (user == null) throw new InvalidOperationException("Could not find user"); var result = await _backOfficeUserManager.AddPasswordAsync(user, newPassword); @@ -194,13 +194,13 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers } //They've successfully set their password, we can now update their user account to be approved - _backOfficeSecurityAccessor.BackOfficeSecurity.CurrentUser.IsApproved = true; + _backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser.IsApproved = true; //They've successfully set their password, and will now get fully logged into the back office, so the lastlogindate is set so the backoffice shows they have logged in - _backOfficeSecurityAccessor.BackOfficeSecurity.CurrentUser.LastLoginDate = DateTime.UtcNow; - _userService.Save(_backOfficeSecurityAccessor.BackOfficeSecurity.CurrentUser); + _backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser.LastLoginDate = DateTime.UtcNow; + _userService.Save(_backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser); //now we can return their full object since they are now really logged into the back office - var userDisplay = _umbracoMapper.Map(_backOfficeSecurityAccessor.BackOfficeSecurity.CurrentUser); + var userDisplay = _umbracoMapper.Map(_backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser); userDisplay.SecondsUntilTimeout = HttpContext.User.GetRemainingAuthSeconds(); return userDisplay; @@ -210,7 +210,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers public IActionResult PostSetAvatar(IList file) { //borrow the logic from the user controller - return UsersController.PostSetAvatarInternal(file, _userService, _appCaches.RuntimeCache, _mediaFileSystem, _shortStringHelper, _contentSettings, _hostingEnvironment, _imageUrlGenerator, _backOfficeSecurityAccessor.BackOfficeSecurity.GetUserId().ResultOr(0)); + return UsersController.PostSetAvatarInternal(file, _userService, _appCaches.RuntimeCache, _mediaFileSystem, _shortStringHelper, _contentSettings, _hostingEnvironment, _imageUrlGenerator, _backofficeSecurityAccessor.BackOfficeSecurity.GetUserId().ResultOr(0)); } /// @@ -222,7 +222,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers /// public async Task>> PostChangePassword(ChangingPasswordModel changingPasswordModel) { - IUser currentUser = _backOfficeSecurityAccessor.BackOfficeSecurity.CurrentUser; + IUser currentUser = _backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser; changingPasswordModel.Id = currentUser.Id; // all current users have access to reset/manually change their password @@ -250,7 +250,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers [ValidateAngularAntiForgeryToken] public async Task> GetCurrentUserLinkedLogins() { - var identityUser = await _backOfficeUserManager.FindByIdAsync(_backOfficeSecurityAccessor.BackOfficeSecurity.GetUserId().ResultOr(0).ToString()); + var identityUser = await _backOfficeUserManager.FindByIdAsync(_backofficeSecurityAccessor.BackOfficeSecurity.GetUserId().ResultOr(0).ToString()); // deduplicate in case there are duplicates (there shouldn't be now since we have a unique constraint on the external logins // but there didn't used to be) From 034b080c41266d10a926e7a0372a99f3fb4c1aa1 Mon Sep 17 00:00:00 2001 From: Emma Garland Date: Fri, 26 Feb 2021 14:52:54 +0000 Subject: [PATCH 006/188] Don't rename, too many file changes --- .../Controllers/UsersController.cs | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/Umbraco.Web.BackOffice/Controllers/UsersController.cs b/src/Umbraco.Web.BackOffice/Controllers/UsersController.cs index ae8618e020..7bf6dcfd1b 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/UsersController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/UsersController.cs @@ -58,7 +58,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers private readonly IImageUrlGenerator _imageUrlGenerator; private readonly SecuritySettings _securitySettings; private readonly IEmailSender _emailSender; - private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor; + private readonly IBackOfficeSecurityAccessor _backofficeSecurityAccessor; private readonly AppCaches _appCaches; private readonly IShortStringHelper _shortStringHelper; private readonly IUserService _userService; @@ -102,7 +102,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers _imageUrlGenerator = imageUrlGenerator; _securitySettings = securitySettings.Value; _emailSender = emailSender; - _backOfficeSecurityAccessor = backofficeSecurityAccessor; + _backofficeSecurityAccessor = backofficeSecurityAccessor; _appCaches = appCaches; _shortStringHelper = shortStringHelper; _userService = userService; @@ -124,7 +124,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers /// public ActionResult GetCurrentUserAvatarUrls() { - var urls = _backOfficeSecurityAccessor.BackOfficeSecurity.CurrentUser.GetUserAvatarUrls(_appCaches.RuntimeCache, _mediaFileSystem, _imageUrlGenerator); + var urls = _backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser.GetUserAvatarUrls(_appCaches.RuntimeCache, _mediaFileSystem, _imageUrlGenerator); if (urls == null) return new ValidationErrorResult("Could not access Gravatar endpoint"); @@ -290,7 +290,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers var hideDisabledUsers = _securitySettings.HideDisabledUsersInBackOffice; var excludeUserGroups = new string[0]; - var isAdmin = _backOfficeSecurityAccessor.BackOfficeSecurity.CurrentUser.IsAdmin(); + var isAdmin = _backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser.IsAdmin(); if (isAdmin == false) { //this user is not an admin so in that case we need to exclude all admin users @@ -299,7 +299,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers var filterQuery = _sqlContext.Query(); - if (!_backOfficeSecurityAccessor.BackOfficeSecurity.CurrentUser.IsSuper()) + if (!_backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser.IsSuper()) { // only super can see super - but don't use IsSuper, cannot be mapped to SQL //filterQuery.Where(x => !x.IsSuper()); @@ -364,7 +364,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers } //Perform authorization here to see if the current user can actually save this user with the info being requested - var canSaveUser = _userEditorAuthorizationHelper.IsAuthorized(_backOfficeSecurityAccessor.BackOfficeSecurity.CurrentUser, null, null, null, userSave.UserGroups); + var canSaveUser = _userEditorAuthorizationHelper.IsAuthorized(_backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser, null, null, null, userSave.UserGroups); if (canSaveUser == false) { return Unauthorized(canSaveUser.Result); @@ -453,7 +453,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers } //Perform authorization here to see if the current user can actually save this user with the info being requested - var canSaveUser = _userEditorAuthorizationHelper.IsAuthorized(_backOfficeSecurityAccessor.BackOfficeSecurity.CurrentUser, user, null, null, userSave.UserGroups); + var canSaveUser = _userEditorAuthorizationHelper.IsAuthorized(_backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser, user, null, null, userSave.UserGroups); if (canSaveUser == false) { return new ValidationErrorResult(canSaveUser.Result, StatusCodes.Status401Unauthorized); @@ -516,7 +516,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers { //send the email - await SendUserInviteEmailAsync(display, _backOfficeSecurityAccessor.BackOfficeSecurity.CurrentUser.Name, _backOfficeSecurityAccessor.BackOfficeSecurity.CurrentUser.Email, user, userSave.Message); + await SendUserInviteEmailAsync(display, _backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser.Name, _backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser.Email, user, userSave.Message); } @@ -610,7 +610,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers return NotFound(); //Perform authorization here to see if the current user can actually save this user with the info being requested - var canSaveUser = _userEditorAuthorizationHelper.IsAuthorized(_backOfficeSecurityAccessor.BackOfficeSecurity.CurrentUser, found, userSave.StartContentIds, userSave.StartMediaIds, userSave.UserGroups); + var canSaveUser = _userEditorAuthorizationHelper.IsAuthorized(_backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser, found, userSave.StartContentIds, userSave.StartMediaIds, userSave.UserGroups); if (canSaveUser == false) { return Unauthorized(canSaveUser.Result); @@ -670,7 +670,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers var display = _umbracoMapper.Map(user); // determine if the user has changed their own language; - var currentUser = _backOfficeSecurityAccessor.BackOfficeSecurity.CurrentUser; + var currentUser = _backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser; var userHasChangedOwnLanguage = user.Id == currentUser.Id && currentUser.Language != user.Language; @@ -708,7 +708,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers return NotFound(); } - IUser currentUser = _backOfficeSecurityAccessor.BackOfficeSecurity.CurrentUser; + IUser currentUser = _backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser; // if it's the current user, the current user cannot reset their own password if (currentUser.Username == found.Username) @@ -747,7 +747,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers [Authorize(Policy = AuthorizationPolicies.AdminUserEditsRequireAdmin)] public IActionResult PostDisableUsers([FromQuery]int[] userIds) { - var tryGetCurrentUserId = _backOfficeSecurityAccessor.BackOfficeSecurity.GetUserId(); + var tryGetCurrentUserId = _backofficeSecurityAccessor.BackOfficeSecurity.GetUserId(); if (tryGetCurrentUserId && userIds.Contains(tryGetCurrentUserId.Result)) { return ValidationErrorResult.CreateNotificationValidationErrorResult("The current user cannot disable itself"); From dee74f4c44b10ab0f6f0b8a967cdbf4cdac285ec Mon Sep 17 00:00:00 2001 From: Emma Garland Date: Fri, 26 Feb 2021 15:29:44 +0000 Subject: [PATCH 007/188] Updated member after password changed on identity member --- .../Controllers/MemberController.cs | 26 ++++++++++--------- 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/src/Umbraco.Web.BackOffice/Controllers/MemberController.cs b/src/Umbraco.Web.BackOffice/Controllers/MemberController.cs index 3174727071..95e2ac021b 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/MemberController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/MemberController.cs @@ -486,26 +486,28 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers Attempt passwordChangeResult = await _passwordChanger.ChangePasswordWithIdentityAsync(changingPasswordModel, _memberManager); - if (passwordChangeResult.Success) - { - contentItem.PersistedContent.RawPasswordValue = passwordChangeResult.Result.ResetPassword; - if (identityMember.LastPasswordChangeDateUtc != null) - { - contentItem.PersistedContent.LastPasswordChangeDate = DateTime.UtcNow; - } - - identityMember.LastPasswordChangeDateUtc = contentItem.PersistedContent.LastPasswordChangeDate; - } - if (passwordChangeResult.Result.ChangeError?.MemberNames != null) { foreach (string memberName in passwordChangeResult.Result.ChangeError?.MemberNames) { ModelState.AddModelError(memberName, passwordChangeResult.Result.ChangeError?.ErrorMessage ?? string.Empty); } + return new ValidationErrorResult(new SimpleValidationModel(ModelState.ToErrorDictionary())); } - return new ValidationErrorResult(new SimpleValidationModel(ModelState.ToErrorDictionary())); + if (passwordChangeResult.Success) + { + // get the identity member now the password and dates have changed + identityMember = await _memberManager.FindByIdAsync(contentItem.Id.ToString()); + + //TODO: confirm this is correct + contentItem.PersistedContent.RawPasswordValue = identityMember.PasswordHash; + + if (identityMember.LastPasswordChangeDateUtc != null) + { + contentItem.PersistedContent.LastPasswordChangeDate = (DateTime)identityMember.LastPasswordChangeDateUtc; + } + } } IdentityResult updatedResult = await _memberManager.UpdateAsync(identityMember); From f5593b1b1422715a7cea0b7f8f1d961a84105fea Mon Sep 17 00:00:00 2001 From: emmagarland Date: Sun, 28 Feb 2021 17:02:05 +0000 Subject: [PATCH 008/188] Fixed membercontroller unit tests --- .../Controllers/MemberControllerUnitTests.cs | 88 +++++++++++++------ 1 file changed, 63 insertions(+), 25 deletions(-) diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Controllers/MemberControllerUnitTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Controllers/MemberControllerUnitTests.cs index 9e26beea28..6dfad9cc9c 100644 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Controllers/MemberControllerUnitTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Controllers/MemberControllerUnitTests.cs @@ -21,7 +21,9 @@ using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Mapping; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.ContentEditing; +using Umbraco.Cms.Core.Models.Identity; using Umbraco.Cms.Core.Models.Mapping; +using Umbraco.Cms.Core.Models.Membership; using Umbraco.Cms.Core.PropertyEditors; using Umbraco.Cms.Core.Security; using Umbraco.Cms.Core.Serialization; @@ -68,11 +70,13 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Web.BackOffice.Controllers IMemberGroupService memberGroupService, IDataTypeService dataTypeService, IBackOfficeSecurityAccessor backOfficeSecurityAccessor, - IPasswordChanger passwordChanger) + IPasswordChanger passwordChanger, + IOptions globalSettings, + IUser user) { // arrange Member member = SetupMemberTestData(out MemberSave fakeMemberData, out MemberDisplay memberDisplay, ContentSaveAction.SaveNew); - MemberController sut = CreateSut(memberService, memberTypeService, memberGroupService, umbracoMembersUserManager, dataTypeService, backOfficeSecurityAccessor, passwordChanger); + MemberController sut = CreateSut(memberService, memberTypeService, memberGroupService, umbracoMembersUserManager, dataTypeService, backOfficeSecurityAccessor, passwordChanger, globalSettings, user); sut.ModelState.AddModelError("key", "Invalid model state"); Mock.Get(umbracoMembersUserManager) @@ -105,7 +109,9 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Web.BackOffice.Controllers IDataTypeService dataTypeService, IBackOfficeSecurityAccessor backOfficeSecurityAccessor, IBackOfficeSecurity backOfficeSecurity, - IPasswordChanger passwordChanger) + IPasswordChanger passwordChanger, + IOptions globalSettings, + IUser user) { // arrange Member member = SetupMemberTestData(out MemberSave fakeMemberData, out MemberDisplay memberDisplay, ContentSaveAction.SaveNew); @@ -123,7 +129,7 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Web.BackOffice.Controllers .Returns(() => member); Mock.Get(memberService).Setup(x => x.GetByUsername(It.IsAny())).Returns(() => member); - MemberController sut = CreateSut(memberService, memberTypeService, memberGroupService, umbracoMembersUserManager, dataTypeService, backOfficeSecurityAccessor, passwordChanger); + MemberController sut = CreateSut(memberService, memberTypeService, memberGroupService, umbracoMembersUserManager, dataTypeService, backOfficeSecurityAccessor, passwordChanger, globalSettings, user); // act ActionResult result = await sut.PostSave(fakeMemberData); @@ -144,7 +150,9 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Web.BackOffice.Controllers IDataTypeService dataTypeService, IBackOfficeSecurityAccessor backOfficeSecurityAccessor, IBackOfficeSecurity backOfficeSecurity, - IPasswordChanger passwordChanger) + IPasswordChanger passwordChanger, + IOptions globalSettings, + IUser user) { // arrange Member member = SetupMemberTestData(out MemberSave fakeMemberData, out MemberDisplay memberDisplay, ContentSaveAction.SaveNew); @@ -162,7 +170,7 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Web.BackOffice.Controllers .Returns(() => member); Mock.Get(memberService).Setup(x => x.GetByUsername(It.IsAny())).Returns(() => member); - MemberController sut = CreateSut(memberService, memberTypeService, memberGroupService, umbracoMembersUserManager, dataTypeService, backOfficeSecurityAccessor, passwordChanger); + MemberController sut = CreateSut(memberService, memberTypeService, memberGroupService, umbracoMembersUserManager, dataTypeService, backOfficeSecurityAccessor, passwordChanger, globalSettings, user); // act ActionResult result = await sut.PostSave(fakeMemberData); @@ -184,33 +192,38 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Web.BackOffice.Controllers IDataTypeService dataTypeService, IBackOfficeSecurityAccessor backOfficeSecurityAccessor, IBackOfficeSecurity backOfficeSecurity, - IPasswordChanger passwordChanger) + IPasswordChanger passwordChanger, + IOptions globalSettings, + IUser user) { // arrange Member member = SetupMemberTestData(out MemberSave fakeMemberData, out MemberDisplay memberDisplay, ContentSaveAction.Save); + var membersIdentityUser = new MembersIdentityUser(123); Mock.Get(umbracoMembersUserManager) .Setup(x => x.FindByIdAsync(It.IsAny())) - .ReturnsAsync(() => new MembersIdentityUser()); + .ReturnsAsync(() => membersIdentityUser); Mock.Get(umbracoMembersUserManager) .Setup(x => x.ValidatePasswordAsync(It.IsAny())) .ReturnsAsync(() => IdentityResult.Success); string password = "fakepassword9aw89rnyco3938cyr^%&*()i8Y"; - Mock.Get(passwordChanger) - .Setup(x => x.ChangePasswordWithIdentityAsync(It.IsAny(), umbracoMembersUserManager)) - .ReturnsAsync(() => new Attempt()); Mock.Get(umbracoMembersUserManager) .Setup(x => x.UpdateAsync(It.IsAny())) .ReturnsAsync(() => IdentityResult.Success); Mock.Get(memberTypeService).Setup(x => x.GetDefault()).Returns("fakeAlias"); - Mock.Get(backOfficeSecurityAccessor).Setup(x => x.BackOfficeSecurity).Returns(backOfficeSecurity); + Mock.Get(globalSettings); + + SetupUserAccess(backOfficeSecurityAccessor, backOfficeSecurity, user); + SetupPasswordSuccess(umbracoMembersUserManager, passwordChanger); + Mock.Get(memberService).Setup(x => x.GetByUsername(It.IsAny())).Returns(() => member); Mock.Get(memberService).SetupSequence( x => x.GetByEmail(It.IsAny())) .Returns(() => null) .Returns(() => member); - MemberController sut = CreateSut(memberService, memberTypeService, memberGroupService, umbracoMembersUserManager, dataTypeService, backOfficeSecurityAccessor, passwordChanger); + + MemberController sut = CreateSut(memberService, memberTypeService, memberGroupService, umbracoMembersUserManager, dataTypeService, backOfficeSecurityAccessor, passwordChanger, globalSettings, user); // act ActionResult result = await sut.PostSave(fakeMemberData); @@ -221,6 +234,26 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Web.BackOffice.Controllers AssertMemberDisplayPropertiesAreEqual(memberDisplay, result.Value); } + private static void SetupUserAccess(IBackOfficeSecurityAccessor backOfficeSecurityAccessor, IBackOfficeSecurity backOfficeSecurity, IUser user) + { + Mock.Get(backOfficeSecurityAccessor).Setup(x => x.BackOfficeSecurity).Returns(backOfficeSecurity); + Mock.Get(user).Setup(x => x.AllowedSections).Returns(new[] { "member" }); + Mock.Get(backOfficeSecurity).Setup(x => x.CurrentUser).Returns(user); + } + + private static void SetupPasswordSuccess(IMemberManager umbracoMembersUserManager, IPasswordChanger passwordChanger) + { + var passwordChanged = new PasswordChangedModel() + { + ChangeError = null, + ResetPassword = null + }; + var attempt = Attempt.Succeed(passwordChanged); + Mock.Get(passwordChanger) + .Setup(x => x.ChangePasswordWithIdentityAsync(It.IsAny(), umbracoMembersUserManager)) + .ReturnsAsync(() => attempt); + } + [Test] [AutoMoqData] public void PostSaveMember_SaveNew_WhenMemberEmailAlreadyExists_ExpectFailResponse( @@ -231,7 +264,9 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Web.BackOffice.Controllers IDataTypeService dataTypeService, IBackOfficeSecurityAccessor backOfficeSecurityAccessor, IBackOfficeSecurity backOfficeSecurity, - IPasswordChanger passwordChanger) + IPasswordChanger passwordChanger, + IOptions globalSettings, + IUser user) { // arrange Member member = SetupMemberTestData(out MemberSave fakeMemberData, out MemberDisplay memberDisplay, ContentSaveAction.SaveNew); @@ -248,7 +283,7 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Web.BackOffice.Controllers x => x.GetByEmail(It.IsAny())) .Returns(() => member); - MemberController sut = CreateSut(memberService, memberTypeService, memberGroupService, umbracoMembersUserManager, dataTypeService, backOfficeSecurityAccessor, passwordChanger); + MemberController sut = CreateSut(memberService, memberTypeService, memberGroupService, umbracoMembersUserManager, dataTypeService, backOfficeSecurityAccessor, passwordChanger, globalSettings, user); string reason = "Validation failed"; // act @@ -271,7 +306,9 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Web.BackOffice.Controllers IDataTypeService dataTypeService, IBackOfficeSecurityAccessor backOfficeSecurityAccessor, IBackOfficeSecurity backOfficeSecurity, - IPasswordChanger passwordChanger) + IPasswordChanger passwordChanger, + IOptions globalSettings, + IUser user) { // arrange string password = "fakepassword9aw89rnyco3938cyr^%&*()i8Y"; @@ -288,9 +325,6 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Web.BackOffice.Controllers Mock.Get(umbracoMembersUserManager) .Setup(x => x.ValidatePasswordAsync(It.IsAny())) .ReturnsAsync(() => IdentityResult.Success); - Mock.Get(passwordChanger) - .Setup(x => x.ChangePasswordWithIdentityAsync(It.IsAny(), umbracoMembersUserManager)) - .ReturnsAsync(() => Attempt.Succeed(new PasswordChangedModel())); Mock.Get(umbracoMembersUserManager) .Setup(x => x.UpdateAsync(It.IsAny())) .ReturnsAsync(() => IdentityResult.Success); @@ -298,12 +332,14 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Web.BackOffice.Controllers Mock.Get(backOfficeSecurityAccessor).Setup(x => x.BackOfficeSecurity).Returns(backOfficeSecurity); Mock.Get(memberService).Setup(x => x.GetByUsername(It.IsAny())).Returns(() => member); + SetupUserAccess(backOfficeSecurityAccessor, backOfficeSecurity, user); + SetupPasswordSuccess(umbracoMembersUserManager, passwordChanger); + Mock.Get(memberService).SetupSequence( x => x.GetByEmail(It.IsAny())) .Returns(() => null) .Returns(() => member); - Mock.Get(memberService).Setup(x => x.GetByUsername(It.IsAny())).Returns(() => member); - MemberController sut = CreateSut(memberService, memberTypeService, memberGroupService, umbracoMembersUserManager, dataTypeService, backOfficeSecurityAccessor, passwordChanger); + MemberController sut = CreateSut(memberService, memberTypeService, memberGroupService, umbracoMembersUserManager, dataTypeService, backOfficeSecurityAccessor, passwordChanger, globalSettings, user); // act ActionResult result = await sut.PostSave(fakeMemberData); @@ -338,7 +374,9 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Web.BackOffice.Controllers IUmbracoUserManager membersUserManager, IDataTypeService dataTypeService, IBackOfficeSecurityAccessor backOfficeSecurityAccessor, - IPasswordChanger mockPasswordChanger) + IPasswordChanger passwordChanger, + IOptions globalSettings, + IUser user) { var mockShortStringHelper = new MockShortStringHelper(); var textService = new Mock(); @@ -397,7 +435,7 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Web.BackOffice.Controllers memberTypeService, new Mock().Object, mockShortStringHelper, - new Mock>().Object, + globalSettings, new Mock().Object) }); _mapper = new UmbracoMapper(map); @@ -415,11 +453,11 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Web.BackOffice.Controllers _mapper, memberService, memberTypeService, - (IMemberManager) membersUserManager, + (IMemberManager)membersUserManager, dataTypeService, backOfficeSecurityAccessor, new ConfigurationEditorJsonSerializer(), - mockPasswordChanger); + passwordChanger); } /// From d67f4d7ec185ed72b5560e80467539b59edeac71 Mon Sep 17 00:00:00 2001 From: emmagarland Date: Sun, 28 Feb 2021 19:09:02 +0000 Subject: [PATCH 009/188] Added failed password test result --- .../Controllers/MemberControllerUnitTests.cs | 72 +++++++++++++++++-- .../Controllers/MemberController.cs | 20 +++--- 2 files changed, 76 insertions(+), 16 deletions(-) diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Controllers/MemberControllerUnitTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Controllers/MemberControllerUnitTests.cs index 6dfad9cc9c..5bb4613912 100644 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Controllers/MemberControllerUnitTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Controllers/MemberControllerUnitTests.cs @@ -21,7 +21,6 @@ using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Mapping; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.ContentEditing; -using Umbraco.Cms.Core.Models.Identity; using Umbraco.Cms.Core.Models.Mapping; using Umbraco.Cms.Core.Models.Membership; using Umbraco.Cms.Core.PropertyEditors; @@ -35,7 +34,6 @@ using Umbraco.Cms.Tests.UnitTests.AutoFixture; using Umbraco.Cms.Tests.UnitTests.Umbraco.Core.ShortStringHelper; using Umbraco.Cms.Web.BackOffice.Controllers; using Umbraco.Cms.Web.BackOffice.Mapping; -using Umbraco.Cms.Web.BackOffice.Security; using Umbraco.Cms.Web.Common.ActionsResults; using Umbraco.Cms.Web.Common.Security; using IHostingEnvironment = Umbraco.Cms.Core.Hosting.IHostingEnvironment; @@ -234,6 +232,56 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Web.BackOffice.Controllers AssertMemberDisplayPropertiesAreEqual(memberDisplay, result.Value); } + [Test] + [AutoMoqData] + public async Task PostSaveMember_SaveExisting_WhenAllIsSetupWithPasswordIncorrectly_ExpectFailureResponse( + [Frozen] IMemberManager umbracoMembersUserManager, + IMemberService memberService, + IMemberTypeService memberTypeService, + IMemberGroupService memberGroupService, + IDataTypeService dataTypeService, + IBackOfficeSecurityAccessor backOfficeSecurityAccessor, + IBackOfficeSecurity backOfficeSecurity, + IPasswordChanger passwordChanger, + IOptions globalSettings, + IUser user) + { + // arrange + Member member = SetupMemberTestData(out MemberSave fakeMemberData, out MemberDisplay memberDisplay, ContentSaveAction.Save); + var membersIdentityUser = new MembersIdentityUser(123); + Mock.Get(umbracoMembersUserManager) + .Setup(x => x.FindByIdAsync(It.IsAny())) + .ReturnsAsync(() => membersIdentityUser); + Mock.Get(umbracoMembersUserManager) + .Setup(x => x.ValidatePasswordAsync(It.IsAny())) + .ReturnsAsync(() => IdentityResult.Success); + + Mock.Get(umbracoMembersUserManager) + .Setup(x => x.UpdateAsync(It.IsAny())) + .ReturnsAsync(() => IdentityResult.Success); + Mock.Get(memberTypeService).Setup(x => x.GetDefault()).Returns("fakeAlias"); + Mock.Get(globalSettings); + + SetupUserAccess(backOfficeSecurityAccessor, backOfficeSecurity, user); + SetupPasswordSuccess(umbracoMembersUserManager, passwordChanger, false); + + Mock.Get(memberService).Setup(x => x.GetByUsername(It.IsAny())).Returns(() => member); + Mock.Get(memberService).SetupSequence( + x => x.GetByEmail(It.IsAny())) + .Returns(() => null) + .Returns(() => member); + + + MemberController sut = CreateSut(memberService, memberTypeService, memberGroupService, umbracoMembersUserManager, dataTypeService, backOfficeSecurityAccessor, passwordChanger, globalSettings, user); + + // act + ActionResult result = await sut.PostSave(fakeMemberData); + + // assert + Assert.IsNotNull(result.Result); + Assert.IsNull(result.Value); + } + private static void SetupUserAccess(IBackOfficeSecurityAccessor backOfficeSecurityAccessor, IBackOfficeSecurity backOfficeSecurity, IUser user) { Mock.Get(backOfficeSecurityAccessor).Setup(x => x.BackOfficeSecurity).Returns(backOfficeSecurity); @@ -241,17 +289,27 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Web.BackOffice.Controllers Mock.Get(backOfficeSecurity).Setup(x => x.CurrentUser).Returns(user); } - private static void SetupPasswordSuccess(IMemberManager umbracoMembersUserManager, IPasswordChanger passwordChanger) + private static void SetupPasswordSuccess(IMemberManager umbracoMembersUserManager, IPasswordChanger passwordChanger, bool successful = true) { var passwordChanged = new PasswordChangedModel() { ChangeError = null, ResetPassword = null }; - var attempt = Attempt.Succeed(passwordChanged); - Mock.Get(passwordChanger) - .Setup(x => x.ChangePasswordWithIdentityAsync(It.IsAny(), umbracoMembersUserManager)) - .ReturnsAsync(() => attempt); + if (!successful) + { + var attempt = Attempt.Fail(passwordChanged); + Mock.Get(passwordChanger) + .Setup(x => x.ChangePasswordWithIdentityAsync(It.IsAny(), umbracoMembersUserManager)) + .ReturnsAsync(() => attempt); + } + else + { + var attempt = Attempt.Succeed(passwordChanged); + Mock.Get(passwordChanger) + .Setup(x => x.ChangePasswordWithIdentityAsync(It.IsAny(), umbracoMembersUserManager)) + .ReturnsAsync(() => attempt); + } } [Test] diff --git a/src/Umbraco.Web.BackOffice/Controllers/MemberController.cs b/src/Umbraco.Web.BackOffice/Controllers/MemberController.cs index 95e2ac021b..3c5adb8ebe 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/MemberController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/MemberController.cs @@ -495,18 +495,20 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers return new ValidationErrorResult(new SimpleValidationModel(ModelState.ToErrorDictionary())); } - if (passwordChangeResult.Success) + if (!passwordChangeResult.Success) { - // get the identity member now the password and dates have changed - identityMember = await _memberManager.FindByIdAsync(contentItem.Id.ToString()); + return new ValidationErrorResult("The password could not be changed"); + } - //TODO: confirm this is correct - contentItem.PersistedContent.RawPasswordValue = identityMember.PasswordHash; + // get the identity member now the password and dates have changed + identityMember = await _memberManager.FindByIdAsync(contentItem.Id.ToString()); - if (identityMember.LastPasswordChangeDateUtc != null) - { - contentItem.PersistedContent.LastPasswordChangeDate = (DateTime)identityMember.LastPasswordChangeDateUtc; - } + //TODO: confirm this is correct + contentItem.PersistedContent.RawPasswordValue = identityMember.PasswordHash; + + if (identityMember.LastPasswordChangeDateUtc != null) + { + contentItem.PersistedContent.LastPasswordChangeDate = (DateTime)identityMember.LastPasswordChangeDateUtc; } } From 400b436d986d0924914e8bd3235f6497177c1b43 Mon Sep 17 00:00:00 2001 From: Mole Date: Mon, 1 Mar 2021 14:45:02 +0100 Subject: [PATCH 010/188] Fir stab/POC This still need ALOT of work, but is just an attempt to get my head around things. --- src/Umbraco.Core/Scoping/Scope.cs | 181 +++++++++++++++++++- src/Umbraco.Tests/Persistence/LocksTests.cs | 93 +++++++++- 2 files changed, 266 insertions(+), 8 deletions(-) diff --git a/src/Umbraco.Core/Scoping/Scope.cs b/src/Umbraco.Core/Scoping/Scope.cs index 24ef92278c..efbfe2ded1 100644 --- a/src/Umbraco.Core/Scoping/Scope.cs +++ b/src/Umbraco.Core/Scoping/Scope.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Data; using Umbraco.Core.Cache; using Umbraco.Core.Composing; @@ -34,6 +35,15 @@ namespace Umbraco.Core.Scoping private ICompletable _fscope; private IEventDispatcher _eventDispatcher; + // ReadLocks and WriteLocks owned by the entire chain, managed by the outermost scope + public Dictionary ReadLocks; + public Dictionary WriteLocks; + + // ReadLocks and WriteLocks requested by this specific scope, required to be able to decrement + // Using HashSets works well assume that you're only gonna request a lock once pr. scope + private HashSet _readLocks; + private HashSet _writelocks; + // initializes a new scope private Scope(ScopeProvider scopeProvider, ILogger logger, FileSystems fileSystems, Scope parent, ScopeContext scopeContext, bool detachable, @@ -58,6 +68,11 @@ namespace Umbraco.Core.Scoping Detachable = detachable; + ReadLocks = new Dictionary(); + WriteLocks = new Dictionary(); + _readLocks = new HashSet(); + _writelocks = new HashSet(); + #if DEBUG_SCOPES _scopeProvider.RegisterScope(this); Console.WriteLine("create " + InstanceId.ToString("N").Substring(0, 8)); @@ -348,6 +363,17 @@ namespace Umbraco.Core.Scoping #endif } + // Decrement the lock counters + foreach (var readLockId in _readLocks) + { + DecrementReadLock(readLockId); + } + + foreach (var writeLockId in _writelocks) + { + DecrementWriteLock(writeLockId); + } + var parent = ParentScope; _scopeProvider.AmbientScope = parent; // might be null = this is how scopes are removed from context objects @@ -486,32 +512,179 @@ namespace Umbraco.Core.Scoping private static bool LogUncompletedScopes => (_logUncompletedScopes ?? (_logUncompletedScopes = Current.Configs.CoreDebug().LogUncompletedScopes)).Value; + /// + /// Decrements the count of the ReadLocks with a specific ID we currently hold + /// + /// Lock ID to decrement + public void DecrementReadLock(int lockId) + { + if (ParentScope != null) + { + ParentScope.DecrementReadLock(lockId); + } + else + { + ReadLocks[lockId] -= 1; + } + } + + /// + /// Decrements the count of the WriteLocks with a specific ID we currently hold + /// + /// Lock ID to decrement + public void DecrementWriteLock(int lockId) + { + if (ParentScope != null) + { + ParentScope.DecrementWriteLock(lockId); + } + else + { + WriteLocks[lockId] -= 1; + } + } + /// - public void ReadLock(params int[] lockIds) => Database.SqlContext.SqlSyntax.ReadLock(Database, lockIds); + public void ReadLock(params int[] lockIds) + { + if (ParentScope != null) + { + foreach (var id in lockIds) + { + // We need to keep track of what lockIds we have requests to be able to decrement them. + // We have to do this out of the recursion to not add the ids to all scopes. + _readLocks.Add(id); + } + } + // Delegate acquiring the lock to the parent. + ReadLockInner(lockIds); + } + + internal void ReadLockInner(params int[] lockIds) + { + if (ParentScope != null) + { + // Delegate acquiring the lock to the parent. + ParentScope.ReadLockInner(lockIds); + return; + } + + foreach (var lockId in lockIds) + { + // Only acquire the lock if we haven't done so yet. + if (!ReadLocks.ContainsKey(lockId)) + { + Database.SqlContext.SqlSyntax.ReadLock(Database, lockId); + ReadLocks[lockId] = 0; + } + + // Increment the amount of locks we have of the specific ID after we've acquired it. + ReadLocks[lockId] += 1; + } + } /// public void ReadLock(TimeSpan timeout, int lockId) { + if (ParentScope != null) + { + _readLocks.Add(lockId); + } + + ReadLockInner(timeout, lockId); + } + + internal void ReadLockInner(TimeSpan timeout, int lockId) + { + if (ParentScope != null) + { + ParentScope.ReadLockInner(timeout, lockId); + return; + } + var syntax2 = Database.SqlContext.SqlSyntax as ISqlSyntaxProvider2; if (syntax2 == null) { throw new InvalidOperationException($"{Database.SqlContext.SqlSyntax.GetType()} is not of type {typeof(ISqlSyntaxProvider2)}"); } - syntax2.ReadLock(Database, timeout, lockId); + + if (!ReadLocks.ContainsKey(lockId)) + { + syntax2.ReadLock(Database, timeout, lockId); + ReadLocks[lockId] = 0; + } + + ReadLocks[lockId] += 1; } /// - public void WriteLock(params int[] lockIds) => Database.SqlContext.SqlSyntax.WriteLock(Database, lockIds); + public void WriteLock(params int[] lockIds) + { + // For some reason adding ids to the writelocks set of the outer most parent scope causes issues... + if (ParentScope != null) + { + foreach (var lockId in lockIds) + { + _writelocks.Add(lockId); + } + } + WriteLockInner(lockIds); + } + + internal void WriteLockInner(int[] lockIds) + { + // If we have a parent we just delegate lock creation to parent. + if (ParentScope != null) + { + ParentScope.WriteLockInner(lockIds); + return; + } + + // Only acquire lock if we haven't yet (WriteLocks not containing the key) + foreach (var lockId in lockIds) + { + if (!WriteLocks.ContainsKey(lockId)) + { + Database.SqlContext.SqlSyntax.WriteLock(Database, lockId); + WriteLocks[lockId] = 0; + } + + // Increment count of the lock by 1. + WriteLocks[lockId] += 1; + } + } /// public void WriteLock(TimeSpan timeout, int lockId) { + if (ParentScope != null) + { + _writelocks.Add(lockId); + } + WriteLockInner(timeout, lockId); + } + + internal void WriteLockInner(TimeSpan timeout, int lockId) + { + if (ParentScope != null) + { + ParentScope.WriteLockInner(timeout, lockId); + return; + } + var syntax2 = Database.SqlContext.SqlSyntax as ISqlSyntaxProvider2; if (syntax2 == null) { throw new InvalidOperationException($"{Database.SqlContext.SqlSyntax.GetType()} is not of type {typeof(ISqlSyntaxProvider2)}"); } - syntax2.WriteLock(Database, timeout, lockId); + + if (!WriteLocks.ContainsKey(lockId)) + { + syntax2.WriteLock(Database, timeout, lockId); + WriteLocks[lockId] = 0; + } + + WriteLocks[lockId] += 1; } } } diff --git a/src/Umbraco.Tests/Persistence/LocksTests.cs b/src/Umbraco.Tests/Persistence/LocksTests.cs index 1c651b9040..27a2947dbe 100644 --- a/src/Umbraco.Tests/Persistence/LocksTests.cs +++ b/src/Umbraco.Tests/Persistence/LocksTests.cs @@ -280,7 +280,7 @@ namespace Umbraco.Tests.Persistence [Test] public void Throws_When_Lock_Timeout_Is_Exceeded() - { + { var t1 = Task.Run(() => { using (var scope = ScopeProvider.CreateScope()) @@ -288,7 +288,7 @@ namespace Umbraco.Tests.Persistence var realScope = (Scope)scope; Console.WriteLine("Write lock A"); - // This will acquire right away + // This will acquire right away realScope.WriteLock(TimeSpan.FromMilliseconds(2000), Constants.Locks.ContentTree); Thread.Sleep(6000); // Wait longer than the Read Lock B timeout scope.Complete(); @@ -349,7 +349,7 @@ namespace Umbraco.Tests.Persistence var realScope = (Scope)scope; Console.WriteLine("Write lock A"); - // This will acquire right away + // This will acquire right away realScope.WriteLock(TimeSpan.FromMilliseconds(2000), Constants.Locks.ContentTree); Thread.Sleep(4000); // Wait less than the Read Lock B timeout scope.Complete(); @@ -377,7 +377,7 @@ namespace Umbraco.Tests.Persistence scope.Complete(); Interlocked.Increment(ref locksCompleted); Console.WriteLine("Finished Read lock B"); - } + } }); var t3 = Task.Run(() => @@ -424,6 +424,91 @@ namespace Umbraco.Tests.Persistence } } + [Test] + public void Nested_Scopes_WriteLocks_Count_Correctly() + { + using (var scope = ScopeProvider.CreateScope()) + { + var parentScope = (Scope) scope; + scope.WriteLock(Constants.Locks.ContentTree); + scope.WriteLock(Constants.Locks.ContentTypes); + + Assert.AreEqual(1, parentScope.WriteLocks[Constants.Locks.ContentTree], $"parentScope after locks acquired: {nameof(Constants.Locks.ContentTree)}"); + Assert.AreEqual(1, parentScope.WriteLocks[Constants.Locks.ContentTypes], $"parentScope after locks acquired: {nameof(Constants.Locks.ContentTypes)}"); + + using (var childScope1 = ScopeProvider.CreateScope()) + { + childScope1.WriteLock(Constants.Locks.ContentTree); + childScope1.WriteLock(Constants.Locks.ContentTypes); + childScope1.WriteLock(Constants.Locks.Languages); + + Assert.AreEqual(2, parentScope.WriteLocks[Constants.Locks.ContentTree], $"childScope1 after locks acquired: {nameof(Constants.Locks.ContentTree)}"); + Assert.AreEqual(2, parentScope.WriteLocks[Constants.Locks.ContentTypes], $"childScope1 after locks acquired: {nameof(Constants.Locks.ContentTypes)}"); + Assert.AreEqual(1, parentScope.WriteLocks[Constants.Locks.Languages], $"childScope1 after locks acquired: {nameof(Constants.Locks.Languages)}"); + + using (var childScope2 = ScopeProvider.CreateScope()) + { + childScope2.WriteLock(Constants.Locks.ContentTree); + childScope2.WriteLock(Constants.Locks.MediaTypes); + + Assert.AreEqual(3, parentScope.WriteLocks[Constants.Locks.ContentTree], $"childScope2 after locks acquired: {nameof(Constants.Locks.ContentTree)}"); + Assert.AreEqual(2, parentScope.WriteLocks[Constants.Locks.ContentTypes], $"childScope2 after locks acquired: {nameof(Constants.Locks.ContentTypes)}"); + Assert.AreEqual(1, parentScope.WriteLocks[Constants.Locks.Languages], $"childScope2 after locks acquired: {nameof(Constants.Locks.Languages)}"); + Assert.AreEqual(1, parentScope.WriteLocks[Constants.Locks.MediaTypes], $"childScope2 after locks acquired: {nameof(Constants.Locks.MediaTypes)}"); + } + Assert.AreEqual(2, parentScope.WriteLocks[Constants.Locks.ContentTree], $"childScope1 after inner scope disposed: {nameof(Constants.Locks.ContentTree)}"); + Assert.AreEqual(2, parentScope.WriteLocks[Constants.Locks.ContentTypes], $"childScope1 after inner scope disposed: {nameof(Constants.Locks.ContentTypes)}"); + Assert.AreEqual(1, parentScope.WriteLocks[Constants.Locks.Languages], $"childScope1 after inner scope disposed: {nameof(Constants.Locks.Languages)}"); + Assert.AreEqual(0, parentScope.WriteLocks[Constants.Locks.MediaTypes], $"childScope1 after inner scope disposed: {nameof(Constants.Locks.MediaTypes)}"); + } + Assert.AreEqual(1, parentScope.WriteLocks[Constants.Locks.ContentTree], $"parentScope after inner scopes disposed: {nameof(Constants.Locks.ContentTree)}"); + Assert.AreEqual(1, parentScope.WriteLocks[Constants.Locks.ContentTypes], $"parentScope after inner scopes disposed: {nameof(Constants.Locks.ContentTypes)}"); + Assert.AreEqual(0, parentScope.WriteLocks[Constants.Locks.Languages], $"parentScope after inner scopes disposed: {nameof(Constants.Locks.Languages)}"); + Assert.AreEqual(0, parentScope.WriteLocks[Constants.Locks.MediaTypes], $"parentScope after inner scopes disposed: {nameof(Constants.Locks.MediaTypes)}"); + } + } + + [Test] + public void Nested_Scopes_ReadLocks_Count_Correctly() + { + using (var scope = ScopeProvider.CreateScope()) + { + var parentScope = (Scope) scope; + scope.ReadLock(Constants.Locks.ContentTree); + scope.ReadLock(Constants.Locks.ContentTypes); + Assert.AreEqual(1, parentScope.ReadLocks[Constants.Locks.ContentTree], $"parentScope after locks acquired: {nameof(Constants.Locks.ContentTree)}"); + Assert.AreEqual(1, parentScope.ReadLocks[Constants.Locks.ContentTypes], $"parentScope after locks acquired: {nameof(Constants.Locks.ContentTypes)}"); + + using (var childScope1 = ScopeProvider.CreateScope()) + { + childScope1.ReadLock(Constants.Locks.ContentTree); + childScope1.ReadLock(Constants.Locks.ContentTypes); + childScope1.ReadLock(Constants.Locks.Languages); + Assert.AreEqual(2, parentScope.ReadLocks[Constants.Locks.ContentTree], $"childScope1 after locks acquired: {nameof(Constants.Locks.ContentTree)}"); + Assert.AreEqual(2, parentScope.ReadLocks[Constants.Locks.ContentTypes], $"childScope1 after locks acquired: {nameof(Constants.Locks.ContentTypes)}"); + Assert.AreEqual(1, parentScope.ReadLocks[Constants.Locks.Languages], $"childScope1 after locks acquired: {nameof(Constants.Locks.Languages)}"); + + using (var childScope2 = ScopeProvider.CreateScope()) + { + childScope2.ReadLock(Constants.Locks.ContentTree); + childScope2.ReadLock(Constants.Locks.MediaTypes); + Assert.AreEqual(3, parentScope.ReadLocks[Constants.Locks.ContentTree], $"childScope2 after locks acquired: {nameof(Constants.Locks.ContentTree)}"); + Assert.AreEqual(2, parentScope.ReadLocks[Constants.Locks.ContentTypes], $"childScope2 after locks acquired: {nameof(Constants.Locks.ContentTypes)}"); + Assert.AreEqual(1, parentScope.ReadLocks[Constants.Locks.Languages], $"childScope2 after locks acquired: {nameof(Constants.Locks.Languages)}"); + Assert.AreEqual(1, parentScope.ReadLocks[Constants.Locks.MediaTypes], $"childScope2 after locks acquired: {nameof(Constants.Locks.MediaTypes)}"); + } + Assert.AreEqual(2, parentScope.ReadLocks[Constants.Locks.ContentTree], $"childScope1 after inner scope disposed: {nameof(Constants.Locks.ContentTree)}"); + Assert.AreEqual(2, parentScope.ReadLocks[Constants.Locks.ContentTypes], $"childScope1 after inner scope disposed: {nameof(Constants.Locks.ContentTypes)}"); + Assert.AreEqual(1, parentScope.ReadLocks[Constants.Locks.Languages], $"childScope1 after inner scope disposed: {nameof(Constants.Locks.Languages)}"); + Assert.AreEqual(0, parentScope.ReadLocks[Constants.Locks.MediaTypes], $"childScope1 after inner scope disposed: {nameof(Constants.Locks.MediaTypes)}"); + } + Assert.AreEqual(1, parentScope.ReadLocks[Constants.Locks.ContentTree], $"parentScope after inner scopes disposed: {nameof(Constants.Locks.ContentTree)}"); + Assert.AreEqual(1, parentScope.ReadLocks[Constants.Locks.ContentTypes], $"parentScope after inner scopes disposed: {nameof(Constants.Locks.ContentTypes)}"); + Assert.AreEqual(0, parentScope.ReadLocks[Constants.Locks.Languages], $"parentScope after inner scopes disposed: {nameof(Constants.Locks.Languages)}"); + Assert.AreEqual(0, parentScope.ReadLocks[Constants.Locks.MediaTypes], $"parentScope after inner scopes disposed: {nameof(Constants.Locks.MediaTypes)}"); + } + } + private void NoDeadLockTestThread(int id, EventWaitHandle myEv, WaitHandle otherEv, ref Exception exception) { using (var scope = ScopeProvider.CreateScope()) From 65f2c5ee705d24964db3267af91aefda4f3cba37 Mon Sep 17 00:00:00 2001 From: Mole Date: Tue, 2 Mar 2021 08:48:09 +0100 Subject: [PATCH 011/188] Bit of cleaning in Scope --- src/Umbraco.Core/Scoping/Scope.cs | 48 +++++++++++++++++++++---------- 1 file changed, 33 insertions(+), 15 deletions(-) diff --git a/src/Umbraco.Core/Scoping/Scope.cs b/src/Umbraco.Core/Scoping/Scope.cs index efbfe2ded1..e79fc4ad19 100644 --- a/src/Umbraco.Core/Scoping/Scope.cs +++ b/src/Umbraco.Core/Scoping/Scope.cs @@ -40,9 +40,9 @@ namespace Umbraco.Core.Scoping public Dictionary WriteLocks; // ReadLocks and WriteLocks requested by this specific scope, required to be able to decrement - // Using HashSets works well assume that you're only gonna request a lock once pr. scope - private HashSet _readLocks; - private HashSet _writelocks; + // Using Lists just in case someone for some reason requests the same lock twice in the same scope. + private List _readLocks; + private List _writelocks; // initializes a new scope private Scope(ScopeProvider scopeProvider, @@ -70,8 +70,8 @@ namespace Umbraco.Core.Scoping ReadLocks = new Dictionary(); WriteLocks = new Dictionary(); - _readLocks = new HashSet(); - _writelocks = new HashSet(); + _readLocks = new List(); + _writelocks = new List(); #if DEBUG_SCOPES _scopeProvider.RegisterScope(this); @@ -518,14 +518,14 @@ namespace Umbraco.Core.Scoping /// Lock ID to decrement public void DecrementReadLock(int lockId) { + // If we aren't the outermost scope, pass it on to the parent. if (ParentScope != null) { ParentScope.DecrementReadLock(lockId); + return; } - else - { - ReadLocks[lockId] -= 1; - } + + ReadLocks[lockId] -= 1; } /// @@ -534,14 +534,14 @@ namespace Umbraco.Core.Scoping /// Lock ID to decrement public void DecrementWriteLock(int lockId) { + // If we aren't the outermost scope, pass it on to the parent. if (ParentScope != null) { ParentScope.DecrementWriteLock(lockId); + return; } - else - { - WriteLocks[lockId] -= 1; - } + + WriteLocks[lockId] -= 1; } /// @@ -551,15 +551,18 @@ namespace Umbraco.Core.Scoping { foreach (var id in lockIds) { - // We need to keep track of what lockIds we have requests to be able to decrement them. + // We need to keep track of what lockIds we have requested locks for to be able to decrement them. // We have to do this out of the recursion to not add the ids to all scopes. _readLocks.Add(id); } } - // Delegate acquiring the lock to the parent. ReadLockInner(lockIds); } + /// + /// Handles acquiring read locks, will delegate it to the parent if there are any. + /// + /// Array of lock object identifiers. internal void ReadLockInner(params int[] lockIds) { if (ParentScope != null) @@ -569,6 +572,7 @@ namespace Umbraco.Core.Scoping return; } + // If we are the parent, then handle the lock request. foreach (var lockId in lockIds) { // Only acquire the lock if we haven't done so yet. @@ -594,6 +598,11 @@ namespace Umbraco.Core.Scoping ReadLockInner(timeout, lockId); } + /// + /// Handles acquiring a read lock with a specified timeout, will delegate it to the parent if there are any. + /// + /// The database timeout in milliseconds. + /// The lock object identifier. internal void ReadLockInner(TimeSpan timeout, int lockId) { if (ParentScope != null) @@ -631,6 +640,10 @@ namespace Umbraco.Core.Scoping WriteLockInner(lockIds); } + /// + /// Handles acquiring write locks, will delegate it to the parent if there are any. + /// + /// Array of lock object identifiers. internal void WriteLockInner(int[] lockIds) { // If we have a parent we just delegate lock creation to parent. @@ -664,6 +677,11 @@ namespace Umbraco.Core.Scoping WriteLockInner(timeout, lockId); } + /// + /// Handles acquiring a write lock with a specified timeout, will delegate it to the parent if there are any. + /// + /// The database timeout in milliseconds. + /// The lock object identifier. internal void WriteLockInner(TimeSpan timeout, int lockId) { if (ParentScope != null) From e75edbcaf0469b7696629817a796a2eed61e2c42 Mon Sep 17 00:00:00 2001 From: Mole Date: Tue, 2 Mar 2021 10:53:10 +0100 Subject: [PATCH 012/188] Add more tests. --- src/Umbraco.Tests/Persistence/LocksTests.cs | 86 ------ src/Umbraco.Tests/Scoping/ScopeUnitTests.cs | 280 ++++++++++++++++++++ src/Umbraco.Tests/Umbraco.Tests.csproj | 1 + 3 files changed, 281 insertions(+), 86 deletions(-) create mode 100644 src/Umbraco.Tests/Scoping/ScopeUnitTests.cs diff --git a/src/Umbraco.Tests/Persistence/LocksTests.cs b/src/Umbraco.Tests/Persistence/LocksTests.cs index 27a2947dbe..8872329284 100644 --- a/src/Umbraco.Tests/Persistence/LocksTests.cs +++ b/src/Umbraco.Tests/Persistence/LocksTests.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using System.Data.SqlServerCe; using System.Linq; using System.Threading; @@ -424,91 +423,6 @@ namespace Umbraco.Tests.Persistence } } - [Test] - public void Nested_Scopes_WriteLocks_Count_Correctly() - { - using (var scope = ScopeProvider.CreateScope()) - { - var parentScope = (Scope) scope; - scope.WriteLock(Constants.Locks.ContentTree); - scope.WriteLock(Constants.Locks.ContentTypes); - - Assert.AreEqual(1, parentScope.WriteLocks[Constants.Locks.ContentTree], $"parentScope after locks acquired: {nameof(Constants.Locks.ContentTree)}"); - Assert.AreEqual(1, parentScope.WriteLocks[Constants.Locks.ContentTypes], $"parentScope after locks acquired: {nameof(Constants.Locks.ContentTypes)}"); - - using (var childScope1 = ScopeProvider.CreateScope()) - { - childScope1.WriteLock(Constants.Locks.ContentTree); - childScope1.WriteLock(Constants.Locks.ContentTypes); - childScope1.WriteLock(Constants.Locks.Languages); - - Assert.AreEqual(2, parentScope.WriteLocks[Constants.Locks.ContentTree], $"childScope1 after locks acquired: {nameof(Constants.Locks.ContentTree)}"); - Assert.AreEqual(2, parentScope.WriteLocks[Constants.Locks.ContentTypes], $"childScope1 after locks acquired: {nameof(Constants.Locks.ContentTypes)}"); - Assert.AreEqual(1, parentScope.WriteLocks[Constants.Locks.Languages], $"childScope1 after locks acquired: {nameof(Constants.Locks.Languages)}"); - - using (var childScope2 = ScopeProvider.CreateScope()) - { - childScope2.WriteLock(Constants.Locks.ContentTree); - childScope2.WriteLock(Constants.Locks.MediaTypes); - - Assert.AreEqual(3, parentScope.WriteLocks[Constants.Locks.ContentTree], $"childScope2 after locks acquired: {nameof(Constants.Locks.ContentTree)}"); - Assert.AreEqual(2, parentScope.WriteLocks[Constants.Locks.ContentTypes], $"childScope2 after locks acquired: {nameof(Constants.Locks.ContentTypes)}"); - Assert.AreEqual(1, parentScope.WriteLocks[Constants.Locks.Languages], $"childScope2 after locks acquired: {nameof(Constants.Locks.Languages)}"); - Assert.AreEqual(1, parentScope.WriteLocks[Constants.Locks.MediaTypes], $"childScope2 after locks acquired: {nameof(Constants.Locks.MediaTypes)}"); - } - Assert.AreEqual(2, parentScope.WriteLocks[Constants.Locks.ContentTree], $"childScope1 after inner scope disposed: {nameof(Constants.Locks.ContentTree)}"); - Assert.AreEqual(2, parentScope.WriteLocks[Constants.Locks.ContentTypes], $"childScope1 after inner scope disposed: {nameof(Constants.Locks.ContentTypes)}"); - Assert.AreEqual(1, parentScope.WriteLocks[Constants.Locks.Languages], $"childScope1 after inner scope disposed: {nameof(Constants.Locks.Languages)}"); - Assert.AreEqual(0, parentScope.WriteLocks[Constants.Locks.MediaTypes], $"childScope1 after inner scope disposed: {nameof(Constants.Locks.MediaTypes)}"); - } - Assert.AreEqual(1, parentScope.WriteLocks[Constants.Locks.ContentTree], $"parentScope after inner scopes disposed: {nameof(Constants.Locks.ContentTree)}"); - Assert.AreEqual(1, parentScope.WriteLocks[Constants.Locks.ContentTypes], $"parentScope after inner scopes disposed: {nameof(Constants.Locks.ContentTypes)}"); - Assert.AreEqual(0, parentScope.WriteLocks[Constants.Locks.Languages], $"parentScope after inner scopes disposed: {nameof(Constants.Locks.Languages)}"); - Assert.AreEqual(0, parentScope.WriteLocks[Constants.Locks.MediaTypes], $"parentScope after inner scopes disposed: {nameof(Constants.Locks.MediaTypes)}"); - } - } - - [Test] - public void Nested_Scopes_ReadLocks_Count_Correctly() - { - using (var scope = ScopeProvider.CreateScope()) - { - var parentScope = (Scope) scope; - scope.ReadLock(Constants.Locks.ContentTree); - scope.ReadLock(Constants.Locks.ContentTypes); - Assert.AreEqual(1, parentScope.ReadLocks[Constants.Locks.ContentTree], $"parentScope after locks acquired: {nameof(Constants.Locks.ContentTree)}"); - Assert.AreEqual(1, parentScope.ReadLocks[Constants.Locks.ContentTypes], $"parentScope after locks acquired: {nameof(Constants.Locks.ContentTypes)}"); - - using (var childScope1 = ScopeProvider.CreateScope()) - { - childScope1.ReadLock(Constants.Locks.ContentTree); - childScope1.ReadLock(Constants.Locks.ContentTypes); - childScope1.ReadLock(Constants.Locks.Languages); - Assert.AreEqual(2, parentScope.ReadLocks[Constants.Locks.ContentTree], $"childScope1 after locks acquired: {nameof(Constants.Locks.ContentTree)}"); - Assert.AreEqual(2, parentScope.ReadLocks[Constants.Locks.ContentTypes], $"childScope1 after locks acquired: {nameof(Constants.Locks.ContentTypes)}"); - Assert.AreEqual(1, parentScope.ReadLocks[Constants.Locks.Languages], $"childScope1 after locks acquired: {nameof(Constants.Locks.Languages)}"); - - using (var childScope2 = ScopeProvider.CreateScope()) - { - childScope2.ReadLock(Constants.Locks.ContentTree); - childScope2.ReadLock(Constants.Locks.MediaTypes); - Assert.AreEqual(3, parentScope.ReadLocks[Constants.Locks.ContentTree], $"childScope2 after locks acquired: {nameof(Constants.Locks.ContentTree)}"); - Assert.AreEqual(2, parentScope.ReadLocks[Constants.Locks.ContentTypes], $"childScope2 after locks acquired: {nameof(Constants.Locks.ContentTypes)}"); - Assert.AreEqual(1, parentScope.ReadLocks[Constants.Locks.Languages], $"childScope2 after locks acquired: {nameof(Constants.Locks.Languages)}"); - Assert.AreEqual(1, parentScope.ReadLocks[Constants.Locks.MediaTypes], $"childScope2 after locks acquired: {nameof(Constants.Locks.MediaTypes)}"); - } - Assert.AreEqual(2, parentScope.ReadLocks[Constants.Locks.ContentTree], $"childScope1 after inner scope disposed: {nameof(Constants.Locks.ContentTree)}"); - Assert.AreEqual(2, parentScope.ReadLocks[Constants.Locks.ContentTypes], $"childScope1 after inner scope disposed: {nameof(Constants.Locks.ContentTypes)}"); - Assert.AreEqual(1, parentScope.ReadLocks[Constants.Locks.Languages], $"childScope1 after inner scope disposed: {nameof(Constants.Locks.Languages)}"); - Assert.AreEqual(0, parentScope.ReadLocks[Constants.Locks.MediaTypes], $"childScope1 after inner scope disposed: {nameof(Constants.Locks.MediaTypes)}"); - } - Assert.AreEqual(1, parentScope.ReadLocks[Constants.Locks.ContentTree], $"parentScope after inner scopes disposed: {nameof(Constants.Locks.ContentTree)}"); - Assert.AreEqual(1, parentScope.ReadLocks[Constants.Locks.ContentTypes], $"parentScope after inner scopes disposed: {nameof(Constants.Locks.ContentTypes)}"); - Assert.AreEqual(0, parentScope.ReadLocks[Constants.Locks.Languages], $"parentScope after inner scopes disposed: {nameof(Constants.Locks.Languages)}"); - Assert.AreEqual(0, parentScope.ReadLocks[Constants.Locks.MediaTypes], $"parentScope after inner scopes disposed: {nameof(Constants.Locks.MediaTypes)}"); - } - } - private void NoDeadLockTestThread(int id, EventWaitHandle myEv, WaitHandle otherEv, ref Exception exception) { using (var scope = ScopeProvider.CreateScope()) diff --git a/src/Umbraco.Tests/Scoping/ScopeUnitTests.cs b/src/Umbraco.Tests/Scoping/ScopeUnitTests.cs new file mode 100644 index 0000000000..34d1843dc4 --- /dev/null +++ b/src/Umbraco.Tests/Scoping/ScopeUnitTests.cs @@ -0,0 +1,280 @@ +using System; +using Moq; +using NPoco; +using NUnit.Framework; +using Umbraco.Core; +using Umbraco.Core.Composing; +using Umbraco.Core.IO; +using Umbraco.Core.Logging; +using Umbraco.Core.Persistence; +using Umbraco.Core.Persistence.SqlSyntax; +using Umbraco.Core.Scoping; + +namespace Umbraco.Tests.Scoping +{ + [TestFixture] + public class ScopeUnitTests + { + /// + /// Creates a ScopeProvider with mocked internals. + /// + /// The mock of the ISqlSyntaxProvider2, used to count method calls. + /// + private ScopeProvider GetScopeProvider(out Mock syntaxProviderMock) + { + var logger = Mock.Of(); + var fac = Mock.Of(); + var fileSystem = new FileSystems(fac, logger); + var databaseFactory = new Mock(); + var database = new Mock(); + var sqlContext = new Mock(); + syntaxProviderMock = new Mock(); + + // Setup mock of database factory to return mock of database. + databaseFactory.Setup(x => x.CreateDatabase()).Returns(database.Object); + + // Setup mock of database to return mock of sql SqlContext + database.Setup(x => x.SqlContext).Returns(sqlContext.Object); + + // Setup mock of ISqlContext to return syntaxProviderMock + sqlContext.Setup(x => x.SqlSyntax).Returns(syntaxProviderMock.Object); + + return new ScopeProvider(databaseFactory.Object, fileSystem, logger); + } + + [Test] + public void WriteLock_Acquired_Only_Once_Per_Key() + { + var scopeProvider = GetScopeProvider(out var syntaxProviderMock); + + using (var outerScope = scopeProvider.CreateScope()) + { + outerScope.WriteLock(Constants.Locks.Domains); + outerScope.WriteLock(Constants.Locks.Languages); + + using (var innerScope1 = scopeProvider.CreateScope()) + { + innerScope1.WriteLock(Constants.Locks.Domains); + innerScope1.WriteLock(Constants.Locks.Languages); + + using (var innerScope2 = scopeProvider.CreateScope()) + { + innerScope2.WriteLock(Constants.Locks.Domains); + innerScope2.WriteLock(Constants.Locks.Languages); + innerScope2.Complete(); + } + innerScope1.Complete(); + } + outerScope.Complete(); + } + + syntaxProviderMock.Verify(x => x.WriteLock(It.IsAny(), Constants.Locks.Domains), Times.Once); + syntaxProviderMock.Verify(x => x.WriteLock(It.IsAny(), Constants.Locks.Languages), Times.Once); + } + + [Test] + public void WriteLock_With_Timeout_Acquired_Only_Once_Per_Key(){ + var scopeProvider = GetScopeProvider(out var syntaxProviderMock); + var timeout = TimeSpan.FromMilliseconds(10000); + + using (var outerScope = scopeProvider.CreateScope()) + { + var realScope = (Scope) outerScope; + realScope.WriteLock(timeout, Constants.Locks.Domains); + realScope.WriteLock(timeout, Constants.Locks.Languages); + + using (var innerScope1 = scopeProvider.CreateScope()) + { + var realInnerScope1 = (Scope) outerScope; + realInnerScope1.WriteLock(timeout, Constants.Locks.Domains); + realInnerScope1.WriteLock(timeout, Constants.Locks.Languages); + + using (var innerScope2 = scopeProvider.CreateScope()) + { + var realInnerScope2 = (Scope) innerScope2; + realInnerScope2.WriteLock(timeout, Constants.Locks.Domains); + realInnerScope2.WriteLock(timeout, Constants.Locks.Languages); + innerScope2.Complete(); + } + innerScope1.Complete(); + } + + outerScope.Complete(); + } + + syntaxProviderMock.Verify(x => x.WriteLock(It.IsAny(), timeout, Constants.Locks.Domains), Times.Once); + syntaxProviderMock.Verify(x => x.WriteLock(It.IsAny(), timeout, Constants.Locks.Languages), Times.Once); + } + + [Test] + public void ReadLock_Acquired_Only_Once_Per_Key() + { + var scopeProvider = GetScopeProvider(out var syntaxProviderMock); + + using (var outerScope = scopeProvider.CreateScope()) + { + outerScope.ReadLock(Constants.Locks.Domains); + outerScope.ReadLock(Constants.Locks.Languages); + + using (var innerScope1 = scopeProvider.CreateScope()) + { + innerScope1.ReadLock(Constants.Locks.Domains); + innerScope1.ReadLock(Constants.Locks.Languages); + + using (var innerScope2 = scopeProvider.CreateScope()) + { + innerScope2.ReadLock(Constants.Locks.Domains); + innerScope2.ReadLock(Constants.Locks.Languages); + + innerScope2.Complete(); + } + + innerScope1.Complete(); + } + + outerScope.Complete(); + } + + syntaxProviderMock.Verify(x => x.ReadLock(It.IsAny(), Constants.Locks.Domains), Times.Once); + syntaxProviderMock.Verify(x => x.ReadLock(It.IsAny(), Constants.Locks.Languages), Times.Once); + } + + [Test] + public void ReadLock_With_Timeout_Acquired_Only_Once_Per_Key() + { + var scopeProvider = GetScopeProvider(out var syntaxProviderMock); + var timeOut = TimeSpan.FromMilliseconds(10000); + + using (var outerScope = scopeProvider.CreateScope()) + { + var realOuterScope = (Scope) outerScope; + realOuterScope.ReadLock(timeOut, Constants.Locks.Domains); + realOuterScope.ReadLock(timeOut, Constants.Locks.Languages); + + using (var innerScope1 = scopeProvider.CreateScope()) + { + var realInnerScope1 = (Scope) innerScope1; + realInnerScope1.ReadLock(timeOut, Constants.Locks.Domains); + realInnerScope1.ReadLock(timeOut, Constants.Locks.Languages); + + using (var innerScope2 = scopeProvider.CreateScope()) + { + var realInnerScope2 = (Scope) innerScope2; + realInnerScope2.ReadLock(timeOut, Constants.Locks.Domains); + realInnerScope2.ReadLock(timeOut, Constants.Locks.Languages); + + innerScope2.Complete(); + } + + innerScope1.Complete(); + } + + outerScope.Complete(); + } + + syntaxProviderMock.Verify(x => x.ReadLock(It.IsAny(), timeOut, Constants.Locks.Domains), Times.Once); + syntaxProviderMock.Verify(x => x.ReadLock(It.IsAny(), timeOut, Constants.Locks.Languages), Times.Once); + } + + [Test] + public void Nested_Scopes_WriteLocks_Count_Correctly() + { + var scopeProvider = GetScopeProvider(out var syntaxProviderMock); + + using (var outerScope = scopeProvider.CreateScope()) + { + var parentScope = (Scope) outerScope; + outerScope.WriteLock(Constants.Locks.ContentTree); + outerScope.WriteLock(Constants.Locks.ContentTypes); + + Assert.AreEqual(1, parentScope.WriteLocks[Constants.Locks.ContentTree], $"parentScope after locks acquired: {nameof(Constants.Locks.ContentTree)}"); + Assert.AreEqual(1, parentScope.WriteLocks[Constants.Locks.ContentTypes], $"parentScope after locks acquired: {nameof(Constants.Locks.ContentTypes)}"); + + using (var innerScope1 = scopeProvider.CreateScope()) + { + innerScope1.WriteLock(Constants.Locks.ContentTree); + innerScope1.WriteLock(Constants.Locks.ContentTypes); + innerScope1.WriteLock(Constants.Locks.Languages); + + Assert.AreEqual(2, parentScope.WriteLocks[Constants.Locks.ContentTree], $"childScope1 after locks acquired: {nameof(Constants.Locks.ContentTree)}"); + Assert.AreEqual(2, parentScope.WriteLocks[Constants.Locks.ContentTypes], $"childScope1 after locks acquired: {nameof(Constants.Locks.ContentTypes)}"); + Assert.AreEqual(1, parentScope.WriteLocks[Constants.Locks.Languages], $"childScope1 after locks acquired: {nameof(Constants.Locks.Languages)}"); + + using (var innerScope2 = scopeProvider.CreateScope()) + { + innerScope2.WriteLock(Constants.Locks.ContentTree); + innerScope2.WriteLock(Constants.Locks.MediaTypes); + + Assert.AreEqual(3, parentScope.WriteLocks[Constants.Locks.ContentTree], $"childScope2 after locks acquired: {nameof(Constants.Locks.ContentTree)}"); + Assert.AreEqual(2, parentScope.WriteLocks[Constants.Locks.ContentTypes], $"childScope2 after locks acquired: {nameof(Constants.Locks.ContentTypes)}"); + Assert.AreEqual(1, parentScope.WriteLocks[Constants.Locks.Languages], $"childScope2 after locks acquired: {nameof(Constants.Locks.Languages)}"); + Assert.AreEqual(1, parentScope.WriteLocks[Constants.Locks.MediaTypes], $"childScope2 after locks acquired: {nameof(Constants.Locks.MediaTypes)}"); + + innerScope2.Complete(); + } + Assert.AreEqual(2, parentScope.WriteLocks[Constants.Locks.ContentTree], $"childScope1 after inner scope disposed: {nameof(Constants.Locks.ContentTree)}"); + Assert.AreEqual(2, parentScope.WriteLocks[Constants.Locks.ContentTypes], $"childScope1 after inner scope disposed: {nameof(Constants.Locks.ContentTypes)}"); + Assert.AreEqual(1, parentScope.WriteLocks[Constants.Locks.Languages], $"childScope1 after inner scope disposed: {nameof(Constants.Locks.Languages)}"); + Assert.AreEqual(0, parentScope.WriteLocks[Constants.Locks.MediaTypes], $"childScope1 after inner scope disposed: {nameof(Constants.Locks.MediaTypes)}"); + + innerScope1.Complete(); + } + Assert.AreEqual(1, parentScope.WriteLocks[Constants.Locks.ContentTree], $"parentScope after inner scopes disposed: {nameof(Constants.Locks.ContentTree)}"); + Assert.AreEqual(1, parentScope.WriteLocks[Constants.Locks.ContentTypes], $"parentScope after inner scopes disposed: {nameof(Constants.Locks.ContentTypes)}"); + Assert.AreEqual(0, parentScope.WriteLocks[Constants.Locks.Languages], $"parentScope after inner scopes disposed: {nameof(Constants.Locks.Languages)}"); + Assert.AreEqual(0, parentScope.WriteLocks[Constants.Locks.MediaTypes], $"parentScope after inner scopes disposed: {nameof(Constants.Locks.MediaTypes)}"); + + outerScope.Complete(); + } + } + + [Test] + public void Nested_Scopes_ReadLocks_Count_Correctly() + { + var scopeProvider = GetScopeProvider(out var syntaxProviderMock); + + using (var outerScope = scopeProvider.CreateScope()) + { + var parentScope = (Scope) outerScope; + outerScope.ReadLock(Constants.Locks.ContentTree); + outerScope.ReadLock(Constants.Locks.ContentTypes); + Assert.AreEqual(1, parentScope.ReadLocks[Constants.Locks.ContentTree], $"parentScope after locks acquired: {nameof(Constants.Locks.ContentTree)}"); + Assert.AreEqual(1, parentScope.ReadLocks[Constants.Locks.ContentTypes], $"parentScope after locks acquired: {nameof(Constants.Locks.ContentTypes)}"); + + using (var innserScope1 = scopeProvider.CreateScope()) + { + innserScope1.ReadLock(Constants.Locks.ContentTree); + innserScope1.ReadLock(Constants.Locks.ContentTypes); + innserScope1.ReadLock(Constants.Locks.Languages); + Assert.AreEqual(2, parentScope.ReadLocks[Constants.Locks.ContentTree], $"childScope1 after locks acquired: {nameof(Constants.Locks.ContentTree)}"); + Assert.AreEqual(2, parentScope.ReadLocks[Constants.Locks.ContentTypes], $"childScope1 after locks acquired: {nameof(Constants.Locks.ContentTypes)}"); + Assert.AreEqual(1, parentScope.ReadLocks[Constants.Locks.Languages], $"childScope1 after locks acquired: {nameof(Constants.Locks.Languages)}"); + + using (var innerScope2 = scopeProvider.CreateScope()) + { + innerScope2.ReadLock(Constants.Locks.ContentTree); + innerScope2.ReadLock(Constants.Locks.MediaTypes); + Assert.AreEqual(3, parentScope.ReadLocks[Constants.Locks.ContentTree], $"childScope2 after locks acquired: {nameof(Constants.Locks.ContentTree)}"); + Assert.AreEqual(2, parentScope.ReadLocks[Constants.Locks.ContentTypes], $"childScope2 after locks acquired: {nameof(Constants.Locks.ContentTypes)}"); + Assert.AreEqual(1, parentScope.ReadLocks[Constants.Locks.Languages], $"childScope2 after locks acquired: {nameof(Constants.Locks.Languages)}"); + Assert.AreEqual(1, parentScope.ReadLocks[Constants.Locks.MediaTypes], $"childScope2 after locks acquired: {nameof(Constants.Locks.MediaTypes)}"); + + innerScope2.Complete(); + } + Assert.AreEqual(2, parentScope.ReadLocks[Constants.Locks.ContentTree], $"childScope1 after inner scope disposed: {nameof(Constants.Locks.ContentTree)}"); + Assert.AreEqual(2, parentScope.ReadLocks[Constants.Locks.ContentTypes], $"childScope1 after inner scope disposed: {nameof(Constants.Locks.ContentTypes)}"); + Assert.AreEqual(1, parentScope.ReadLocks[Constants.Locks.Languages], $"childScope1 after inner scope disposed: {nameof(Constants.Locks.Languages)}"); + Assert.AreEqual(0, parentScope.ReadLocks[Constants.Locks.MediaTypes], $"childScope1 after inner scope disposed: {nameof(Constants.Locks.MediaTypes)}"); + + innserScope1.Complete(); + } + Assert.AreEqual(1, parentScope.ReadLocks[Constants.Locks.ContentTree], $"parentScope after inner scopes disposed: {nameof(Constants.Locks.ContentTree)}"); + Assert.AreEqual(1, parentScope.ReadLocks[Constants.Locks.ContentTypes], $"parentScope after inner scopes disposed: {nameof(Constants.Locks.ContentTypes)}"); + Assert.AreEqual(0, parentScope.ReadLocks[Constants.Locks.Languages], $"parentScope after inner scopes disposed: {nameof(Constants.Locks.Languages)}"); + Assert.AreEqual(0, parentScope.ReadLocks[Constants.Locks.MediaTypes], $"parentScope after inner scopes disposed: {nameof(Constants.Locks.MediaTypes)}"); + + outerScope.Complete(); + } + } + } +} diff --git a/src/Umbraco.Tests/Umbraco.Tests.csproj b/src/Umbraco.Tests/Umbraco.Tests.csproj index 97604df0c6..3059119dd4 100644 --- a/src/Umbraco.Tests/Umbraco.Tests.csproj +++ b/src/Umbraco.Tests/Umbraco.Tests.csproj @@ -161,6 +161,7 @@ + From 8c99a8e8a7952d80603515ba24f31fe9b8410815 Mon Sep 17 00:00:00 2001 From: Mole Date: Tue, 2 Mar 2021 12:55:18 +0100 Subject: [PATCH 013/188] Combine the two lock inner methods into one And add a couple of more unit tests --- src/Umbraco.Core/Scoping/Scope.cs | 210 +++++++++++--------- src/Umbraco.Tests/Scoping/ScopeUnitTests.cs | 58 ++++++ 2 files changed, 169 insertions(+), 99 deletions(-) diff --git a/src/Umbraco.Core/Scoping/Scope.cs b/src/Umbraco.Core/Scoping/Scope.cs index e79fc4ad19..f04d827209 100644 --- a/src/Umbraco.Core/Scoping/Scope.cs +++ b/src/Umbraco.Core/Scoping/Scope.cs @@ -513,9 +513,9 @@ namespace Umbraco.Core.Scoping ?? (_logUncompletedScopes = Current.Configs.CoreDebug().LogUncompletedScopes)).Value; /// - /// Decrements the count of the ReadLocks with a specific ID we currently hold + /// Decrements the count of the ReadLocks with a specific lock object identifier we currently hold /// - /// Lock ID to decrement + /// Lock object identifier to decrement public void DecrementReadLock(int lockId) { // If we aren't the outermost scope, pass it on to the parent. @@ -529,9 +529,9 @@ namespace Umbraco.Core.Scoping } /// - /// Decrements the count of the WriteLocks with a specific ID we currently hold + /// Decrements the count of the WriteLocks with a specific lock object identifier we currently hold. /// - /// Lock ID to decrement + /// Lock object identifier to decrement. public void DecrementWriteLock(int lockId) { // If we aren't the outermost scope, pass it on to the parent. @@ -556,35 +556,7 @@ namespace Umbraco.Core.Scoping _readLocks.Add(id); } } - ReadLockInner(lockIds); - } - - /// - /// Handles acquiring read locks, will delegate it to the parent if there are any. - /// - /// Array of lock object identifiers. - internal void ReadLockInner(params int[] lockIds) - { - if (ParentScope != null) - { - // Delegate acquiring the lock to the parent. - ParentScope.ReadLockInner(lockIds); - return; - } - - // If we are the parent, then handle the lock request. - foreach (var lockId in lockIds) - { - // Only acquire the lock if we haven't done so yet. - if (!ReadLocks.ContainsKey(lockId)) - { - Database.SqlContext.SqlSyntax.ReadLock(Database, lockId); - ReadLocks[lockId] = 0; - } - - // Increment the amount of locks we have of the specific ID after we've acquired it. - ReadLocks[lockId] += 1; - } + ReadLockInner(null, lockIds); } /// @@ -598,38 +570,9 @@ namespace Umbraco.Core.Scoping ReadLockInner(timeout, lockId); } - /// - /// Handles acquiring a read lock with a specified timeout, will delegate it to the parent if there are any. - /// - /// The database timeout in milliseconds. - /// The lock object identifier. - internal void ReadLockInner(TimeSpan timeout, int lockId) - { - if (ParentScope != null) - { - ParentScope.ReadLockInner(timeout, lockId); - return; - } - - var syntax2 = Database.SqlContext.SqlSyntax as ISqlSyntaxProvider2; - if (syntax2 == null) - { - throw new InvalidOperationException($"{Database.SqlContext.SqlSyntax.GetType()} is not of type {typeof(ISqlSyntaxProvider2)}"); - } - - if (!ReadLocks.ContainsKey(lockId)) - { - syntax2.ReadLock(Database, timeout, lockId); - ReadLocks[lockId] = 0; - } - - ReadLocks[lockId] += 1; - } - /// public void WriteLock(params int[] lockIds) { - // For some reason adding ids to the writelocks set of the outer most parent scope causes issues... if (ParentScope != null) { foreach (var lockId in lockIds) @@ -637,34 +580,7 @@ namespace Umbraco.Core.Scoping _writelocks.Add(lockId); } } - WriteLockInner(lockIds); - } - - /// - /// Handles acquiring write locks, will delegate it to the parent if there are any. - /// - /// Array of lock object identifiers. - internal void WriteLockInner(int[] lockIds) - { - // If we have a parent we just delegate lock creation to parent. - if (ParentScope != null) - { - ParentScope.WriteLockInner(lockIds); - return; - } - - // Only acquire lock if we haven't yet (WriteLocks not containing the key) - foreach (var lockId in lockIds) - { - if (!WriteLocks.ContainsKey(lockId)) - { - Database.SqlContext.SqlSyntax.WriteLock(Database, lockId); - WriteLocks[lockId] = 0; - } - - // Increment count of the lock by 1. - WriteLocks[lockId] += 1; - } + WriteLockInner(null, lockIds); } /// @@ -678,31 +594,127 @@ namespace Umbraco.Core.Scoping } /// - /// Handles acquiring a write lock with a specified timeout, will delegate it to the parent if there are any. + /// Handles acquiring a read lock, will delegate it to the parent if there are any. /// - /// The database timeout in milliseconds. - /// The lock object identifier. - internal void WriteLockInner(TimeSpan timeout, int lockId) + /// Optional database timeout in milliseconds. + /// Array of lock object identifiers. + internal void ReadLockInner(TimeSpan? timeout = null, params int[] lockIds) { if (ParentScope != null) { - ParentScope.WriteLockInner(timeout, lockId); + // Delegate acquiring the lock to the parent if any. + ParentScope.ReadLockInner(timeout, lockIds); return; } + // If we are the parent, then handle the lock request. + foreach (var lockId in lockIds) + { + // Only acquire the lock if we haven't done so yet. + if (!ReadLocks.ContainsKey(lockId)) + { + if (timeout is null) + { + // We want a lock with a custom timeout + ObtainReadLock(lockId); + } + else + { + // We just want an ordinary lock. + ObtainTimoutReadLock(lockId, timeout.Value); + } + // Add the lockId as a key to the dict. + ReadLocks[lockId] = 0; + } + + ReadLocks[lockId] += 1; + } + } + + /// + /// Handles acquiring a write lock with a specified timeout, will delegate it to the parent if there are any. + /// + /// Optional database timeout in milliseconds. + /// Array of lock object identifiers. + internal void WriteLockInner(TimeSpan? timeout = null, params int[] lockIds) + { + if (ParentScope != null) + { + // If we have a parent we delegate lock creation to parent. + ParentScope.WriteLockInner(timeout, lockIds); + return; + } + + foreach (var lockId in lockIds) + { + // Only acquire lock if we haven't yet (WriteLocks not containing the key) + if (!WriteLocks.ContainsKey(lockId)) + { + if (timeout is null) + { + ObtainWriteLock(lockId); + } + else + { + ObtainTimeoutWriteLock(lockId, timeout.Value); + } + // Add the lockId as a key to the dict. + WriteLocks[lockId] = 0; + } + + // Increment count of the lock by 1. + WriteLocks[lockId] += 1; + } + } + + /// + /// Obtains an ordinary read lock. + /// + /// Lock object identifier to lock. + private void ObtainReadLock(int lockId) + { + Database.SqlContext.SqlSyntax.ReadLock(Database, lockId); + } + + /// + /// Obtains a read lock with a custom timeout. + /// + /// Lock object identifier to lock. + /// TimeSpan specifying the timout period. + private void ObtainTimoutReadLock(int lockId, TimeSpan timeout) + { var syntax2 = Database.SqlContext.SqlSyntax as ISqlSyntaxProvider2; if (syntax2 == null) { throw new InvalidOperationException($"{Database.SqlContext.SqlSyntax.GetType()} is not of type {typeof(ISqlSyntaxProvider2)}"); } - if (!WriteLocks.ContainsKey(lockId)) + syntax2.ReadLock(Database, timeout, lockId); + } + + /// + /// Obtains an ordinary write lock. + /// + /// Lock object identifier to lock. + private void ObtainWriteLock(int lockId) + { + Database.SqlContext.SqlSyntax.WriteLock(Database, lockId); + } + + /// + /// Obtains a write lock with a custom timeout. + /// + /// Lock object identifier to lock. + /// TimeSpan specifying the timout period. + private void ObtainTimeoutWriteLock(int lockId, TimeSpan timeout) + { + var syntax2 = Database.SqlContext.SqlSyntax as ISqlSyntaxProvider2; + if (syntax2 == null) { - syntax2.WriteLock(Database, timeout, lockId); - WriteLocks[lockId] = 0; + throw new InvalidOperationException($"{Database.SqlContext.SqlSyntax.GetType()} is not of type {typeof(ISqlSyntaxProvider2)}"); } - WriteLocks[lockId] += 1; + syntax2.WriteLock(Database, timeout, lockId); } } } diff --git a/src/Umbraco.Tests/Scoping/ScopeUnitTests.cs b/src/Umbraco.Tests/Scoping/ScopeUnitTests.cs index 34d1843dc4..32bd7e2afe 100644 --- a/src/Umbraco.Tests/Scoping/ScopeUnitTests.cs +++ b/src/Umbraco.Tests/Scoping/ScopeUnitTests.cs @@ -176,6 +176,64 @@ namespace Umbraco.Tests.Scoping syntaxProviderMock.Verify(x => x.ReadLock(It.IsAny(), timeOut, Constants.Locks.Languages), Times.Once); } + [Test] + public void WriteLocks_Count_correctly_If_Lock_Requested_Twice_In_Scope() + { + var scopeProvider = GetScopeProvider(out var syntaxProviderMock); + + using (var outerscope = scopeProvider.CreateScope()) + { + var realOuterScope = (Scope) outerscope; + outerscope.WriteLock(Constants.Locks.ContentTree); + outerscope.WriteLock(Constants.Locks.ContentTree); + Assert.AreEqual(2, realOuterScope.WriteLocks[Constants.Locks.ContentTree]); + + using (var innerScope = scopeProvider.CreateScope()) + { + innerScope.WriteLock(Constants.Locks.ContentTree); + innerScope.WriteLock(Constants.Locks.ContentTree); + Assert.AreEqual(4, realOuterScope.WriteLocks[Constants.Locks.ContentTree]); + + innerScope.WriteLock(Constants.Locks.Languages); + innerScope.WriteLock(Constants.Locks.Languages); + Assert.AreEqual(2, realOuterScope.WriteLocks[Constants.Locks.Languages]); + innerScope.Complete(); + } + Assert.AreEqual(0, realOuterScope.WriteLocks[Constants.Locks.Languages]); + Assert.AreEqual(2, realOuterScope.WriteLocks[Constants.Locks.ContentTree]); + outerscope.Complete(); + } + } + + [Test] + public void ReadLocks_Count_correctly_If_Lock_Requested_Twice_In_Scope() + { + var scopeProvider = GetScopeProvider(out var syntaxProviderMock); + + using (var outerscope = scopeProvider.CreateScope()) + { + var realOuterScope = (Scope) outerscope; + outerscope.ReadLock(Constants.Locks.ContentTree); + outerscope.ReadLock(Constants.Locks.ContentTree); + Assert.AreEqual(2, realOuterScope.ReadLocks[Constants.Locks.ContentTree]); + + using (var innerScope = scopeProvider.CreateScope()) + { + innerScope.ReadLock(Constants.Locks.ContentTree); + innerScope.ReadLock(Constants.Locks.ContentTree); + Assert.AreEqual(4, realOuterScope.ReadLocks[Constants.Locks.ContentTree]); + + innerScope.ReadLock(Constants.Locks.Languages); + innerScope.ReadLock(Constants.Locks.Languages); + Assert.AreEqual(2, realOuterScope.ReadLocks[Constants.Locks.Languages]); + innerScope.Complete(); + } + Assert.AreEqual(0, realOuterScope.ReadLocks[Constants.Locks.Languages]); + Assert.AreEqual(2, realOuterScope.ReadLocks[Constants.Locks.ContentTree]); + outerscope.Complete(); + } + } + [Test] public void Nested_Scopes_WriteLocks_Count_Correctly() { From c01fab97b82761619aa3b62322fa8df05b6b7397 Mon Sep 17 00:00:00 2001 From: Mole Date: Tue, 2 Mar 2021 13:43:34 +0100 Subject: [PATCH 014/188] Count requested locks in child scopes with the existing dictionaries. This way we don't have to have a two dictionaries AND two lists. --- src/Umbraco.Core/Scoping/Scope.cs | 118 ++++++++++++++++++------------ 1 file changed, 72 insertions(+), 46 deletions(-) diff --git a/src/Umbraco.Core/Scoping/Scope.cs b/src/Umbraco.Core/Scoping/Scope.cs index f04d827209..bab40a7adf 100644 --- a/src/Umbraco.Core/Scoping/Scope.cs +++ b/src/Umbraco.Core/Scoping/Scope.cs @@ -35,15 +35,11 @@ namespace Umbraco.Core.Scoping private ICompletable _fscope; private IEventDispatcher _eventDispatcher; - // ReadLocks and WriteLocks owned by the entire chain, managed by the outermost scope + // ReadLocks and WriteLocks if we're the outer most scope it's those owned by the entire chain + // If we're a child scope it's those that we have requested. public Dictionary ReadLocks; public Dictionary WriteLocks; - // ReadLocks and WriteLocks requested by this specific scope, required to be able to decrement - // Using Lists just in case someone for some reason requests the same lock twice in the same scope. - private List _readLocks; - private List _writelocks; - // initializes a new scope private Scope(ScopeProvider scopeProvider, ILogger logger, FileSystems fileSystems, Scope parent, ScopeContext scopeContext, bool detachable, @@ -70,8 +66,6 @@ namespace Umbraco.Core.Scoping ReadLocks = new Dictionary(); WriteLocks = new Dictionary(); - _readLocks = new List(); - _writelocks = new List(); #if DEBUG_SCOPES _scopeProvider.RegisterScope(this); @@ -363,15 +357,18 @@ namespace Umbraco.Core.Scoping #endif } - // Decrement the lock counters - foreach (var readLockId in _readLocks) + // Decrement the lock counters on the parent if any. + if (ParentScope != null) { - DecrementReadLock(readLockId); - } + foreach (var readLockPair in ReadLocks) + { + DecrementReadLock(readLockPair.Key, readLockPair.Value); + } - foreach (var writeLockId in _writelocks) - { - DecrementWriteLock(writeLockId); + foreach (var writeLockPair in WriteLocks) + { + DecrementWriteLock(writeLockPair.Key, writeLockPair.Value); + } } var parent = ParentScope; @@ -516,80 +513,109 @@ namespace Umbraco.Core.Scoping /// Decrements the count of the ReadLocks with a specific lock object identifier we currently hold /// /// Lock object identifier to decrement - public void DecrementReadLock(int lockId) + /// Amount to decrement the lock count with + public void DecrementReadLock(int lockId, int amountToDecrement) { // If we aren't the outermost scope, pass it on to the parent. if (ParentScope != null) { - ParentScope.DecrementReadLock(lockId); + ParentScope.DecrementReadLock(lockId, amountToDecrement); return; } - ReadLocks[lockId] -= 1; + ReadLocks[lockId] -= amountToDecrement; } /// /// Decrements the count of the WriteLocks with a specific lock object identifier we currently hold. /// /// Lock object identifier to decrement. - public void DecrementWriteLock(int lockId) + /// Amount to decrement the lock count with + public void DecrementWriteLock(int lockId, int amountToDecrement) { // If we aren't the outermost scope, pass it on to the parent. if (ParentScope != null) { - ParentScope.DecrementWriteLock(lockId); + ParentScope.DecrementWriteLock(lockId, amountToDecrement); return; } - WriteLocks[lockId] -= 1; + WriteLocks[lockId] -= amountToDecrement; + } + + /// + /// Increment the count of the read locks we've requested + /// + /// + /// This should only be done on child scopes since it's then used to decrement the count later. + /// + /// + private void IncrementRequestedReadLock(params int[] lockIds) + { + // We need to keep track of what lockIds we have requested locks for to be able to decrement them. + if (ParentScope != null) + { + foreach (var lockId in lockIds) + { + if (!ReadLocks.ContainsKey(lockId)) + { + ReadLocks[lockId] = 0; + } + + ReadLocks[lockId] += 1; + } + } + } + + /// + /// Increment the count of the write locks we've requested + /// + /// + /// This should only be done on child scopes since it's then used to decrement the count later. + /// + /// + private void IncrementRequestedWriteLock(params int[] lockIds) + { + // We need to keep track of what lockIds we have requested locks for to be able to decrement them. + if (ParentScope != null) + { + foreach (var lockId in lockIds) + { + if (!WriteLocks.ContainsKey(lockId)) + { + WriteLocks[lockId] = 0; + } + + WriteLocks[lockId] += 1; + } + } } /// public void ReadLock(params int[] lockIds) { - if (ParentScope != null) - { - foreach (var id in lockIds) - { - // We need to keep track of what lockIds we have requested locks for to be able to decrement them. - // We have to do this out of the recursion to not add the ids to all scopes. - _readLocks.Add(id); - } - } + IncrementRequestedReadLock(lockIds); ReadLockInner(null, lockIds); } /// public void ReadLock(TimeSpan timeout, int lockId) { - if (ParentScope != null) - { - _readLocks.Add(lockId); - } - + IncrementRequestedReadLock(lockId); ReadLockInner(timeout, lockId); } /// public void WriteLock(params int[] lockIds) { - if (ParentScope != null) - { - foreach (var lockId in lockIds) - { - _writelocks.Add(lockId); - } - } + IncrementRequestedWriteLock(lockIds); WriteLockInner(null, lockIds); } /// public void WriteLock(TimeSpan timeout, int lockId) { - if (ParentScope != null) - { - _writelocks.Add(lockId); - } + IncrementRequestedWriteLock(lockId); WriteLockInner(timeout, lockId); } From f0723870962b70f81c76d8029e4c30def2ba3f59 Mon Sep 17 00:00:00 2001 From: Mole Date: Wed, 3 Mar 2021 08:55:14 +0100 Subject: [PATCH 015/188] Wrap access to dictionaries in locks and make dictionaries internal readonly --- src/Umbraco.Core/Scoping/Scope.cs | 113 ++++++++++++++++++------------ 1 file changed, 70 insertions(+), 43 deletions(-) diff --git a/src/Umbraco.Core/Scoping/Scope.cs b/src/Umbraco.Core/Scoping/Scope.cs index bab40a7adf..f1827d7d83 100644 --- a/src/Umbraco.Core/Scoping/Scope.cs +++ b/src/Umbraco.Core/Scoping/Scope.cs @@ -35,10 +35,12 @@ namespace Umbraco.Core.Scoping private ICompletable _fscope; private IEventDispatcher _eventDispatcher; + private object _dictionaryLocker; + // ReadLocks and WriteLocks if we're the outer most scope it's those owned by the entire chain // If we're a child scope it's those that we have requested. - public Dictionary ReadLocks; - public Dictionary WriteLocks; + internal readonly Dictionary ReadLocks; + internal readonly Dictionary WriteLocks; // initializes a new scope private Scope(ScopeProvider scopeProvider, @@ -64,6 +66,7 @@ namespace Umbraco.Core.Scoping Detachable = detachable; + _dictionaryLocker = new object(); ReadLocks = new Dictionary(); WriteLocks = new Dictionary(); @@ -360,14 +363,20 @@ namespace Umbraco.Core.Scoping // Decrement the lock counters on the parent if any. if (ParentScope != null) { - foreach (var readLockPair in ReadLocks) + lock (_dictionaryLocker) { - DecrementReadLock(readLockPair.Key, readLockPair.Value); + foreach (var readLockPair in ReadLocks) + { + DecrementReadLock(readLockPair.Key, readLockPair.Value); + } } - foreach (var writeLockPair in WriteLocks) + lock (_dictionaryLocker) { - DecrementWriteLock(writeLockPair.Key, writeLockPair.Value); + foreach (var writeLockPair in WriteLocks) + { + DecrementWriteLock(writeLockPair.Key, writeLockPair.Value); + } } } @@ -523,7 +532,10 @@ namespace Umbraco.Core.Scoping return; } - ReadLocks[lockId] -= amountToDecrement; + lock (_dictionaryLocker) + { + ReadLocks[lockId] -= amountToDecrement; + } } /// @@ -540,7 +552,10 @@ namespace Umbraco.Core.Scoping return; } - WriteLocks[lockId] -= amountToDecrement; + lock (_dictionaryLocker) + { + WriteLocks[lockId] -= amountToDecrement; + } } /// @@ -557,12 +572,15 @@ namespace Umbraco.Core.Scoping { foreach (var lockId in lockIds) { - if (!ReadLocks.ContainsKey(lockId)) + lock (_dictionaryLocker) { - ReadLocks[lockId] = 0; - } + if (!ReadLocks.ContainsKey(lockId)) + { + ReadLocks[lockId] = 0; + } - ReadLocks[lockId] += 1; + ReadLocks[lockId] += 1; + } } } } @@ -581,12 +599,15 @@ namespace Umbraco.Core.Scoping { foreach (var lockId in lockIds) { - if (!WriteLocks.ContainsKey(lockId)) + lock (_dictionaryLocker) { - WriteLocks[lockId] = 0; - } + if (!WriteLocks.ContainsKey(lockId)) + { + WriteLocks[lockId] = 0; + } - WriteLocks[lockId] += 1; + WriteLocks[lockId] += 1; + } } } } @@ -636,24 +657,27 @@ namespace Umbraco.Core.Scoping // If we are the parent, then handle the lock request. foreach (var lockId in lockIds) { - // Only acquire the lock if we haven't done so yet. - if (!ReadLocks.ContainsKey(lockId)) + lock (_dictionaryLocker) { - if (timeout is null) + // Only acquire the lock if we haven't done so yet. + if (!ReadLocks.ContainsKey(lockId)) { - // We want a lock with a custom timeout - ObtainReadLock(lockId); + if (timeout is null) + { + // We want a lock with a custom timeout + ObtainReadLock(lockId); + } + else + { + // We just want an ordinary lock. + ObtainTimoutReadLock(lockId, timeout.Value); + } + // Add the lockId as a key to the dict. + ReadLocks[lockId] = 0; } - else - { - // We just want an ordinary lock. - ObtainTimoutReadLock(lockId, timeout.Value); - } - // Add the lockId as a key to the dict. - ReadLocks[lockId] = 0; - } - ReadLocks[lockId] += 1; + ReadLocks[lockId] += 1; + } } } @@ -673,23 +697,26 @@ namespace Umbraco.Core.Scoping foreach (var lockId in lockIds) { - // Only acquire lock if we haven't yet (WriteLocks not containing the key) - if (!WriteLocks.ContainsKey(lockId)) + lock (_dictionaryLocker) { - if (timeout is null) + // Only acquire lock if we haven't yet (WriteLocks not containing the key) + if (!WriteLocks.ContainsKey(lockId)) { - ObtainWriteLock(lockId); + if (timeout is null) + { + ObtainWriteLock(lockId); + } + else + { + ObtainTimeoutWriteLock(lockId, timeout.Value); + } + // Add the lockId as a key to the dict. + WriteLocks[lockId] = 0; } - else - { - ObtainTimeoutWriteLock(lockId, timeout.Value); - } - // Add the lockId as a key to the dict. - WriteLocks[lockId] = 0; - } - // Increment count of the lock by 1. - WriteLocks[lockId] += 1; + // Increment count of the lock by 1. + WriteLocks[lockId] += 1; + } } } From 6db390e1d52e957a7489979c2f6acb28435cfe5a Mon Sep 17 00:00:00 2001 From: Andy Butland Date: Sun, 7 Mar 2021 09:53:25 +0100 Subject: [PATCH 016/188] Broke out SQL calls in DatabaseServerMessenger and BatchedDatabaseServerMessenger into a service and repository layer. --- src/Umbraco.Core/Models/CacheInstruction.cs | 51 ++ .../ICacheInstructionRepository.cs | 50 ++ ...eInstructionServiceInitializationResult.cs | 31 + ...ructionServiceProcessInstructionsResult.cs | 24 + .../Services/ICacheInstructionService.cs | 32 + .../Sync/RefreshInstruction.cs | 109 ++-- .../UmbracoBuilder.Repositories.cs | 1 + .../UmbracoBuilder.Services.cs | 1 + .../Factories/CacheInstructionFactory.cs | 25 + .../Implement/CacheInstructionRepository.cs | 73 +++ .../Services/Implement/AuditService.cs | 4 +- .../Implement/CacheInstructionService.cs | 501 ++++++++++++++++ .../Services/Implement/ConsentService.cs | 4 +- .../Implement/ContentTypeServiceBase.cs | 4 +- .../Services/Implement/DataTypeService.cs | 4 +- .../Services/Implement/DomainService.cs | 2 +- .../Services/Implement/EntityService.cs | 2 +- .../Implement/ExternalLoginService.cs | 2 +- .../Services/Implement/FileService.cs | 2 +- .../Services/Implement/LocalizationService.cs | 2 +- .../Services/Implement/MacroService.cs | 2 +- .../Services/Implement/MediaService.cs | 2 +- .../Services/Implement/MemberService.cs | 2 +- .../Services/Implement/PublicAccessService.cs | 2 +- .../Services/Implement/RedirectUrlService.cs | 2 +- .../Services/Implement/RelationService.cs | 2 +- .../Implement/ScopeRepositoryService.cs | 14 - .../Implement/ServerRegistrationService.cs | 2 +- .../Services/Implement/TagService.cs | 2 +- .../Services/Implement/UserService.cs | 2 +- .../Sync/BatchedDatabaseServerMessenger.cs | 56 +- .../Sync/DatabaseServerMessenger.cs | 557 +++--------------- 32 files changed, 949 insertions(+), 620 deletions(-) create mode 100644 src/Umbraco.Core/Models/CacheInstruction.cs create mode 100644 src/Umbraco.Core/Persistence/Repositories/ICacheInstructionRepository.cs create mode 100644 src/Umbraco.Core/Services/CacheInstructionServiceInitializationResult.cs create mode 100644 src/Umbraco.Core/Services/CacheInstructionServiceProcessInstructionsResult.cs create mode 100644 src/Umbraco.Core/Services/ICacheInstructionService.cs rename src/{Umbraco.Infrastructure => Umbraco.Core}/Sync/RefreshInstruction.cs (74%) create mode 100644 src/Umbraco.Infrastructure/Persistence/Factories/CacheInstructionFactory.cs create mode 100644 src/Umbraco.Infrastructure/Persistence/Repositories/Implement/CacheInstructionRepository.cs create mode 100644 src/Umbraco.Infrastructure/Services/Implement/CacheInstructionService.cs delete mode 100644 src/Umbraco.Infrastructure/Services/Implement/ScopeRepositoryService.cs diff --git a/src/Umbraco.Core/Models/CacheInstruction.cs b/src/Umbraco.Core/Models/CacheInstruction.cs new file mode 100644 index 0000000000..000788c8a0 --- /dev/null +++ b/src/Umbraco.Core/Models/CacheInstruction.cs @@ -0,0 +1,51 @@ +using System; +using System.Runtime.Serialization; + +namespace Umbraco.Cms.Core.Models +{ + /// + /// Represents a cache instruction. + /// + [Serializable] + [DataContract(IsReference = true)] + public class CacheInstruction + { + /// + /// Initializes a new instance of the class. + /// + public CacheInstruction(int id, DateTime utcStamp, string instructions, string originIdentity, int instructionCount) + { + Id = id; + UtcStamp = utcStamp; + Instructions = instructions; + OriginIdentity = originIdentity; + InstructionCount = instructionCount; + } + + /// + /// Cache instruction Id. + /// + public int Id { get; private set; } + + /// + /// Cache instruction created date. + /// + public DateTime UtcStamp { get; private set; } + + /// + /// Serialized instructions. + /// + public string Instructions { get; private set; } + + /// + /// Identity of server originating the instruction. + /// + public string OriginIdentity { get; private set; } + + /// + /// Count of instructions. + /// + public int InstructionCount { get; private set; } + + } +} diff --git a/src/Umbraco.Core/Persistence/Repositories/ICacheInstructionRepository.cs b/src/Umbraco.Core/Persistence/Repositories/ICacheInstructionRepository.cs new file mode 100644 index 0000000000..0381c74a03 --- /dev/null +++ b/src/Umbraco.Core/Persistence/Repositories/ICacheInstructionRepository.cs @@ -0,0 +1,50 @@ +using System; +using System.Collections.Generic; +using Umbraco.Cms.Core.Models; + +namespace Umbraco.Cms.Core.Persistence.Repositories +{ + /// + /// Represents a repository for entities. + /// + public interface ICacheInstructionRepository : IRepository + { + /// + /// Gets the count of pending cache instruction records. + /// + int CountAll(); + + /// + /// Gets the count of pending cache instructions. + /// + int CountPendingInstructions(int lastId); + + /// + /// Gets the most recent cache instruction record Id. + /// + /// + int GetMaxId(); + + /// + /// Checks to see if a single cache instruction by Id exists. + /// + bool Exists(int id); + + /// + /// Adds a new cache instruction record. + /// + void Add(CacheInstruction cacheInstruction); + + /// + /// Gets a collection of cache instructions created later than the provided Id. + /// + /// Last id processed. + /// The maximum number of instructions to retrieve. + IEnumerable GetInstructions(int lastId, int maxNumberToRetrieve); + + /// + /// Deletes cache instructions older than the provided date. + /// + void DeleteInstructionsOlderThan(DateTime pruneDate); + } +} diff --git a/src/Umbraco.Core/Services/CacheInstructionServiceInitializationResult.cs b/src/Umbraco.Core/Services/CacheInstructionServiceInitializationResult.cs new file mode 100644 index 0000000000..e7cea8ef33 --- /dev/null +++ b/src/Umbraco.Core/Services/CacheInstructionServiceInitializationResult.cs @@ -0,0 +1,31 @@ +namespace Umbraco.Cms.Core.Services +{ + /// + /// Defines a result object for the operation. + /// + public class CacheInstructionServiceInitializationResult + { + private CacheInstructionServiceInitializationResult() + { + } + + public bool Initialized { get; private set; } + + public bool ColdBootRequired { get; private set; } + + public int MaxId { get; private set; } + + public int LastId { get; private set; } + + public static CacheInstructionServiceInitializationResult AsUninitialized() => new CacheInstructionServiceInitializationResult { Initialized = false }; + + public static CacheInstructionServiceInitializationResult AsInitialized(bool coldBootRequired, int maxId, int lastId) => + new CacheInstructionServiceInitializationResult + { + Initialized = true, + ColdBootRequired = coldBootRequired, + MaxId = maxId, + LastId = lastId, + }; + } +} diff --git a/src/Umbraco.Core/Services/CacheInstructionServiceProcessInstructionsResult.cs b/src/Umbraco.Core/Services/CacheInstructionServiceProcessInstructionsResult.cs new file mode 100644 index 0000000000..79d8ec1bbb --- /dev/null +++ b/src/Umbraco.Core/Services/CacheInstructionServiceProcessInstructionsResult.cs @@ -0,0 +1,24 @@ +using System; + +namespace Umbraco.Cms.Core.Services +{ + /// + /// Defines a result object for the operation. + /// + public class CacheInstructionServiceProcessInstructionsResult + { + private CacheInstructionServiceProcessInstructionsResult() + { + } + + public int LastId { get; private set; } + + public bool InstructionsWerePruned { get; private set; } + + public static CacheInstructionServiceProcessInstructionsResult AsCompleted(int lastId) => + new CacheInstructionServiceProcessInstructionsResult { LastId = lastId }; + + public static CacheInstructionServiceProcessInstructionsResult AsCompletedAndPruned(int lastId) => + new CacheInstructionServiceProcessInstructionsResult { LastId = lastId, InstructionsWerePruned = true }; + }; +} diff --git a/src/Umbraco.Core/Services/ICacheInstructionService.cs b/src/Umbraco.Core/Services/ICacheInstructionService.cs new file mode 100644 index 0000000000..4589053fa8 --- /dev/null +++ b/src/Umbraco.Core/Services/ICacheInstructionService.cs @@ -0,0 +1,32 @@ +using System; +using System.Collections.Generic; +using Umbraco.Cms.Core.Sync; + +namespace Umbraco.Cms.Core.Services +{ + public interface ICacheInstructionService + { + /// + /// Ensures that the cache instruction service is initialized and can be used for syncing messages. + /// + CacheInstructionServiceInitializationResult EnsureInitialized(bool released, int lastId); + + /// + /// Creates a cache instruction record from a set of individual instructions and saves it. + /// + void DeliverInstructions(IEnumerable instructions, string localIdentity); + + /// + /// Creates one or more cache instruction records base on the configured batch size from a set of individual instructions and saves them. + /// + void DeliverInstructionsInBatches(IEnumerable instructions, string localIdentity); + + /// + /// Processes and then prunes pending database cache instructions. + /// + /// Flag indicating if process is shutting now and operations should exit. + /// Local local identity of the executing AppDomain. + /// Date of last prune operation. + CacheInstructionServiceProcessInstructionsResult ProcessInstructions(bool released, string localIdentity, DateTime lastPruned); + } +} diff --git a/src/Umbraco.Infrastructure/Sync/RefreshInstruction.cs b/src/Umbraco.Core/Sync/RefreshInstruction.cs similarity index 74% rename from src/Umbraco.Infrastructure/Sync/RefreshInstruction.cs rename to src/Umbraco.Core/Sync/RefreshInstruction.cs index ec6ffd0ed8..c01d758583 100644 --- a/src/Umbraco.Infrastructure/Sync/RefreshInstruction.cs +++ b/src/Umbraco.Core/Sync/RefreshInstruction.cs @@ -1,11 +1,10 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; -using Newtonsoft.Json; using Umbraco.Cms.Core.Cache; -using Umbraco.Cms.Core.Sync; +using Umbraco.Cms.Core.Serialization; -namespace Umbraco.Cms.Infrastructure.Sync +namespace Umbraco.Cms.Core.Sync { [Serializable] public class RefreshInstruction @@ -17,15 +16,27 @@ namespace Umbraco.Cms.Infrastructure.Sync // need this public, parameter-less constructor so the web service messenger // can de-serialize the instructions it receives - public RefreshInstruction() - { - //set default - this value is not used for reading after it's been deserialized, it's only used for persisting the instruction to the db - JsonIdCount = 1; - } - // need this public one so it can be de-serialized - used by the Json thing - // otherwise, should use GetInstructions(...) + /// + /// Initializes a new instance of the class. + /// + /// + /// Need this public, parameter-less constructor so the web service messenger can de-serialize the instructions it receives. + /// + public RefreshInstruction() => + + // Set default - this value is not used for reading after it's been deserialized, it's only used for persisting the instruction to the db + JsonIdCount = 1; + + /// + /// Initializes a new instance of the class. + /// + /// + /// Need this public one so it can be de-serialized - used by the Json thing + /// otherwise, should use GetInstructions(...) + /// public RefreshInstruction(Guid refresherId, RefreshMethodType refreshType, Guid guidId, int intId, string jsonIds, string jsonPayload) + : this() { RefresherId = refresherId; RefreshType = refreshType; @@ -33,36 +44,24 @@ namespace Umbraco.Cms.Infrastructure.Sync IntId = intId; JsonIds = jsonIds; JsonPayload = jsonPayload; - //set default - this value is not used for reading after it's been deserialized, it's only used for persisting the instruction to the db - JsonIdCount = 1; } private RefreshInstruction(ICacheRefresher refresher, RefreshMethodType refreshType) + : this() { RefresherId = refresher.RefresherUniqueId; RefreshType = refreshType; - //set default - this value is not used for reading after it's been deserialized, it's only used for persisting the instruction to the db - JsonIdCount = 1; } private RefreshInstruction(ICacheRefresher refresher, RefreshMethodType refreshType, Guid guidId) - : this(refresher, refreshType) - { - GuidId = guidId; - } + : this(refresher, refreshType) => GuidId = guidId; private RefreshInstruction(ICacheRefresher refresher, RefreshMethodType refreshType, int intId) - : this(refresher, refreshType) - { - IntId = intId; - } + : this(refresher, refreshType) => IntId = intId; /// /// A private constructor to create a new instance /// - /// - /// - /// /// /// When the refresh method is we know how many Ids are being refreshed so we know the instruction /// count which will be taken into account when we store this count in the database. @@ -73,13 +72,18 @@ namespace Umbraco.Cms.Infrastructure.Sync JsonIdCount = idCount; if (refreshType == RefreshMethodType.RefreshByJson) + { JsonPayload = json; + } else + { JsonIds = json; + } } public static IEnumerable GetInstructions( ICacheRefresher refresher, + IJsonSerializer jsonSerializer, MessageType messageType, IEnumerable ids, Type idType, @@ -95,20 +99,27 @@ namespace Umbraco.Cms.Infrastructure.Sync case MessageType.RefreshById: if (idType == null) + { throw new InvalidOperationException("Cannot refresh by id if idType is null."); + } + if (idType == typeof(int)) { - // bulk of ints is supported + // Bulk of ints is supported var intIds = ids.Cast().ToArray(); - return new[] { new RefreshInstruction(refresher, RefreshMethodType.RefreshByIds, JsonConvert.SerializeObject(intIds), intIds.Length) }; + return new[] { new RefreshInstruction(refresher, RefreshMethodType.RefreshByIds, jsonSerializer.Serialize(intIds), intIds.Length) }; } - // else must be guids, bulk of guids is not supported, iterate + + // Else must be guids, bulk of guids is not supported, so iterate. return ids.Select(x => new RefreshInstruction(refresher, RefreshMethodType.RefreshByGuid, (Guid) x)); case MessageType.RemoveById: if (idType == null) + { throw new InvalidOperationException("Cannot remove by id if idType is null."); - // must be ints, bulk-remove is not supported, iterate + } + + // Must be ints, bulk-remove is not supported, so iterate. return ids.Select(x => new RefreshInstruction(refresher, RefreshMethodType.RemoveById, (int) x)); //return new[] { new RefreshInstruction(refresher, RefreshMethodType.RemoveByIds, JsonConvert.SerializeObject(ids.Cast().ToArray())) }; @@ -145,10 +156,10 @@ namespace Umbraco.Cms.Infrastructure.Sync public string JsonIds { get; set; } /// - /// Gets or sets the number of Ids contained in the JsonIds json value + /// Gets or sets the number of Ids contained in the JsonIds json value. /// /// - /// This is used to determine the instruction count per row + /// This is used to determine the instruction count per row. /// public int JsonIdCount { get; set; } @@ -157,21 +168,31 @@ namespace Umbraco.Cms.Infrastructure.Sync /// public string JsonPayload { get; set; } - protected bool Equals(RefreshInstruction other) - { - return RefreshType == other.RefreshType + protected bool Equals(RefreshInstruction other) => + RefreshType == other.RefreshType && RefresherId.Equals(other.RefresherId) && GuidId.Equals(other.GuidId) && IntId == other.IntId && string.Equals(JsonIds, other.JsonIds) && string.Equals(JsonPayload, other.JsonPayload); - } public override bool Equals(object other) { - if (ReferenceEquals(null, other)) return false; - if (ReferenceEquals(this, other)) return true; - if (other.GetType() != this.GetType()) return false; + if (other is null) + { + return false; + } + + if (ReferenceEquals(this, other)) + { + return true; + } + + if (other.GetType() != GetType()) + { + return false; + } + return Equals((RefreshInstruction) other); } @@ -189,14 +210,8 @@ namespace Umbraco.Cms.Infrastructure.Sync } } - public static bool operator ==(RefreshInstruction left, RefreshInstruction right) - { - return Equals(left, right); - } + public static bool operator ==(RefreshInstruction left, RefreshInstruction right) => Equals(left, right); - public static bool operator !=(RefreshInstruction left, RefreshInstruction right) - { - return Equals(left, right) == false; - } + public static bool operator !=(RefreshInstruction left, RefreshInstruction right) => Equals(left, right) == false; } } diff --git a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Repositories.cs b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Repositories.cs index 8292fd2ecb..9b57e8586c 100644 --- a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Repositories.cs +++ b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Repositories.cs @@ -18,6 +18,7 @@ namespace Umbraco.Cms.Infrastructure.DependencyInjection // repositories builder.Services.AddUnique(); builder.Services.AddUnique(); + builder.Services.AddUnique(); builder.Services.AddUnique(); builder.Services.AddUnique(); builder.Services.AddUnique(); diff --git a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Services.cs b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Services.cs index 97e32451a5..30e8ae37f8 100644 --- a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Services.cs +++ b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Services.cs @@ -37,6 +37,7 @@ namespace Umbraco.Cms.Infrastructure.DependencyInjection builder.Services.AddUnique(); builder.Services.AddUnique(); builder.Services.AddUnique(); + builder.Services.AddUnique(); builder.Services.AddUnique(); builder.Services.AddUnique(); builder.Services.AddUnique(); diff --git a/src/Umbraco.Infrastructure/Persistence/Factories/CacheInstructionFactory.cs b/src/Umbraco.Infrastructure/Persistence/Factories/CacheInstructionFactory.cs new file mode 100644 index 0000000000..1a38348acf --- /dev/null +++ b/src/Umbraco.Infrastructure/Persistence/Factories/CacheInstructionFactory.cs @@ -0,0 +1,25 @@ +using System.Collections.Generic; +using System.Linq; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Infrastructure.Persistence.Dtos; + +namespace Umbraco.Cms.Infrastructure.Persistence.Factories +{ + internal static class CacheInstructionFactory + { + public static IEnumerable BuildEntities(IEnumerable dtos) => dtos.Select(BuildEntity).ToList(); + + public static CacheInstruction BuildEntity(CacheInstructionDto dto) => + new CacheInstruction(dto.Id, dto.UtcStamp, dto.Instructions, dto.OriginIdentity, dto.InstructionCount); + + public static CacheInstructionDto BuildDto(CacheInstruction entity) => + new CacheInstructionDto + { + Id = entity.Id, + UtcStamp = entity.UtcStamp, + Instructions = entity.Instructions, + OriginIdentity = entity.OriginIdentity, + InstructionCount = entity.InstructionCount, + }; + } +} diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/CacheInstructionRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/CacheInstructionRepository.cs new file mode 100644 index 0000000000..f5452b53c0 --- /dev/null +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/CacheInstructionRepository.cs @@ -0,0 +1,73 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using NPoco; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Persistence.Repositories; +using Umbraco.Cms.Core.Scoping; +using Umbraco.Cms.Infrastructure.Persistence.Dtos; +using Umbraco.Cms.Infrastructure.Persistence.Factories; +using Umbraco.Extensions; + +namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement +{ + /// + /// Represents the NPoco implementation of . + /// + internal class CacheInstructionRepository : ICacheInstructionRepository + { + private readonly IScopeAccessor _scopeAccessor; + + public CacheInstructionRepository(IScopeAccessor scopeAccessor) => _scopeAccessor = scopeAccessor; + + /// + private IScope AmbientScope => _scopeAccessor.AmbientScope; + + /// + public int CountAll() + { + Sql sql = AmbientScope.SqlContext.Sql().Select("COUNT(*)") + .From(); + + return AmbientScope.Database.ExecuteScalar(sql); + } + + /// + public int CountPendingInstructions(int lastId) => + AmbientScope.Database.ExecuteScalar("SELECT SUM(instructionCount) FROM umbracoCacheInstruction WHERE id > @lastId", new { lastId }); + + /// + public int GetMaxId() => + AmbientScope.Database.ExecuteScalar("SELECT MAX(id) FROM umbracoCacheInstruction"); + + /// + public bool Exists(int id) => AmbientScope.Database.Exists(id); + + /// + public void Add(CacheInstruction cacheInstruction) + { + CacheInstructionDto dto = CacheInstructionFactory.BuildDto(cacheInstruction); + AmbientScope.Database.Insert(dto); + } + + /// + public IEnumerable GetInstructions(int lastId, int maxNumberToRetrieve) + { + Sql sql = AmbientScope.SqlContext.Sql().SelectAll() + .From() + .Where(dto => dto.Id > lastId) + .OrderBy(dto => dto.Id); + Sql topSql = sql.SelectTop(maxNumberToRetrieve); + return AmbientScope.Database.Fetch(topSql).Select(CacheInstructionFactory.BuildEntity); + } + + /// + public void DeleteInstructionsOlderThan(DateTime pruneDate) + { + // Using 2 queries is faster than convoluted joins. + var maxId = AmbientScope.Database.ExecuteScalar("SELECT MAX(id) FROM umbracoCacheInstruction;"); + Sql deleteSql = new Sql().Append(@"DELETE FROM umbracoCacheInstruction WHERE utcStamp < @pruneDate AND id < @maxId", new { pruneDate, maxId }); + AmbientScope.Database.Execute(deleteSql); + } + } +} diff --git a/src/Umbraco.Infrastructure/Services/Implement/AuditService.cs b/src/Umbraco.Infrastructure/Services/Implement/AuditService.cs index cc8110f38e..05c489102f 100644 --- a/src/Umbraco.Infrastructure/Services/Implement/AuditService.cs +++ b/src/Umbraco.Infrastructure/Services/Implement/AuditService.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using Microsoft.Extensions.Logging; @@ -11,7 +11,7 @@ using Umbraco.Cms.Infrastructure.Persistence.Dtos; namespace Umbraco.Cms.Core.Services.Implement { - public sealed class AuditService : ScopeRepositoryService, IAuditService + public sealed class AuditService : RepositoryService, IAuditService { private readonly Lazy _isAvailable; private readonly IAuditRepository _auditRepository; diff --git a/src/Umbraco.Infrastructure/Services/Implement/CacheInstructionService.cs b/src/Umbraco.Infrastructure/Services/Implement/CacheInstructionService.cs new file mode 100644 index 0000000000..6ca01ccb50 --- /dev/null +++ b/src/Umbraco.Infrastructure/Services/Implement/CacheInstructionService.cs @@ -0,0 +1,501 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using Umbraco.Cms.Core.Cache; +using Umbraco.Cms.Core.Configuration.Models; +using Umbraco.Cms.Core.Events; +using Umbraco.Cms.Core.Logging; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Persistence.Repositories; +using Umbraco.Cms.Core.Scoping; +using Umbraco.Cms.Core.Sync; +using Umbraco.Extensions; + +namespace Umbraco.Cms.Core.Services.Implement +{ + /// + /// Implements providing a service for retrieving and saving cache instructions. + /// + public class CacheInstructionService : RepositoryService, ICacheInstructionService + { + private readonly IServerRoleAccessor _serverRoleAccessor; + private readonly CacheRefresherCollection _cacheRefreshers; + private readonly ICacheInstructionRepository _cacheInstructionRepository; + private readonly IProfilingLogger _profilingLogger; + private readonly ILogger _logger; + private readonly GlobalSettings _globalSettings; + + private readonly object _locko = new object(); + + /// + /// Initializes a new instance of the class. + /// + public CacheInstructionService( + IScopeProvider provider, + ILoggerFactory loggerFactory, + IEventMessagesFactory eventMessagesFactory, + IServerRoleAccessor serverRoleAccessor, + CacheRefresherCollection cacheRefreshers, + ICacheInstructionRepository cacheInstructionRepository, + IProfilingLogger profilingLogger, + ILogger logger, + IOptions globalSettings) + : base(provider, loggerFactory, eventMessagesFactory) + { + _serverRoleAccessor = serverRoleAccessor; + _cacheRefreshers = cacheRefreshers; + _cacheInstructionRepository = cacheInstructionRepository; + _profilingLogger = profilingLogger; + _logger = logger; + _globalSettings = globalSettings.Value; + } + + /// + public CacheInstructionServiceInitializationResult EnsureInitialized(bool released, int lastId) + { + using (IScope scope = ScopeProvider.CreateScope(autoComplete: true)) + { + lastId = EnsureInstructions(lastId); // reset _lastId if instructions are missing + return Initialize(released, lastId); // boot + } + } + + /// + /// Ensure that the last instruction that was processed is still in the database. + /// + /// + /// If the last instruction is not in the database anymore, then the messenger + /// should not try to process any instructions, because some instructions might be lost, + /// and it should instead cold-boot. + /// However, if the last synced instruction id is '0' and there are '0' records, then this indicates + /// that it's a fresh site and no user actions have taken place, in this circumstance we do not want to cold + /// boot. See: http://issues.umbraco.org/issue/U4-8627 + /// + private int EnsureInstructions(int lastId) + { + if (lastId == 0) + { + var count = _cacheInstructionRepository.CountAll(); + + // If there are instructions but we haven't synced, then a cold boot is necessary. + if (count > 0) + { + lastId = -1; + } + } + else + { + // If the last synced instruction is not found in the db, then a cold boot is necessary. + if (!_cacheInstructionRepository.Exists(lastId)) + { + lastId = -1; + } + } + + return lastId; + } + + /// + /// Initializes a server that has never synchronized before. + /// + /// + /// Thread safety: this is NOT thread safe. Because it is NOT meant to run multi-threaded. + /// Callers MUST ensure thread-safety. + /// + private CacheInstructionServiceInitializationResult Initialize(bool released, int lastId) + { + lock (_locko) + { + if (released) + { + return CacheInstructionServiceInitializationResult.AsUninitialized(); + } + + var coldboot = false; + + // Never synced before. + if (lastId < 0) + { + // We haven't synced - in this case we aren't going to sync the whole thing, we will assume this is a new + // server and it will need to rebuild it's own caches, e.g. Lucene or the XML cache file. + _logger.LogWarning("No last synced Id found, this generally means this is a new server/install." + + " The server will build its caches and indexes, and then adjust its last synced Id to the latest found in" + + " the database and maintain cache updates based on that Id."); + + coldboot = true; + } + else + { + // Check for how many instructions there are to process. Each row contains a count of the number of instructions contained in each + // row so we will sum these numbers to get the actual count. + var count = _cacheInstructionRepository.CountPendingInstructions(lastId); + if (count > _globalSettings.DatabaseServerMessenger.MaxProcessingInstructionCount) + { + // Too many instructions, proceed to cold boot + _logger.LogWarning( + "The instruction count ({InstructionCount}) exceeds the specified MaxProcessingInstructionCount ({MaxProcessingInstructionCount})." + + " The server will skip existing instructions, rebuild its caches and indexes entirely, adjust its last synced Id" + + " to the latest found in the database and maintain cache updates based on that Id.", + count, _globalSettings.DatabaseServerMessenger.MaxProcessingInstructionCount); + + coldboot = true; + } + } + + // If cold boot is required, go get the last id in the db and store it. + // Note: do it BEFORE initializing otherwise some instructions might get lost. + // When doing it before, some instructions might run twice - not an issue. + var maxId = coldboot + ? _cacheInstructionRepository.GetMaxId() + : 0; + + return CacheInstructionServiceInitializationResult.AsInitialized(coldboot, maxId, lastId); + } + } + + /// + public void DeliverInstructions(IEnumerable instructions, string localIdentity) + { + CacheInstruction entity = CreateCacheInstruction(instructions, localIdentity); + + using (IScope scope = ScopeProvider.CreateScope()) + { + _cacheInstructionRepository.Add(entity); + } + } + + /// + public void DeliverInstructionsInBatches(IEnumerable instructions, string localIdentity) + { + // Write the instructions but only create JSON blobs with a max instruction count equal to MaxProcessingInstructionCount. + using (IScope scope = ScopeProvider.CreateScope()) + { + foreach (IEnumerable instructionsBatch in instructions.InGroupsOf(_globalSettings.DatabaseServerMessenger.MaxProcessingInstructionCount)) + { + CacheInstruction entity = CreateCacheInstruction(instructionsBatch, localIdentity); + _cacheInstructionRepository.Add(entity); + } + + scope.Complete(); + } + } + + private CacheInstruction CreateCacheInstruction(IEnumerable instructions, string localIdentity) => + new CacheInstruction(0, DateTime.UtcNow, JsonConvert.SerializeObject(instructions, Formatting.None), localIdentity, instructions.Sum(x => x.JsonIdCount)); + + /// + public CacheInstructionServiceProcessInstructionsResult ProcessInstructions(bool released, string localIdentity, DateTime lastPruned) + { + using (_profilingLogger.DebugDuration("Syncing from database...")) + using (IScope scope = ScopeProvider.CreateScope()) + { + ProcessDatabaseInstructions(released, localIdentity, out int lastId); + + // Check for pruning throttling. + if (released || (DateTime.UtcNow - lastPruned) <= _globalSettings.DatabaseServerMessenger.TimeBetweenPruneOperations) + { + scope.Complete(); + return CacheInstructionServiceProcessInstructionsResult.AsCompleted(lastId); + } + + switch (_serverRoleAccessor.CurrentServerRole) + { + case ServerRole.Single: + case ServerRole.Master: + PruneOldInstructions(); + break; + } + + scope.Complete(); + return CacheInstructionServiceProcessInstructionsResult.AsCompletedAndPruned(lastId); + } + } + + /// + /// Process instructions from the database. + /// + /// + /// Thread safety: this is NOT thread safe. Because it is NOT meant to run multi-threaded. + /// + private void ProcessDatabaseInstructions(bool released, string localIdentity, out int lastId) + { + // NOTE: + // We 'could' recurse to ensure that no remaining instructions are pending in the table before proceeding but I don't think that + // would be a good idea since instructions could keep getting added and then all other threads will probably get stuck from serving requests + // (depending on what the cache refreshers are doing). I think it's best we do the one time check, process them and continue, if there are + // pending requests after being processed, they'll just be processed on the next poll. + // + // TODO: not true if we're running on a background thread, assuming we can? + + // Only retrieve the top 100 (just in case there are tons). + // Even though MaxProcessingInstructionCount is by default 1000 we still don't want to process that many + // rows in one request thread since each row can contain a ton of instructions (until 7.5.5 in which case + // a row can only contain MaxProcessingInstructionCount). + const int MaxInstructionsToRetrieve = 100; + + // Only process instructions coming from a remote server, and ignore instructions coming from + // the local server as they've already been processed. We should NOT assume that the sequence of + // instructions in the database makes any sense whatsoever, because it's all async. + + // Tracks which ones have already been processed to avoid duplicates + var processed = new HashSet(); + + // It would have been nice to do this in a Query instead of Fetch using a data reader to save + // some memory however we cannot do that because inside of this loop the cache refreshers are also + // performing some lookups which cannot be done with an active reader open. + lastId = 0; + foreach (CacheInstruction instruction in _cacheInstructionRepository.GetInstructions(lastId, MaxInstructionsToRetrieve)) + { + // If this flag gets set it means we're shutting down! In this case, we need to exit asap and cannot + // continue processing anything otherwise we'll hold up the app domain shutdown. + if (released) + { + break; + } + + if (instruction.OriginIdentity == localIdentity) + { + // Just skip that local one but update lastId nevertheless. + lastId = instruction.Id; + continue; + } + + // Deserialize remote instructions & skip if it fails. + if (!TryDeserializeInstructions(instruction, out JArray jsonInstructions)) + { + lastId = instruction.Id; // skip + continue; + } + + List instructionBatch = GetAllInstructions(jsonInstructions); + + // Process as per-normal. + var success = ProcessDatabaseInstructions(instructionBatch, instruction, processed, released, ref lastId); + + // If they couldn't be all processed (i.e. we're shutting down) then exit. + if (success == false) + { + _logger.LogInformation("The current batch of instructions was not processed, app is shutting down"); + break; + } + } + } + + /// + /// Attempts to deserialize the instructions to a JArray. + /// + private bool TryDeserializeInstructions(CacheInstruction instruction, out JArray jsonInstructions) + { + try + { + jsonInstructions = JsonConvert.DeserializeObject(instruction.Instructions); + return true; + } + catch (JsonException ex) + { + _logger.LogError(ex, "Failed to deserialize instructions ({DtoId}: '{DtoInstructions}').", + instruction.Id, + instruction.Instructions); + jsonInstructions = null; + return false; + } + } + + /// + /// Parses out the individual instructions to be processed. + /// + private static List GetAllInstructions(IEnumerable jsonInstructions) + { + var result = new List(); + foreach (JToken jsonItem in jsonInstructions) + { + // Could be a JObject in which case we can convert to a RefreshInstruction. + // Otherwise it could be another JArray - in which case we'll iterate that. + if (jsonItem is JObject jsonObj) + { + RefreshInstruction instruction = jsonObj.ToObject(); + result.Add(instruction); + } + else + { + var jsonInnerArray = (JArray)jsonItem; + result.AddRange(GetAllInstructions(jsonInnerArray)); // recurse + } + } + + return result; + } + + /// + /// Processes the instruction batch and checks for errors. + /// + /// + /// Tracks which instructions have already been processed to avoid duplicates + /// + /// Returns true if all instructions in the batch were processed, otherwise false if they could not be due to the app being shut down + /// + private bool ProcessDatabaseInstructions(IReadOnlyCollection instructionBatch, CacheInstruction instruction, HashSet processed, bool released, ref int lastId) + { + // Execute remote instructions & update lastId. + try + { + var result = NotifyRefreshers(instructionBatch, processed, released); + if (result) + { + // If all instructions were processed, set the last id. + lastId = instruction.Id; + } + + return result; + } + //catch (ThreadAbortException ex) + //{ + // //This will occur if the instructions processing is taking too long since this is occurring on a request thread. + // // Or possibly if IIS terminates the appdomain. In any case, we should deal with this differently perhaps... + //} + catch (Exception ex) + { + _logger.LogError( + ex, + "DISTRIBUTED CACHE IS NOT UPDATED. Failed to execute instructions ({DtoId}: '{DtoInstructions}'). Instruction is being skipped/ignored", + instruction.Id, + instruction.Instructions); + + // We cannot throw here because this invalid instruction will just keep getting processed over and over and errors + // will be thrown over and over. The only thing we can do is ignore and move on. + lastId = instruction.Id; + return false; + } + } + + /// + /// Executes the instructions against the cache refresher instances. + /// + /// + /// Returns true if all instructions were processed, otherwise false if the processing was interrupted (i.e. by app shutdown). + /// + private bool NotifyRefreshers(IEnumerable instructions, HashSet processed, bool released) + { + foreach (RefreshInstruction instruction in instructions) + { + // Check if the app is shutting down, we need to exit if this happens. + if (released) + { + return false; + } + + // This has already been processed. + if (processed.Contains(instruction)) + { + continue; + } + + switch (instruction.RefreshType) + { + case RefreshMethodType.RefreshAll: + RefreshAll(instruction.RefresherId); + break; + case RefreshMethodType.RefreshByGuid: + RefreshByGuid(instruction.RefresherId, instruction.GuidId); + break; + case RefreshMethodType.RefreshById: + RefreshById(instruction.RefresherId, instruction.IntId); + break; + case RefreshMethodType.RefreshByIds: + RefreshByIds(instruction.RefresherId, instruction.JsonIds); + break; + case RefreshMethodType.RefreshByJson: + RefreshByJson(instruction.RefresherId, instruction.JsonPayload); + break; + case RefreshMethodType.RemoveById: + RemoveById(instruction.RefresherId, instruction.IntId); + break; + } + + processed.Add(instruction); + } + + return true; + } + + private void RefreshAll(Guid uniqueIdentifier) + { + ICacheRefresher refresher = GetRefresher(uniqueIdentifier); + refresher.RefreshAll(); + } + + private void RefreshByGuid(Guid uniqueIdentifier, Guid id) + { + ICacheRefresher refresher = GetRefresher(uniqueIdentifier); + refresher.Refresh(id); + } + + private void RefreshById(Guid uniqueIdentifier, int id) + { + ICacheRefresher refresher = GetRefresher(uniqueIdentifier); + refresher.Refresh(id); + } + + private void RefreshByIds(Guid uniqueIdentifier, string jsonIds) + { + ICacheRefresher refresher = GetRefresher(uniqueIdentifier); + foreach (var id in JsonConvert.DeserializeObject(jsonIds)) + { + refresher.Refresh(id); + } + } + + private void RefreshByJson(Guid uniqueIdentifier, string jsonPayload) + { + IJsonCacheRefresher refresher = GetJsonRefresher(uniqueIdentifier); + refresher.Refresh(jsonPayload); + } + + private void RemoveById(Guid uniqueIdentifier, int id) + { + ICacheRefresher refresher = GetRefresher(uniqueIdentifier); + refresher.Remove(id); + } + + private ICacheRefresher GetRefresher(Guid id) + { + ICacheRefresher refresher = _cacheRefreshers[id]; + if (refresher == null) + { + throw new InvalidOperationException("Cache refresher with ID \"" + id + "\" does not exist."); + } + + return refresher; + } + + private IJsonCacheRefresher GetJsonRefresher(Guid id) => GetJsonRefresher(GetRefresher(id)); + + private static IJsonCacheRefresher GetJsonRefresher(ICacheRefresher refresher) + { + if (refresher is not IJsonCacheRefresher jsonRefresher) + { + throw new InvalidOperationException("Cache refresher with ID \"" + refresher.RefresherUniqueId + "\" does not implement " + typeof(IJsonCacheRefresher) + "."); + } + + return jsonRefresher; + } + + /// + /// Remove old instructions from the database + /// + /// + /// Always leave the last (most recent) record in the db table, this is so that not all instructions are removed which would cause + /// the site to cold boot if there's been no instruction activity for more than TimeToRetainInstructions. + /// See: http://issues.umbraco.org/issue/U4-7643#comment=67-25085 + /// + private void PruneOldInstructions() + { + DateTime pruneDate = DateTime.UtcNow - _globalSettings.DatabaseServerMessenger.TimeToRetainInstructions; + _cacheInstructionRepository.DeleteInstructionsOlderThan(pruneDate); + } + } +} diff --git a/src/Umbraco.Infrastructure/Services/Implement/ConsentService.cs b/src/Umbraco.Infrastructure/Services/Implement/ConsentService.cs index aebef075a5..b00a2579fd 100644 --- a/src/Umbraco.Infrastructure/Services/Implement/ConsentService.cs +++ b/src/Umbraco.Infrastructure/Services/Implement/ConsentService.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using Microsoft.Extensions.Logging; using Umbraco.Cms.Core.Events; @@ -11,7 +11,7 @@ namespace Umbraco.Cms.Core.Services.Implement /// /// Implements . /// - internal class ConsentService : ScopeRepositoryService, IConsentService + internal class ConsentService : RepositoryService, IConsentService { private readonly IConsentRepository _consentRepository; diff --git a/src/Umbraco.Infrastructure/Services/Implement/ContentTypeServiceBase.cs b/src/Umbraco.Infrastructure/Services/Implement/ContentTypeServiceBase.cs index 3886050432..d850e0c21a 100644 --- a/src/Umbraco.Infrastructure/Services/Implement/ContentTypeServiceBase.cs +++ b/src/Umbraco.Infrastructure/Services/Implement/ContentTypeServiceBase.cs @@ -1,10 +1,10 @@ -using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Scoping; namespace Umbraco.Cms.Core.Services.Implement { - public abstract class ContentTypeServiceBase : ScopeRepositoryService + public abstract class ContentTypeServiceBase : RepositoryService { protected ContentTypeServiceBase(IScopeProvider provider, ILoggerFactory loggerFactory, IEventMessagesFactory eventMessagesFactory) : base(provider, loggerFactory, eventMessagesFactory) diff --git a/src/Umbraco.Infrastructure/Services/Implement/DataTypeService.cs b/src/Umbraco.Infrastructure/Services/Implement/DataTypeService.cs index dacaa7e228..2284483c2d 100644 --- a/src/Umbraco.Infrastructure/Services/Implement/DataTypeService.cs +++ b/src/Umbraco.Infrastructure/Services/Implement/DataTypeService.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using Microsoft.Extensions.Logging; @@ -19,7 +19,7 @@ namespace Umbraco.Cms.Core.Services.Implement /// /// Represents the DataType Service, which is an easy access to operations involving /// - public class DataTypeService : ScopeRepositoryService, IDataTypeService + public class DataTypeService : RepositoryService, IDataTypeService { private readonly IDataTypeRepository _dataTypeRepository; private readonly IDataTypeContainerRepository _dataTypeContainerRepository; diff --git a/src/Umbraco.Infrastructure/Services/Implement/DomainService.cs b/src/Umbraco.Infrastructure/Services/Implement/DomainService.cs index ce0d2ee0ed..2b7d964a13 100644 --- a/src/Umbraco.Infrastructure/Services/Implement/DomainService.cs +++ b/src/Umbraco.Infrastructure/Services/Implement/DomainService.cs @@ -7,7 +7,7 @@ using Umbraco.Cms.Core.Scoping; namespace Umbraco.Cms.Core.Services.Implement { - public class DomainService : ScopeRepositoryService, IDomainService + public class DomainService : RepositoryService, IDomainService { private readonly IDomainRepository _domainRepository; diff --git a/src/Umbraco.Infrastructure/Services/Implement/EntityService.cs b/src/Umbraco.Infrastructure/Services/Implement/EntityService.cs index b7ff4f4d5f..0cbcc8d729 100644 --- a/src/Umbraco.Infrastructure/Services/Implement/EntityService.cs +++ b/src/Umbraco.Infrastructure/Services/Implement/EntityService.cs @@ -15,7 +15,7 @@ using Umbraco.Extensions; namespace Umbraco.Cms.Core.Services.Implement { - public class EntityService : ScopeRepositoryService, IEntityService + public class EntityService : RepositoryService, IEntityService { private readonly IEntityRepository _entityRepository; private readonly Dictionary _objectTypes; diff --git a/src/Umbraco.Infrastructure/Services/Implement/ExternalLoginService.cs b/src/Umbraco.Infrastructure/Services/Implement/ExternalLoginService.cs index bbec75338b..662259a093 100644 --- a/src/Umbraco.Infrastructure/Services/Implement/ExternalLoginService.cs +++ b/src/Umbraco.Infrastructure/Services/Implement/ExternalLoginService.cs @@ -8,7 +8,7 @@ using Umbraco.Cms.Core.Scoping; namespace Umbraco.Cms.Core.Services.Implement { - public class ExternalLoginService : ScopeRepositoryService, IExternalLoginService + public class ExternalLoginService : RepositoryService, IExternalLoginService { private readonly IExternalLoginRepository _externalLoginRepository; diff --git a/src/Umbraco.Infrastructure/Services/Implement/FileService.cs b/src/Umbraco.Infrastructure/Services/Implement/FileService.cs index 364454f876..67e0607292 100644 --- a/src/Umbraco.Infrastructure/Services/Implement/FileService.cs +++ b/src/Umbraco.Infrastructure/Services/Implement/FileService.cs @@ -19,7 +19,7 @@ namespace Umbraco.Cms.Core.Services.Implement /// /// Represents the File Service, which is an easy access to operations involving objects like Scripts, Stylesheets and Templates /// - public class FileService : ScopeRepositoryService, IFileService + public class FileService : RepositoryService, IFileService { private readonly IStylesheetRepository _stylesheetRepository; private readonly IScriptRepository _scriptRepository; diff --git a/src/Umbraco.Infrastructure/Services/Implement/LocalizationService.cs b/src/Umbraco.Infrastructure/Services/Implement/LocalizationService.cs index a88c883eef..abdda2e68c 100644 --- a/src/Umbraco.Infrastructure/Services/Implement/LocalizationService.cs +++ b/src/Umbraco.Infrastructure/Services/Implement/LocalizationService.cs @@ -13,7 +13,7 @@ namespace Umbraco.Cms.Core.Services.Implement /// /// Represents the Localization Service, which is an easy access to operations involving and /// - public class LocalizationService : ScopeRepositoryService, ILocalizationService + public class LocalizationService : RepositoryService, ILocalizationService { private readonly IDictionaryRepository _dictionaryRepository; private readonly ILanguageRepository _languageRepository; diff --git a/src/Umbraco.Infrastructure/Services/Implement/MacroService.cs b/src/Umbraco.Infrastructure/Services/Implement/MacroService.cs index 92308e671f..c42d29b3c0 100644 --- a/src/Umbraco.Infrastructure/Services/Implement/MacroService.cs +++ b/src/Umbraco.Infrastructure/Services/Implement/MacroService.cs @@ -12,7 +12,7 @@ namespace Umbraco.Cms.Core.Services.Implement /// /// Represents the Macro Service, which is an easy access to operations involving /// - public class MacroService : ScopeRepositoryService, IMacroService + public class MacroService : RepositoryService, IMacroService { private readonly IMacroRepository _macroRepository; private readonly IAuditRepository _auditRepository; diff --git a/src/Umbraco.Infrastructure/Services/Implement/MediaService.cs b/src/Umbraco.Infrastructure/Services/Implement/MediaService.cs index 60061ed9bf..04e4008da2 100644 --- a/src/Umbraco.Infrastructure/Services/Implement/MediaService.cs +++ b/src/Umbraco.Infrastructure/Services/Implement/MediaService.cs @@ -20,7 +20,7 @@ namespace Umbraco.Cms.Core.Services.Implement /// /// Represents the Media Service, which is an easy access to operations involving /// - public class MediaService : ScopeRepositoryService, IMediaService + public class MediaService : RepositoryService, IMediaService { private readonly IMediaRepository _mediaRepository; private readonly IMediaTypeRepository _mediaTypeRepository; diff --git a/src/Umbraco.Infrastructure/Services/Implement/MemberService.cs b/src/Umbraco.Infrastructure/Services/Implement/MemberService.cs index 96ba494790..38b70af19c 100644 --- a/src/Umbraco.Infrastructure/Services/Implement/MemberService.cs +++ b/src/Umbraco.Infrastructure/Services/Implement/MemberService.cs @@ -16,7 +16,7 @@ namespace Umbraco.Cms.Core.Services.Implement /// /// Represents the MemberService. /// - public class MemberService : ScopeRepositoryService, IMemberService + public class MemberService : RepositoryService, IMemberService { private readonly IMemberRepository _memberRepository; private readonly IMemberTypeRepository _memberTypeRepository; diff --git a/src/Umbraco.Infrastructure/Services/Implement/PublicAccessService.cs b/src/Umbraco.Infrastructure/Services/Implement/PublicAccessService.cs index 4c8615f442..72e7873a7c 100644 --- a/src/Umbraco.Infrastructure/Services/Implement/PublicAccessService.cs +++ b/src/Umbraco.Infrastructure/Services/Implement/PublicAccessService.cs @@ -10,7 +10,7 @@ using Umbraco.Extensions; namespace Umbraco.Cms.Core.Services.Implement { - public class PublicAccessService : ScopeRepositoryService, IPublicAccessService + public class PublicAccessService : RepositoryService, IPublicAccessService { private readonly IPublicAccessRepository _publicAccessRepository; diff --git a/src/Umbraco.Infrastructure/Services/Implement/RedirectUrlService.cs b/src/Umbraco.Infrastructure/Services/Implement/RedirectUrlService.cs index d2ea10df49..a3faf64081 100644 --- a/src/Umbraco.Infrastructure/Services/Implement/RedirectUrlService.cs +++ b/src/Umbraco.Infrastructure/Services/Implement/RedirectUrlService.cs @@ -8,7 +8,7 @@ using Umbraco.Cms.Core.Scoping; namespace Umbraco.Cms.Core.Services.Implement { - internal class RedirectUrlService : ScopeRepositoryService, IRedirectUrlService + internal class RedirectUrlService : RepositoryService, IRedirectUrlService { private readonly IRedirectUrlRepository _redirectUrlRepository; diff --git a/src/Umbraco.Infrastructure/Services/Implement/RelationService.cs b/src/Umbraco.Infrastructure/Services/Implement/RelationService.cs index b83d3f286c..19fb68ae8c 100644 --- a/src/Umbraco.Infrastructure/Services/Implement/RelationService.cs +++ b/src/Umbraco.Infrastructure/Services/Implement/RelationService.cs @@ -11,7 +11,7 @@ using Umbraco.Extensions; namespace Umbraco.Cms.Core.Services.Implement { - public class RelationService : ScopeRepositoryService, IRelationService + public class RelationService : RepositoryService, IRelationService { private readonly IEntityService _entityService; private readonly IRelationRepository _relationRepository; diff --git a/src/Umbraco.Infrastructure/Services/Implement/ScopeRepositoryService.cs b/src/Umbraco.Infrastructure/Services/Implement/ScopeRepositoryService.cs deleted file mode 100644 index ca8e074b04..0000000000 --- a/src/Umbraco.Infrastructure/Services/Implement/ScopeRepositoryService.cs +++ /dev/null @@ -1,14 +0,0 @@ -using Microsoft.Extensions.Logging; -using Umbraco.Cms.Core.Events; -using Umbraco.Cms.Core.Scoping; - -namespace Umbraco.Cms.Core.Services.Implement -{ - // TODO: that one does not add anything = kill - public abstract class ScopeRepositoryService : RepositoryService - { - protected ScopeRepositoryService(IScopeProvider provider, ILoggerFactory loggerFactory, IEventMessagesFactory eventMessagesFactory) - : base(provider, loggerFactory, eventMessagesFactory) - { } - } -} diff --git a/src/Umbraco.Infrastructure/Services/Implement/ServerRegistrationService.cs b/src/Umbraco.Infrastructure/Services/Implement/ServerRegistrationService.cs index 75466c2013..9c03f9aabc 100644 --- a/src/Umbraco.Infrastructure/Services/Implement/ServerRegistrationService.cs +++ b/src/Umbraco.Infrastructure/Services/Implement/ServerRegistrationService.cs @@ -16,7 +16,7 @@ namespace Umbraco.Cms.Core.Services.Implement /// /// Manages server registrations in the database. /// - public sealed class ServerRegistrationService : ScopeRepositoryService, IServerRegistrationService + public sealed class ServerRegistrationService : RepositoryService, IServerRegistrationService { private readonly IServerRegistrationRepository _serverRegistrationRepository; private readonly IHostingEnvironment _hostingEnvironment; diff --git a/src/Umbraco.Infrastructure/Services/Implement/TagService.cs b/src/Umbraco.Infrastructure/Services/Implement/TagService.cs index 2d2cf082c6..907af05ab2 100644 --- a/src/Umbraco.Infrastructure/Services/Implement/TagService.cs +++ b/src/Umbraco.Infrastructure/Services/Implement/TagService.cs @@ -14,7 +14,7 @@ namespace Umbraco.Cms.Core.Services.Implement /// /// If there is unpublished content with tags, those tags will not be contained /// - public class TagService : ScopeRepositoryService, ITagService + public class TagService : RepositoryService, ITagService { private readonly ITagRepository _tagRepository; diff --git a/src/Umbraco.Infrastructure/Services/Implement/UserService.cs b/src/Umbraco.Infrastructure/Services/Implement/UserService.cs index 751581e068..d35bcbfa50 100644 --- a/src/Umbraco.Infrastructure/Services/Implement/UserService.cs +++ b/src/Umbraco.Infrastructure/Services/Implement/UserService.cs @@ -21,7 +21,7 @@ namespace Umbraco.Cms.Core.Services.Implement /// /// Represents the UserService, which is an easy access to operations involving , and eventually Backoffice Users. /// - public class UserService : ScopeRepositoryService, IUserService + public class UserService : RepositoryService, IUserService { private readonly IUserRepository _userRepository; private readonly IUserGroupRepository _userGroupRepository; diff --git a/src/Umbraco.Infrastructure/Sync/BatchedDatabaseServerMessenger.cs b/src/Umbraco.Infrastructure/Sync/BatchedDatabaseServerMessenger.cs index 96bdeea82d..940ebfe0cd 100644 --- a/src/Umbraco.Infrastructure/Sync/BatchedDatabaseServerMessenger.cs +++ b/src/Umbraco.Infrastructure/Sync/BatchedDatabaseServerMessenger.cs @@ -3,17 +3,14 @@ using System.Collections.Generic; using System.Linq; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -using Newtonsoft.Json; using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Hosting; -using Umbraco.Cms.Core.Logging; using Umbraco.Cms.Core.Runtime; -using Umbraco.Cms.Core.Scoping; +using Umbraco.Cms.Core.Serialization; +using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Sync; using Umbraco.Cms.Core.Web; -using Umbraco.Cms.Infrastructure.Persistence.Dtos; -using Umbraco.Extensions; namespace Umbraco.Cms.Infrastructure.Sync { @@ -30,17 +27,15 @@ namespace Umbraco.Cms.Infrastructure.Sync /// public BatchedDatabaseServerMessenger( IMainDom mainDom, - IScopeProvider scopeProvider, - IProfilingLogger proflog, ILogger logger, - IServerRoleAccessor serverRegistrar, DatabaseServerMessengerCallbacks callbacks, IHostingEnvironment hostingEnvironment, - CacheRefresherCollection cacheRefreshers, + ICacheInstructionService cacheInstructionService, + IJsonSerializer jsonSerializer, IRequestCache requestCache, IRequestAccessor requestAccessor, IOptions globalSettings) - : base(mainDom, scopeProvider, proflog, logger, serverRegistrar, true, callbacks, hostingEnvironment, cacheRefreshers, globalSettings) + : base(mainDom, logger, true, callbacks, hostingEnvironment, cacheInstructionService, jsonSerializer, globalSettings) { _requestCache = requestCache; _requestAccessor = requestAccessor; @@ -71,28 +66,7 @@ namespace Umbraco.Cms.Infrastructure.Sync RefreshInstruction[] instructions = batch.SelectMany(x => x.Instructions).ToArray(); batch.Clear(); - // Write the instructions but only create JSON blobs with a max instruction count equal to MaxProcessingInstructionCount - using (IScope scope = ScopeProvider.CreateScope()) - { - foreach (IEnumerable instructionsBatch in instructions.InGroupsOf(GlobalSettings.DatabaseServerMessenger.MaxProcessingInstructionCount)) - { - WriteInstructions(scope, instructionsBatch); - } - - scope.Complete(); - } - } - - private void WriteInstructions(IScope scope, IEnumerable instructions) - { - var dto = new CacheInstructionDto - { - UtcStamp = DateTime.UtcNow, - Instructions = JsonConvert.SerializeObject(instructions, Formatting.None), - OriginIdentity = LocalIdentity, - InstructionCount = instructions.Sum(x => x.JsonIdCount) - }; - scope.Database.Insert(dto); + CacheInstructionService.DeliverInstructionsInBatches(instructions, LocalIdentity); } private ICollection GetBatch(bool create) @@ -104,7 +78,7 @@ namespace Umbraco.Cms.Infrastructure.Sync return null; } - // no thread-safety here because it'll run in only 1 thread (request) at a time + // No thread-safety here because it'll run in only 1 thread (request) at a time. var batch = (ICollection)_requestCache.Get(key); if (batch == null && create) { @@ -123,27 +97,17 @@ namespace Umbraco.Cms.Infrastructure.Sync string json = null) { ICollection batch = GetBatch(true); - IEnumerable instructions = RefreshInstruction.GetInstructions(refresher, messageType, ids, idType, json); + IEnumerable instructions = RefreshInstruction.GetInstructions(refresher, JsonSerializer, messageType, ids, idType, json); - // batch if we can, else write to DB immediately + // Batch if we can, else write to DB immediately. if (batch == null) { - // only write the json blob with a maximum count of the MaxProcessingInstructionCount - using (IScope scope = ScopeProvider.CreateScope()) - { - foreach (IEnumerable maxBatch in instructions.InGroupsOf(GlobalSettings.DatabaseServerMessenger.MaxProcessingInstructionCount)) - { - WriteInstructions(scope, maxBatch); - } - - scope.Complete(); - } + CacheInstructionService.DeliverInstructionsInBatches(instructions, LocalIdentity); } else { batch.Add(new RefreshInstructionEnvelope(refresher, instructions)); } - } } } diff --git a/src/Umbraco.Infrastructure/Sync/DatabaseServerMessenger.cs b/src/Umbraco.Infrastructure/Sync/DatabaseServerMessenger.cs index 92bd732246..7d1ef9f360 100644 --- a/src/Umbraco.Infrastructure/Sync/DatabaseServerMessenger.cs +++ b/src/Umbraco.Infrastructure/Sync/DatabaseServerMessenger.cs @@ -7,19 +7,14 @@ using System.Linq; using System.Threading; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; -using NPoco; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Hosting; -using Umbraco.Cms.Core.Logging; using Umbraco.Cms.Core.Runtime; -using Umbraco.Cms.Core.Scoping; +using Umbraco.Cms.Core.Serialization; +using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Sync; -using Umbraco.Cms.Infrastructure.Persistence; -using Umbraco.Cms.Infrastructure.Persistence.Dtos; using Umbraco.Extensions; namespace Umbraco.Cms.Infrastructure.Sync @@ -29,8 +24,6 @@ namespace Umbraco.Cms.Infrastructure.Sync /// public abstract class DatabaseServerMessenger : ServerMessengerBase { - // TODO: This class needs to be split into a service/repo for DB access - /* * this messenger writes ALL instructions to the database, * but only processes instructions coming from remote servers, @@ -40,10 +33,7 @@ namespace Umbraco.Cms.Infrastructure.Sync private readonly IMainDom _mainDom; private readonly ManualResetEvent _syncIdle; private readonly object _locko = new object(); - private readonly IProfilingLogger _profilingLogger; - private readonly IServerRoleAccessor _serverRegistrar; private readonly IHostingEnvironment _hostingEnvironment; - private readonly CacheRefresherCollection _cacheRefreshers; private readonly Lazy _distCacheFilePath; private int _lastId = -1; @@ -58,33 +48,29 @@ namespace Umbraco.Cms.Infrastructure.Sync /// protected DatabaseServerMessenger( IMainDom mainDom, - IScopeProvider scopeProvider, - IProfilingLogger proflog, ILogger logger, - IServerRoleAccessor serverRegistrar, bool distributedEnabled, DatabaseServerMessengerCallbacks callbacks, IHostingEnvironment hostingEnvironment, - CacheRefresherCollection cacheRefreshers, + ICacheInstructionService cacheInstructionService, + IJsonSerializer jsonSerializer, IOptions globalSettings) : base(distributedEnabled) { - ScopeProvider = scopeProvider ?? throw new ArgumentNullException(nameof(scopeProvider)); _mainDom = mainDom; - _profilingLogger = proflog ?? throw new ArgumentNullException(nameof(proflog)); - _serverRegistrar = serverRegistrar; _hostingEnvironment = hostingEnvironment; - _cacheRefreshers = cacheRefreshers; Logger = logger; Callbacks = callbacks ?? throw new ArgumentNullException(nameof(callbacks)); + CacheInstructionService = cacheInstructionService; + JsonSerializer = jsonSerializer; GlobalSettings = globalSettings.Value; _lastPruned = _lastSync = DateTime.UtcNow; _syncIdle = new ManualResetEvent(true); _distCacheFilePath = new Lazy(() => GetDistCacheFilePath(hostingEnvironment)); - // See notes on LocalIdentity + // See notes on _localIdentity LocalIdentity = NetworkHelper.MachineName // eg DOMAIN\SERVER - + "/" + _hostingEnvironment.ApplicationId // eg /LM/S3SVC/11/ROOT + + "/" + hostingEnvironment.ApplicationId // eg /LM/S3SVC/11/ROOT + " [P" + Process.GetCurrentProcess().Id // eg 1234 + "/D" + AppDomain.CurrentDomain.Id // eg 22 + "] " + Guid.NewGuid().ToString("N").ToUpper(); // make it truly unique @@ -92,21 +78,33 @@ namespace Umbraco.Cms.Infrastructure.Sync _initialized = new Lazy(EnsureInitialized); } + private string DistCacheFilePath => _distCacheFilePath.Value; + public DatabaseServerMessengerCallbacks Callbacks { get; } public GlobalSettings GlobalSettings { get; } protected ILogger Logger { get; } - protected IScopeProvider ScopeProvider { get; } + protected ICacheInstructionService CacheInstructionService { get; } - protected Sql Sql() => ScopeProvider.SqlContext.Sql(); + protected IJsonSerializer JsonSerializer { get; } - private string DistCacheFilePath => _distCacheFilePath.Value; + /// + /// Gets the unique local identity of the executing AppDomain. + /// + /// + /// It is not only about the "server" (machine name and appDomainappId), but also about + /// an AppDomain, within a Process, on that server - because two AppDomains running at the same + /// time on the same server (eg during a restart) are, practically, a LB setup. + /// Practically, all we really need is the guid, the other infos are here for information + /// and debugging purposes. + /// + protected string LocalIdentity { get; } #region Messenger - // we don't care if there's servers listed or not, + // we don't care if there are servers listed or not, // if distributed call is enabled we will make the call protected override bool RequiresDistributed(ICacheRefresher refresher, MessageType dispatchType) => _initialized.Value && DistributedEnabled; @@ -119,26 +117,14 @@ namespace Umbraco.Cms.Infrastructure.Sync { var idsA = ids?.ToArray(); - if (GetArrayType(idsA, out var idType) == false) + if (GetArrayType(idsA, out Type idType) == false) { throw new ArgumentException("All items must be of the same type, either int or Guid.", nameof(ids)); } - var instructions = RefreshInstruction.GetInstructions(refresher, messageType, idsA, idType, json); + IEnumerable instructions = RefreshInstruction.GetInstructions(refresher, JsonSerializer, messageType, idsA, idType, json); - var dto = new CacheInstructionDto - { - UtcStamp = DateTime.UtcNow, - Instructions = JsonConvert.SerializeObject(instructions, Formatting.None), - OriginIdentity = LocalIdentity, - InstructionCount = instructions.Sum(x => x.JsonIdCount) - }; - - using (var scope = ScopeProvider.CreateScope()) - { - scope.Database.Insert(dto); - scope.Complete(); - } + CacheInstructionService.DeliverInstructions(instructions, LocalIdentity); } #endregion @@ -151,7 +137,7 @@ namespace Umbraco.Cms.Infrastructure.Sync private bool EnsureInitialized() { // weight:10, must release *before* the published snapshot service, because once released - // the service will *not* be able to properly handle our notifications anymore + // the service will *not* be able to properly handle our notifications anymore. const int weight = 10; var registered = _mainDom.Register( @@ -162,11 +148,11 @@ namespace Umbraco.Cms.Infrastructure.Sync _released = true; // no more syncs } - // wait a max of 5 seconds and then return, so that we don't block + // Wait a max of 5 seconds and then return, so that we don't block // the entire MainDom callbacks chain and prevent the AppDomain from // properly releasing MainDom - a timeout here means that one refresher // is taking too much time processing, however when it's done we will - // not update lastId and stop everything + // not update lastId and stop everything. var idle = _syncIdle.WaitOne(5000); if (idle == false) { @@ -182,86 +168,28 @@ namespace Umbraco.Cms.Infrastructure.Sync ReadLastSynced(); // get _lastId - using (var scope = ScopeProvider.CreateScope(autoComplete: true)) + CacheInstructionServiceInitializationResult result = CacheInstructionService.EnsureInitialized(_released, _lastId); + + if (result.ColdBootRequired) { - EnsureInstructions(scope.Database); // reset _lastId if instructions are missing - return Initialize(scope.Database); // boot + // If there is a max currently, or if we've never synced. + if (result.MaxId > 0 || result.LastId < 0) + { + SaveLastSynced(result.MaxId); + } + + // Execute initializing callbacks. + if (Callbacks.InitializingCallbacks != null) + { + foreach (Action callback in Callbacks.InitializingCallbacks) + { + callback(); + } + } } - } - /// - /// Initializes a server that has never synchronized before. - /// - /// - /// Thread safety: this is NOT thread safe. Because it is NOT meant to run multi-threaded. - /// Callers MUST ensure thread-safety. - /// - private bool Initialize(IUmbracoDatabase database) - { - lock (_locko) - { - if (_released) - { - return false; - } - - var coldboot = false; - - // never synced before - if (_lastId < 0) - { - // we haven't synced - in this case we aren't going to sync the whole thing, we will assume this is a new - // server and it will need to rebuild it's own caches, eg Lucene or the xml cache file. - Logger.LogWarning("No last synced Id found, this generally means this is a new server/install." - + " The server will build its caches and indexes, and then adjust its last synced Id to the latest found in" - + " the database and maintain cache updates based on that Id."); - - coldboot = true; - } - else - { - // check for how many instructions there are to process, each row contains a count of the number of instructions contained in each - // row so we will sum these numbers to get the actual count. - var count = database.ExecuteScalar("SELECT SUM(instructionCount) FROM umbracoCacheInstruction WHERE id > @lastId", new { lastId = _lastId }); - if (count > GlobalSettings.DatabaseServerMessenger.MaxProcessingInstructionCount) - { - // too many instructions, proceed to cold boot - Logger.LogWarning( - "The instruction count ({InstructionCount}) exceeds the specified MaxProcessingInstructionCount ({MaxProcessingInstructionCount})." - + " The server will skip existing instructions, rebuild its caches and indexes entirely, adjust its last synced Id" - + " to the latest found in the database and maintain cache updates based on that Id.", - count, GlobalSettings.DatabaseServerMessenger.MaxProcessingInstructionCount); - - coldboot = true; - } - } - - if (coldboot) - { - // go get the last id in the db and store it - // note: do it BEFORE initializing otherwise some instructions might get lost - // when doing it before, some instructions might run twice - not an issue - var maxId = database.ExecuteScalar("SELECT MAX(id) FROM umbracoCacheInstruction"); - - // if there is a max currently, or if we've never synced - if (maxId > 0 || _lastId < 0) - { - SaveLastSynced(maxId); - } - - // execute initializing callbacks - if (Callbacks.InitializingCallbacks != null) - { - foreach (var callback in Callbacks.InitializingCallbacks) - { - callback(); - } - } - } - - return true; - } - } + return result.Initialized; + } /// /// Synchronize the server (throttled). @@ -299,29 +227,15 @@ namespace Umbraco.Cms.Infrastructure.Sync try { - using (_profilingLogger.DebugDuration("Syncing from database...")) - using (var scope = ScopeProvider.CreateScope()) + CacheInstructionServiceProcessInstructionsResult result = CacheInstructionService.ProcessInstructions(_released, LocalIdentity, _lastPruned); + if (result.InstructionsWerePruned) { - ProcessDatabaseInstructions(scope.Database); - - // Check for pruning throttling - if (_released || (DateTime.UtcNow - _lastPruned) <= GlobalSettings.DatabaseServerMessenger.TimeBetweenPruneOperations) - { - scope.Complete(); - return; - } - _lastPruned = _lastSync; + } - switch (_serverRegistrar.CurrentServerRole) - { - case ServerRole.Single: - case ServerRole.Master: - PruneOldInstructions(scope.Database); - break; - } - - scope.Complete(); + if (result.LastId > 0) + { + SaveLastSynced(result.LastId); } } finally @@ -334,206 +248,8 @@ namespace Umbraco.Cms.Infrastructure.Sync _syncIdle.Set(); } - } - - /// - /// Process instructions from the database. - /// - /// - /// Thread safety: this is NOT thread safe. Because it is NOT meant to run multi-threaded. - /// - private void ProcessDatabaseInstructions(IUmbracoDatabase database) - { - // NOTE - // we 'could' recurse to ensure that no remaining instructions are pending in the table before proceeding but I don't think that - // would be a good idea since instructions could keep getting added and then all other threads will probably get stuck from serving requests - // (depending on what the cache refreshers are doing). I think it's best we do the one time check, process them and continue, if there are - // pending requests after being processed, they'll just be processed on the next poll. - // - // TODO: not true if we're running on a background thread, assuming we can? - - var sql = Sql().SelectAll() - .From() - .Where(dto => dto.Id > _lastId) - .OrderBy(dto => dto.Id); - - // only retrieve the top 100 (just in case there's tons) - // even though MaxProcessingInstructionCount is by default 1000 we still don't want to process that many - // rows in one request thread since each row can contain a ton of instructions (until 7.5.5 in which case - // a row can only contain MaxProcessingInstructionCount) - var topSql = sql.SelectTop(100); - - // only process instructions coming from a remote server, and ignore instructions coming from - // the local server as they've already been processed. We should NOT assume that the sequence of - // instructions in the database makes any sense whatsoever, because it's all async. - var localIdentity = LocalIdentity; - - var lastId = 0; - - // tracks which ones have already been processed to avoid duplicates - var processed = new HashSet(); - - // It would have been nice to do this in a Query instead of Fetch using a data reader to save - // some memory however we cannot do that because inside of this loop the cache refreshers are also - // performing some lookups which cannot be done with an active reader open - foreach (var dto in database.Fetch(topSql)) - { - // If this flag gets set it means we're shutting down! In this case, we need to exit asap and cannot - // continue processing anything otherwise we'll hold up the app domain shutdown - if (_released) - { - break; - } - - if (dto.OriginIdentity == localIdentity) - { - // just skip that local one but update lastId nevertheless - lastId = dto.Id; - continue; - } - - // deserialize remote instructions & skip if it fails - JArray jsonA; - try - { - jsonA = JsonConvert.DeserializeObject(dto.Instructions); - } - catch (JsonException ex) - { - Logger.LogError(ex, "Failed to deserialize instructions ({DtoId}: '{DtoInstructions}').", - dto.Id, - dto.Instructions); - - lastId = dto.Id; // skip - continue; - } - - var instructionBatch = GetAllInstructions(jsonA); - - // process as per-normal - var success = ProcessDatabaseInstructions(instructionBatch, dto, processed, ref lastId); - - // if they couldn't be all processed (i.e. we're shutting down) then exit - if (success == false) - { - Logger.LogInformation("The current batch of instructions was not processed, app is shutting down"); - break; - } - - } - - if (lastId > 0) - SaveLastSynced(lastId); - } - - /// - /// Processes the instruction batch and checks for errors - /// - /// - /// - /// - /// Tracks which instructions have already been processed to avoid duplicates - /// - /// - /// - /// returns true if all instructions in the batch were processed, otherwise false if they could not be due to the app being shut down - /// - private bool ProcessDatabaseInstructions(IReadOnlyCollection instructionBatch, CacheInstructionDto dto, HashSet processed, ref int lastId) - { - // execute remote instructions & update lastId - try - { - var result = NotifyRefreshers(instructionBatch, processed); - if (result) - { - //if all instructions we're processed, set the last id - lastId = dto.Id; - } - return result; - } - //catch (ThreadAbortException ex) - //{ - // //This will occur if the instructions processing is taking too long since this is occurring on a request thread. - // // Or possibly if IIS terminates the appdomain. In any case, we should deal with this differently perhaps... - //} - catch (Exception ex) - { - Logger.LogError( - ex, - "DISTRIBUTED CACHE IS NOT UPDATED. Failed to execute instructions ({DtoId}: '{DtoInstructions}'). Instruction is being skipped/ignored", - dto.Id, - dto.Instructions); - - //we cannot throw here because this invalid instruction will just keep getting processed over and over and errors - // will be thrown over and over. The only thing we can do is ignore and move on. - lastId = dto.Id; - return false; - } - - ////if this is returned it will not be saved - //return -1; - } - - /// - /// Remove old instructions from the database - /// - /// - /// Always leave the last (most recent) record in the db table, this is so that not all instructions are removed which would cause - /// the site to cold boot if there's been no instruction activity for more than DaysToRetainInstructions. - /// See: http://issues.umbraco.org/issue/U4-7643#comment=67-25085 - /// - private void PruneOldInstructions(IUmbracoDatabase database) - { - var pruneDate = DateTime.UtcNow - GlobalSettings.DatabaseServerMessenger.TimeToRetainInstructions; - - // using 2 queries is faster than convoluted joins - - var maxId = database.ExecuteScalar("SELECT MAX(id) FROM umbracoCacheInstruction;"); - - var delete = new Sql().Append(@"DELETE FROM umbracoCacheInstruction WHERE utcStamp < @pruneDate AND id < @maxId", - new { pruneDate, maxId }); - - database.Execute(delete); - } - - /// - /// Ensure that the last instruction that was processed is still in the database. - /// - /// - /// If the last instruction is not in the database anymore, then the messenger - /// should not try to process any instructions, because some instructions might be lost, - /// and it should instead cold-boot. - /// However, if the last synced instruction id is '0' and there are '0' records, then this indicates - /// that it's a fresh site and no user actions have taken place, in this circumstance we do not want to cold - /// boot. See: http://issues.umbraco.org/issue/U4-8627 - /// - private void EnsureInstructions(IUmbracoDatabase database) - { - if (_lastId == 0) - { - var sql = Sql().Select("COUNT(*)") - .From(); - - var count = database.ExecuteScalar(sql); - - //if there are instructions but we haven't synced, then a cold boot is necessary - if (count > 0) - _lastId = -1; - } - else - { - var sql = Sql().SelectAll() - .From() - .Where(dto => dto.Id == _lastId); - - var dtos = database.Fetch(sql); - - //if the last synced instruction is not found in the db, then a cold boot is necessary - if (dtos.Count == 0) - _lastId = -1; - } - } - + } + /// /// Reads the last-synced id from file into memory. /// @@ -543,11 +259,15 @@ namespace Umbraco.Cms.Infrastructure.Sync private void ReadLastSynced() { if (File.Exists(DistCacheFilePath) == false) + { return; + } var content = File.ReadAllText(DistCacheFilePath); if (int.TryParse(content, out var last)) + { _lastId = last; + } } /// @@ -563,18 +283,6 @@ namespace Umbraco.Cms.Infrastructure.Sync _lastId = id; } - /// - /// Gets the unique local identity of the executing AppDomain. - /// - /// - /// It is not only about the "server" (machine name and appDomainappId), but also about - /// an AppDomain, within a Process, on that server - because two AppDomains running at the same - /// time on the same server (eg during a restart) are, practically, a LB setup. - /// Practically, all we really need is the guid, the other infos are here for information - /// and debugging purposes. - /// - protected string LocalIdentity { get; } - private string GetDistCacheFilePath(IHostingEnvironment hostingEnvironment) { var fileName = _hostingEnvironment.ApplicationId.ReplaceNonAlphanumericChars(string.Empty) + "-lastsynced.txt"; @@ -584,151 +292,18 @@ namespace Umbraco.Cms.Infrastructure.Sync //ensure the folder exists var folder = Path.GetDirectoryName(distCacheFilePath); if (folder == null) + { throw new InvalidOperationException("The folder could not be determined for the file " + distCacheFilePath); + } + if (Directory.Exists(folder) == false) + { Directory.CreateDirectory(folder); + } return distCacheFilePath; } #endregion - - #region Notify refreshers - - private ICacheRefresher GetRefresher(Guid id) - { - var refresher = _cacheRefreshers[id]; - if (refresher == null) - throw new InvalidOperationException("Cache refresher with ID \"" + id + "\" does not exist."); - return refresher; - } - - private IJsonCacheRefresher GetJsonRefresher(Guid id) - { - return GetJsonRefresher(GetRefresher(id)); - } - - private static IJsonCacheRefresher GetJsonRefresher(ICacheRefresher refresher) - { - var jsonRefresher = refresher as IJsonCacheRefresher; - if (jsonRefresher == null) - throw new InvalidOperationException("Cache refresher with ID \"" + refresher.RefresherUniqueId + "\" does not implement " + typeof(IJsonCacheRefresher) + "."); - return jsonRefresher; - } - - /// - /// Parses out the individual instructions to be processed - /// - /// - /// - private static List GetAllInstructions(IEnumerable jsonArray) - { - var result = new List(); - foreach (var jsonItem in jsonArray) - { - // could be a JObject in which case we can convert to a RefreshInstruction, - // otherwise it could be another JArray - in which case we'll iterate that. - var jsonObj = jsonItem as JObject; - if (jsonObj != null) - { - var instruction = jsonObj.ToObject(); - result.Add(instruction); - } - else - { - var jsonInnerArray = (JArray)jsonItem; - result.AddRange(GetAllInstructions(jsonInnerArray)); // recurse - } - } - return result; - } - - /// - /// executes the instructions against the cache refresher instances - /// - /// - /// - /// - /// Returns true if all instructions were processed, otherwise false if the processing was interrupted (i.e. app shutdown) - /// - private bool NotifyRefreshers(IEnumerable instructions, HashSet processed) - { - foreach (var instruction in instructions) - { - //Check if the app is shutting down, we need to exit if this happens. - if (_released) - { - return false; - } - - //this has already been processed - if (processed.Contains(instruction)) - continue; - - switch (instruction.RefreshType) - { - case RefreshMethodType.RefreshAll: - RefreshAll(instruction.RefresherId); - break; - case RefreshMethodType.RefreshByGuid: - RefreshByGuid(instruction.RefresherId, instruction.GuidId); - break; - case RefreshMethodType.RefreshById: - RefreshById(instruction.RefresherId, instruction.IntId); - break; - case RefreshMethodType.RefreshByIds: - RefreshByIds(instruction.RefresherId, instruction.JsonIds); - break; - case RefreshMethodType.RefreshByJson: - RefreshByJson(instruction.RefresherId, instruction.JsonPayload); - break; - case RefreshMethodType.RemoveById: - RemoveById(instruction.RefresherId, instruction.IntId); - break; - } - - processed.Add(instruction); - } - return true; - } - - private void RefreshAll(Guid uniqueIdentifier) - { - var refresher = GetRefresher(uniqueIdentifier); - refresher.RefreshAll(); - } - - private void RefreshByGuid(Guid uniqueIdentifier, Guid id) - { - var refresher = GetRefresher(uniqueIdentifier); - refresher.Refresh(id); - } - - private void RefreshById(Guid uniqueIdentifier, int id) - { - var refresher = GetRefresher(uniqueIdentifier); - refresher.Refresh(id); - } - - private void RefreshByIds(Guid uniqueIdentifier, string jsonIds) - { - var refresher = GetRefresher(uniqueIdentifier); - foreach (var id in JsonConvert.DeserializeObject(jsonIds)) - refresher.Refresh(id); - } - - private void RefreshByJson(Guid uniqueIdentifier, string jsonPayload) - { - var refresher = GetJsonRefresher(uniqueIdentifier); - refresher.Refresh(jsonPayload); - } - - private void RemoveById(Guid uniqueIdentifier, int id) - { - var refresher = GetRefresher(uniqueIdentifier); - refresher.Remove(id); - } - - #endregion } } From dc21e9ee8a9ea00a74bc6e3ee4365ede52a6284c Mon Sep 17 00:00:00 2001 From: Andy Butland Date: Sun, 7 Mar 2021 19:35:36 +0100 Subject: [PATCH 017/188] Added integration tests for CacheInstructionRepository. --- .../ICacheInstructionRepository.cs | 2 +- .../Implement/CacheInstructionRepository.cs | 2 +- .../Implement/CacheInstructionService.cs | 2 +- .../CacheInstructionRepositoryTest.cs | 153 ++++++++++++++++++ 4 files changed, 156 insertions(+), 3 deletions(-) create mode 100644 src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/CacheInstructionRepositoryTest.cs diff --git a/src/Umbraco.Core/Persistence/Repositories/ICacheInstructionRepository.cs b/src/Umbraco.Core/Persistence/Repositories/ICacheInstructionRepository.cs index 0381c74a03..e93f5829a1 100644 --- a/src/Umbraco.Core/Persistence/Repositories/ICacheInstructionRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/ICacheInstructionRepository.cs @@ -40,7 +40,7 @@ namespace Umbraco.Cms.Core.Persistence.Repositories /// /// Last id processed. /// The maximum number of instructions to retrieve. - IEnumerable GetInstructions(int lastId, int maxNumberToRetrieve); + IEnumerable GetPendingInstructions(int lastId, int maxNumberToRetrieve); /// /// Deletes cache instructions older than the provided date. diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/CacheInstructionRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/CacheInstructionRepository.cs index f5452b53c0..d0e025e06e 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/CacheInstructionRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/CacheInstructionRepository.cs @@ -51,7 +51,7 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement } /// - public IEnumerable GetInstructions(int lastId, int maxNumberToRetrieve) + public IEnumerable GetPendingInstructions(int lastId, int maxNumberToRetrieve) { Sql sql = AmbientScope.SqlContext.Sql().SelectAll() .From() diff --git a/src/Umbraco.Infrastructure/Services/Implement/CacheInstructionService.cs b/src/Umbraco.Infrastructure/Services/Implement/CacheInstructionService.cs index 6ca01ccb50..a2b400293a 100644 --- a/src/Umbraco.Infrastructure/Services/Implement/CacheInstructionService.cs +++ b/src/Umbraco.Infrastructure/Services/Implement/CacheInstructionService.cs @@ -248,7 +248,7 @@ namespace Umbraco.Cms.Core.Services.Implement // some memory however we cannot do that because inside of this loop the cache refreshers are also // performing some lookups which cannot be done with an active reader open. lastId = 0; - foreach (CacheInstruction instruction in _cacheInstructionRepository.GetInstructions(lastId, MaxInstructionsToRetrieve)) + foreach (CacheInstruction instruction in _cacheInstructionRepository.GetPendingInstructions(lastId, MaxInstructionsToRetrieve)) { // If this flag gets set it means we're shutting down! In this case, we need to exit asap and cannot // continue processing anything otherwise we'll hold up the app domain shutdown. diff --git a/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/CacheInstructionRepositoryTest.cs b/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/CacheInstructionRepositoryTest.cs new file mode 100644 index 0000000000..faa5fd9d6e --- /dev/null +++ b/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/CacheInstructionRepositoryTest.cs @@ -0,0 +1,153 @@ +// Copyright (c) Umbraco. +// See LICENSE for more details. + +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.Extensions.Logging; +using NUnit.Framework; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Persistence.Querying; +using Umbraco.Cms.Core.Scoping; +using Umbraco.Cms.Infrastructure.Persistence; +using Umbraco.Cms.Infrastructure.Persistence.Dtos; +using Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; +using Umbraco.Cms.Tests.Common.Testing; +using Umbraco.Cms.Tests.Integration.Testing; + +namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence.Repositories +{ + [TestFixture] + [UmbracoTest(Database = UmbracoTestOptions.Database.NewSchemaPerTest, Logger = UmbracoTestOptions.Logger.Console)] + public class CacheInstructionRepositoryTest : UmbracoIntegrationTest + { + private const string OriginIdentiy = "Test1"; + private const string Instructions = "{}"; + private readonly DateTime _date = new DateTime(2021, 7, 3, 10, 30, 0); + private const int InstructionCount = 1; + + [Test] + public void Can_Count_All() + { + const int Count = 5; + + IScopeProvider sp = ScopeProvider; + using (IScope scope = ScopeProvider.CreateScope()) + { + var repo = new CacheInstructionRepository((IScopeAccessor)sp); + for (var i = 0; i < Count; i++) + { + repo.Add(new CacheInstruction(0, _date, Instructions, OriginIdentiy, InstructionCount)); + } + + var count = repo.CountAll(); + + Assert.That(count, Is.EqualTo(Count)); + } + } + + [Test] + public void Can_Count_Pending_Instructions() + { + IScopeProvider sp = ScopeProvider; + using (IScope scope = ScopeProvider.CreateScope()) + { + var repo = new CacheInstructionRepository((IScopeAccessor)sp); + for (var i = 0; i < 5; i++) + { + repo.Add(new CacheInstruction(0, _date, Instructions, OriginIdentiy, InstructionCount)); + } + + var count = repo.CountPendingInstructions(2); + + Assert.That(count, Is.EqualTo(3)); + } + } + + [Test] + public void Can_Check_Exists() + { + IScopeProvider sp = ScopeProvider; + using (IScope scope = ScopeProvider.CreateScope()) + { + var repo = new CacheInstructionRepository((IScopeAccessor)sp); + + var existsBefore = repo.Exists(1); + + repo.Add(new CacheInstruction(0, _date, Instructions, OriginIdentiy, InstructionCount)); + + var existsAfter = repo.Exists(1); + + Assert.That(existsBefore, Is.False); + Assert.That(existsAfter, Is.True); + } + } + + [Test] + public void Can_Add_Cache_Instruction() + { + const string OriginIdentiy = "Test1"; + const string Instructions = "{}"; + var date = new DateTime(2021, 7, 3, 10, 30, 0); + const int InstructionCount = 1; + + IScopeProvider sp = ScopeProvider; + using (IScope scope = ScopeProvider.CreateScope()) + { + var repo = new CacheInstructionRepository((IScopeAccessor)sp); + repo.Add(new CacheInstruction(0, date, Instructions, OriginIdentiy, InstructionCount)); + + List dtos = scope.Database.Fetch("WHERE id > -1"); + + Assert.That(dtos.Any(), Is.True); + + CacheInstructionDto dto = dtos.First(); + Assert.That(dto.UtcStamp, Is.EqualTo(date)); + Assert.That(dto.Instructions, Is.EqualTo(Instructions)); + Assert.That(dto.OriginIdentity, Is.EqualTo(OriginIdentiy)); + Assert.That(dto.InstructionCount, Is.EqualTo(InstructionCount)); + } + } + + [Test] + public void Can_Get_Pending_Instructions() + { + IScopeProvider sp = ScopeProvider; + using (IScope scope = ScopeProvider.CreateScope()) + { + var repo = new CacheInstructionRepository((IScopeAccessor)sp); + for (var i = 0; i < 5; i++) + { + repo.Add(new CacheInstruction(0, _date, Instructions, OriginIdentiy, InstructionCount)); + } + + IEnumerable instructions = repo.GetPendingInstructions(2, 2); + + Assert.That(instructions.Count(), Is.EqualTo(2)); + + Assert.That(string.Join(",", instructions.Select(x => x.Id)), Is.EqualTo("3,4")); + } + } + + [Test] + public void Can_Delete_Old_Instructions() + { + IScopeProvider sp = ScopeProvider; + using (IScope scope = ScopeProvider.CreateScope()) + { + var repo = new CacheInstructionRepository((IScopeAccessor)sp); + for (var i = 0; i < 5; i++) + { + DateTime date = i == 0 ? DateTime.UtcNow.AddDays(-2) : DateTime.UtcNow; + repo.Add(new CacheInstruction(0, date, Instructions, OriginIdentiy, InstructionCount)); + } + + repo.DeleteInstructionsOlderThan(DateTime.UtcNow.AddDays(-1)); + + var count = repo.CountAll(); + Assert.That(count, Is.EqualTo(4)); // 5 have been added, 1 is older and deleted. + } + } + } +} From a8ff1952c3ff2a3fea259beef69c4c49e69dc56f Mon Sep 17 00:00:00 2001 From: Andy Butland Date: Mon, 8 Mar 2021 17:13:37 +0100 Subject: [PATCH 018/188] Added integration tests for CacheInstructionService. --- ...ructionServiceProcessInstructionsResult.cs | 10 +- .../Implement/CacheInstructionService.cs | 20 +- .../Services/CacheInstructionServiceTests.cs | 196 ++++++++++++++++++ 3 files changed, 218 insertions(+), 8 deletions(-) create mode 100644 src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/CacheInstructionServiceTests.cs diff --git a/src/Umbraco.Core/Services/CacheInstructionServiceProcessInstructionsResult.cs b/src/Umbraco.Core/Services/CacheInstructionServiceProcessInstructionsResult.cs index 79d8ec1bbb..84116584a2 100644 --- a/src/Umbraco.Core/Services/CacheInstructionServiceProcessInstructionsResult.cs +++ b/src/Umbraco.Core/Services/CacheInstructionServiceProcessInstructionsResult.cs @@ -11,14 +11,16 @@ namespace Umbraco.Cms.Core.Services { } + public int NumberOfInstructionsProcessed { get; private set; } + public int LastId { get; private set; } public bool InstructionsWerePruned { get; private set; } - public static CacheInstructionServiceProcessInstructionsResult AsCompleted(int lastId) => - new CacheInstructionServiceProcessInstructionsResult { LastId = lastId }; + public static CacheInstructionServiceProcessInstructionsResult AsCompleted(int numberOfInstructionsProcessed, int lastId) => + new CacheInstructionServiceProcessInstructionsResult { NumberOfInstructionsProcessed = numberOfInstructionsProcessed, LastId = lastId }; - public static CacheInstructionServiceProcessInstructionsResult AsCompletedAndPruned(int lastId) => - new CacheInstructionServiceProcessInstructionsResult { LastId = lastId, InstructionsWerePruned = true }; + public static CacheInstructionServiceProcessInstructionsResult AsCompletedAndPruned(int numberOfInstructionsProcessed, int lastId) => + new CacheInstructionServiceProcessInstructionsResult { NumberOfInstructionsProcessed = numberOfInstructionsProcessed, LastId = lastId, InstructionsWerePruned = true }; }; } diff --git a/src/Umbraco.Infrastructure/Services/Implement/CacheInstructionService.cs b/src/Umbraco.Infrastructure/Services/Implement/CacheInstructionService.cs index a2b400293a..31c018d41c 100644 --- a/src/Umbraco.Infrastructure/Services/Implement/CacheInstructionService.cs +++ b/src/Umbraco.Infrastructure/Services/Implement/CacheInstructionService.cs @@ -165,6 +165,7 @@ namespace Umbraco.Cms.Core.Services.Implement using (IScope scope = ScopeProvider.CreateScope()) { _cacheInstructionRepository.Add(entity); + scope.Complete(); } } @@ -193,25 +194,30 @@ namespace Umbraco.Cms.Core.Services.Implement using (_profilingLogger.DebugDuration("Syncing from database...")) using (IScope scope = ScopeProvider.CreateScope()) { - ProcessDatabaseInstructions(released, localIdentity, out int lastId); + var numberOfInstructionsProcessed = ProcessDatabaseInstructions(released, localIdentity, out int lastId); // Check for pruning throttling. if (released || (DateTime.UtcNow - lastPruned) <= _globalSettings.DatabaseServerMessenger.TimeBetweenPruneOperations) { scope.Complete(); - return CacheInstructionServiceProcessInstructionsResult.AsCompleted(lastId); + return CacheInstructionServiceProcessInstructionsResult.AsCompleted(numberOfInstructionsProcessed, lastId); } + var instructionsWerePruned = false; switch (_serverRoleAccessor.CurrentServerRole) { case ServerRole.Single: case ServerRole.Master: PruneOldInstructions(); + instructionsWerePruned = true; break; } scope.Complete(); - return CacheInstructionServiceProcessInstructionsResult.AsCompletedAndPruned(lastId); + + return instructionsWerePruned + ? CacheInstructionServiceProcessInstructionsResult.AsCompletedAndPruned(numberOfInstructionsProcessed, lastId) + : CacheInstructionServiceProcessInstructionsResult.AsCompleted(numberOfInstructionsProcessed, lastId); } } @@ -221,7 +227,8 @@ namespace Umbraco.Cms.Core.Services.Implement /// /// Thread safety: this is NOT thread safe. Because it is NOT meant to run multi-threaded. /// - private void ProcessDatabaseInstructions(bool released, string localIdentity, out int lastId) + /// Number of instructions processed. + private int ProcessDatabaseInstructions(bool released, string localIdentity, out int lastId) { // NOTE: // We 'could' recurse to ensure that no remaining instructions are pending in the table before proceeding but I don't think that @@ -243,6 +250,7 @@ namespace Umbraco.Cms.Core.Services.Implement // Tracks which ones have already been processed to avoid duplicates var processed = new HashSet(); + var numberOfInstructionsProcessed = 0; // It would have been nice to do this in a Query instead of Fetch using a data reader to save // some memory however we cannot do that because inside of this loop the cache refreshers are also @@ -282,7 +290,11 @@ namespace Umbraco.Cms.Core.Services.Implement _logger.LogInformation("The current batch of instructions was not processed, app is shutting down"); break; } + + numberOfInstructionsProcessed++; } + + return numberOfInstructionsProcessed; } /// diff --git a/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/CacheInstructionServiceTests.cs b/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/CacheInstructionServiceTests.cs new file mode 100644 index 0000000000..155bdaabaf --- /dev/null +++ b/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/CacheInstructionServiceTests.cs @@ -0,0 +1,196 @@ +// Copyright (c) Umbraco. +// See LICENSE for more details. + +using System; +using System.Collections.Generic; +using System.Linq; +using NPoco; +using NUnit.Framework; +using Umbraco.Cms.Core.Cache; +using Umbraco.Cms.Core.Scoping; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Services.Implement; +using Umbraco.Cms.Core.Sync; +using Umbraco.Cms.Infrastructure.Persistence; +using Umbraco.Cms.Infrastructure.Persistence.Dtos; +using Umbraco.Cms.Tests.Common.Testing; +using Umbraco.Cms.Tests.Integration.Testing; +using Umbraco.Extensions; + +namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services +{ + [TestFixture] + [UmbracoTest(Database = UmbracoTestOptions.Database.NewSchemaPerTest)] + public class CacheInstructionServiceTests : UmbracoIntegrationTest + { + private const string LocalIdentity = "localIdentity"; + private const string AlternateIdentity = "alternateIdentity"; + + [Test] + public void Can_Ensure_Initialized_With_No_Instructions() + { + var sut = (CacheInstructionService)GetRequiredService(); + + CacheInstructionServiceInitializationResult result = sut.EnsureInitialized(false, 0); + + Assert.Multiple(() => + { + Assert.IsFalse(result.ColdBootRequired); + Assert.AreEqual(0, result.MaxId); + Assert.AreEqual(0, result.LastId); + }); + } + + [Test] + public void Can_Ensure_Initialized_With_UnSynced_Instructions() + { + var sut = (CacheInstructionService)GetRequiredService(); + + List instructions = CreateInstructions(); + sut.DeliverInstructions(instructions, LocalIdentity); + + CacheInstructionServiceInitializationResult result = sut.EnsureInitialized(false, 0); + + Assert.Multiple(() => + { + Assert.IsTrue(result.ColdBootRequired); + Assert.AreEqual(1, result.MaxId); + Assert.AreEqual(-1, result.LastId); + }); + } + + [Test] + public void Can_Ensure_Initialized_With_Synced_Instructions() + { + var sut = (CacheInstructionService)GetRequiredService(); + + List instructions = CreateInstructions(); + sut.DeliverInstructions(instructions, LocalIdentity); + + CacheInstructionServiceInitializationResult result = sut.EnsureInitialized(false, 1); + + Assert.Multiple(() => + { + Assert.IsFalse(result.ColdBootRequired); + Assert.AreEqual(1, result.LastId); + }); + } + + [Test] + public void Can_Deliver_Instructions() + { + var sut = (CacheInstructionService)GetRequiredService(); + + List instructions = CreateInstructions(); + + sut.DeliverInstructions(instructions, LocalIdentity); + + AssertDeliveredInstructions(); + } + + [Test] + public void Can_Deliver_Instructions_In_Batches() + { + var sut = (CacheInstructionService)GetRequiredService(); + + List instructions = CreateInstructions(); + + sut.DeliverInstructionsInBatches(instructions, LocalIdentity); + + AssertDeliveredInstructions(); + } + + private List CreateInstructions() => new List + { + new RefreshInstruction(UserCacheRefresher.UniqueId, RefreshMethodType.RefreshByIds, Guid.Empty, 0, "[-1]", null), + new RefreshInstruction(UserCacheRefresher.UniqueId, RefreshMethodType.RefreshByIds, Guid.Empty, 0, "[-1]", null), + }; + + private void AssertDeliveredInstructions() + { + List cacheInstructions; + ISqlContext sqlContext = GetRequiredService(); + Sql sql = sqlContext.Sql() + .Select() + .From(); + using (IScope scope = ScopeProvider.CreateScope()) + { + cacheInstructions = scope.Database.Fetch(sql); + scope.Complete(); + } + + Assert.Multiple(() => + { + Assert.AreEqual(1, cacheInstructions.Count); + + CacheInstructionDto firstInstruction = cacheInstructions.First(); + Assert.AreEqual(2, firstInstruction.InstructionCount); + Assert.AreEqual(LocalIdentity, firstInstruction.OriginIdentity); + }); + } + + [Test] + public void Can_Process_Instructions() + { + var sut = (CacheInstructionService)GetRequiredService(); + + // Create three instruction records, each with two instructions. First two records are for a different identity. + CreateMultipleInstructions(sut); + + CacheInstructionServiceProcessInstructionsResult result = sut.ProcessInstructions(false, LocalIdentity, DateTime.UtcNow.AddSeconds(-1)); + + Assert.Multiple(() => + { + Assert.AreEqual(3, result.LastId); // 3 records found. + Assert.AreEqual(2, result.NumberOfInstructionsProcessed); // 2 records processed (as one is for the same identity). + Assert.IsFalse(result.InstructionsWerePruned); + }); + } + + [Test] + public void Can_Process_And_Purge_Instructions() + { + // Purging of instructions only occurs on single or master servers, so we need to ensure this is set before running the test. + EnsureServerRegistered(); + var sut = (CacheInstructionService)GetRequiredService(); + + CreateMultipleInstructions(sut); + + CacheInstructionServiceProcessInstructionsResult result = sut.ProcessInstructions(false, LocalIdentity, DateTime.UtcNow.AddHours(-1)); + + Assert.IsTrue(result.InstructionsWerePruned); + } + + [Test] + public void Processes_No_Instructions_When_Released() + { + var sut = (CacheInstructionService)GetRequiredService(); + + CreateMultipleInstructions(sut); + + CacheInstructionServiceProcessInstructionsResult result = sut.ProcessInstructions(true, LocalIdentity, DateTime.UtcNow.AddSeconds(-1)); + + Assert.Multiple(() => + { + Assert.AreEqual(0, result.LastId); + Assert.AreEqual(0, result.NumberOfInstructionsProcessed); + Assert.IsFalse(result.InstructionsWerePruned); + }); + } + + private void CreateMultipleInstructions(CacheInstructionService sut) + { + for (int i = 0; i < 3; i++) + { + List instructions = CreateInstructions(); + sut.DeliverInstructions(instructions, i == 2 ? LocalIdentity : AlternateIdentity); + } + } + + private void EnsureServerRegistered() + { + IServerRegistrationService serverRegistrationService = GetRequiredService(); + serverRegistrationService.TouchServer("http://localhost", TimeSpan.FromMinutes(10)); + } + } +} From 6422a9a58c1e85ccf436ad902370bc1c952a8974 Mon Sep 17 00:00:00 2001 From: Andy Butland Date: Mon, 8 Mar 2021 17:39:41 +0100 Subject: [PATCH 019/188] Further integration tests for CacheInstructionService. --- .../Services/CacheInstructionServiceTests.cs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/CacheInstructionServiceTests.cs b/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/CacheInstructionServiceTests.cs index 155bdaabaf..eaf92efe6c 100644 --- a/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/CacheInstructionServiceTests.cs +++ b/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/CacheInstructionServiceTests.cs @@ -35,12 +35,23 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services Assert.Multiple(() => { + Assert.IsTrue(result.Initialized); Assert.IsFalse(result.ColdBootRequired); Assert.AreEqual(0, result.MaxId); Assert.AreEqual(0, result.LastId); }); } + [Test] + public void Is_Not_Initialized_When_Released() + { + var sut = (CacheInstructionService)GetRequiredService(); + + CacheInstructionServiceInitializationResult result = sut.EnsureInitialized(true, 0); + + Assert.IsFalse(result.Initialized); + } + [Test] public void Can_Ensure_Initialized_With_UnSynced_Instructions() { @@ -53,6 +64,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services Assert.Multiple(() => { + Assert.IsTrue(result.Initialized); Assert.IsTrue(result.ColdBootRequired); Assert.AreEqual(1, result.MaxId); Assert.AreEqual(-1, result.LastId); @@ -71,6 +83,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services Assert.Multiple(() => { + Assert.IsTrue(result.Initialized); Assert.IsFalse(result.ColdBootRequired); Assert.AreEqual(1, result.LastId); }); From 6898fd2784393f1d6523cd9f1156aff404893a3b Mon Sep 17 00:00:00 2001 From: Mole Date: Tue, 9 Mar 2021 08:19:40 +0100 Subject: [PATCH 020/188] Only lock once when decrementing --- src/Umbraco.Core/Scoping/Scope.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/Umbraco.Core/Scoping/Scope.cs b/src/Umbraco.Core/Scoping/Scope.cs index f1827d7d83..0b10086154 100644 --- a/src/Umbraco.Core/Scoping/Scope.cs +++ b/src/Umbraco.Core/Scoping/Scope.cs @@ -369,10 +369,7 @@ namespace Umbraco.Core.Scoping { DecrementReadLock(readLockPair.Key, readLockPair.Value); } - } - lock (_dictionaryLocker) - { foreach (var writeLockPair in WriteLocks) { DecrementWriteLock(writeLockPair.Key, writeLockPair.Value); From b554aa9484af83f3c17322d14c010142b13c3796 Mon Sep 17 00:00:00 2001 From: Mole Date: Tue, 9 Mar 2021 08:22:16 +0100 Subject: [PATCH 021/188] Apply suggestions from code review Co-authored-by: Bjarke Berg --- src/Umbraco.Core/Scoping/Scope.cs | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/src/Umbraco.Core/Scoping/Scope.cs b/src/Umbraco.Core/Scoping/Scope.cs index 0b10086154..819bc515ed 100644 --- a/src/Umbraco.Core/Scoping/Scope.cs +++ b/src/Umbraco.Core/Scoping/Scope.cs @@ -571,12 +571,14 @@ namespace Umbraco.Core.Scoping { lock (_dictionaryLocker) { - if (!ReadLocks.ContainsKey(lockId)) + if (ReadLocks.ContainsKey(lockId)) { - ReadLocks[lockId] = 0; + ReadLocks[lockId] += 1; + } + else + { + ReadLocks[lockId] = 1; } - - ReadLocks[lockId] += 1; } } } @@ -598,12 +600,14 @@ namespace Umbraco.Core.Scoping { lock (_dictionaryLocker) { - if (!WriteLocks.ContainsKey(lockId)) + if (WriteLocks.ContainsKey(lockId)) { - WriteLocks[lockId] = 0; + WriteLocks[lockId] = 1; + } + else + { + WriteLocks[lockId] += 1; } - - WriteLocks[lockId] += 1; } } } From adf504c20ce9fcf73ab014e6a314230153798fea Mon Sep 17 00:00:00 2001 From: Mole Date: Tue, 9 Mar 2021 09:33:34 +0100 Subject: [PATCH 022/188] Fix IncrementRequestedWriteLock --- src/Umbraco.Core/Scoping/Scope.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Core/Scoping/Scope.cs b/src/Umbraco.Core/Scoping/Scope.cs index 819bc515ed..7015cee5eb 100644 --- a/src/Umbraco.Core/Scoping/Scope.cs +++ b/src/Umbraco.Core/Scoping/Scope.cs @@ -602,11 +602,11 @@ namespace Umbraco.Core.Scoping { if (WriteLocks.ContainsKey(lockId)) { - WriteLocks[lockId] = 1; + WriteLocks[lockId] += 1; } else { - WriteLocks[lockId] += 1; + WriteLocks[lockId] = 1; } } } From 16573446dbb3814a5e62fbd731b294de8e801176 Mon Sep 17 00:00:00 2001 From: Chad Date: Mon, 22 Feb 2021 21:53:53 +1300 Subject: [PATCH 023/188] Fixes #9615 - Upgrade to Htmlsanitizer v5 (#9856) --- build/NuSpecs/UmbracoCms.Web.nuspec | 2 +- src/Umbraco.Web/Runtime/WebInitialComposer.cs | 9 +++++++++ src/Umbraco.Web/Umbraco.Web.csproj | 5 +++-- 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/build/NuSpecs/UmbracoCms.Web.nuspec b/build/NuSpecs/UmbracoCms.Web.nuspec index 82d15d2b95..ac787f64e3 100644 --- a/build/NuSpecs/UmbracoCms.Web.nuspec +++ b/build/NuSpecs/UmbracoCms.Web.nuspec @@ -42,7 +42,7 @@ - + diff --git a/src/Umbraco.Web/Runtime/WebInitialComposer.cs b/src/Umbraco.Web/Runtime/WebInitialComposer.cs index 112910930e..97e2cc1b15 100644 --- a/src/Umbraco.Web/Runtime/WebInitialComposer.cs +++ b/src/Umbraco.Web/Runtime/WebInitialComposer.cs @@ -40,6 +40,7 @@ using Current = Umbraco.Web.Composing.Current; using Umbraco.Web.PropertyEditors; using Umbraco.Core.Models; using Umbraco.Web.Models; +using Ganss.XSS; namespace Umbraco.Web.Runtime { @@ -139,6 +140,14 @@ namespace Umbraco.Web.Runtime composition.RegisterUnique(); composition.RegisterUnique(); composition.RegisterUnique(); + composition.Register(_ => + { + var sanitizer = new HtmlSanitizer(); + sanitizer.AllowedAttributes.UnionWith(Umbraco.Core.Constants.SvgSanitizer.Attributes); + sanitizer.AllowedCssProperties.UnionWith(Umbraco.Core.Constants.SvgSanitizer.Attributes); + sanitizer.AllowedTags.UnionWith(Umbraco.Core.Constants.SvgSanitizer.Tags); + return sanitizer; + },Lifetime.Singleton); composition.RegisterUnique(factory => ExamineManager.Instance); diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index c3eba87d6f..1a6c9a49a2 100644 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -67,6 +67,7 @@ 4.0.217 + 5.0.376 2.7.0.100 @@ -1286,7 +1287,7 @@ - + + + + + + + + + + + + diff --git a/build/NuSpecs/UmbracoCms.nuspec b/build/NuSpecs/UmbracoCms.nuspec index c031d71704..0c9c914d6c 100644 --- a/build/NuSpecs/UmbracoCms.nuspec +++ b/build/NuSpecs/UmbracoCms.nuspec @@ -19,6 +19,7 @@ + - - - - - - - - - diff --git a/build/NuSpecs/build/Umbraco.Cms.props b/build/NuSpecs/build/Umbraco.Cms.StaticAssets.props similarity index 100% rename from build/NuSpecs/build/Umbraco.Cms.props rename to build/NuSpecs/build/Umbraco.Cms.StaticAssets.props diff --git a/build/NuSpecs/build/Umbraco.Cms.targets b/build/NuSpecs/build/Umbraco.Cms.StaticAssets.targets similarity index 82% rename from build/NuSpecs/build/Umbraco.Cms.targets rename to build/NuSpecs/build/Umbraco.Cms.StaticAssets.targets index 29d972d480..9f40bdf125 100644 --- a/build/NuSpecs/build/Umbraco.Cms.targets +++ b/build/NuSpecs/build/Umbraco.Cms.StaticAssets.targets @@ -9,7 +9,7 @@ - + "$($this.BuildTemp)\nupack.cmssqlce.log" if (-not $?) { throw "Failed to pack NuGet UmbracoCms.SqlCe." } + &$this.BuildEnv.NuGet Pack "$nuspecs\UmbracoCms.StaticAssets.nuspec" ` + -Properties BuildTmp="$($this.BuildTemp)" ` + -Version "$($this.Version.Semver.ToString())" ` + -Verbosity detailed -outputDirectory "$($this.BuildOutput)" > "$($this.BuildTemp)\nupack.cmsstaticassets.log" + if (-not $?) { throw "Failed to pack NuGet UmbracoCms.StaticAssets." } + &$this.BuildEnv.NuGet Pack "$templates\Umbraco.Templates.nuspec" ` -Properties BuildTmp="$($this.BuildTemp)" ` -Version "$($this.Version.Semver.ToString())" ` diff --git a/src/umbraco.sln b/src/umbraco.sln index 0041474014..f0088501fa 100644 --- a/src/umbraco.sln +++ b/src/umbraco.sln @@ -40,6 +40,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "NuSpecs", "NuSpecs", "{227C ..\build\NuSpecs\UmbracoCms.nuspec = ..\build\NuSpecs\UmbracoCms.nuspec ..\build\NuSpecs\UmbracoCms.SqlCe.nuspec = ..\build\NuSpecs\UmbracoCms.SqlCe.nuspec ..\build\NuSpecs\UmbracoCms.Examine.Lucene.nuspec = ..\build\NuSpecs\UmbracoCms.Examine.Lucene.nuspec + ..\build\NuSpecs\UmbracoCms.StaticAssets.nuspec = ..\build\NuSpecs\UmbracoCms.StaticAssets.nuspec EndProjectSection EndProject Project("{E24C65DC-7377-472B-9ABA-BC803B73C61A}") = "Umbraco.Web.UI.Client", "http://localhost:3961", "{3819A550-DCEC-4153-91B4-8BA9F7F0B9B4}" @@ -113,8 +114,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tools", "tools", "{E3F9F378 EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "build", "build", "{5B03EF4E-E0AC-4905-861B-8C3EC1A0D458}" ProjectSection(SolutionItems) = preProject - ..\build\NuSpecs\build\Umbraco.Cms.props = ..\build\NuSpecs\build\Umbraco.Cms.props - ..\build\NuSpecs\build\Umbraco.Cms.targets = ..\build\NuSpecs\build\Umbraco.Cms.targets + ..\build\NuSpecs\build\Umbraco.Cms.StaticAssets.props = ..\build\NuSpecs\build\Umbraco.Cms.StaticAssets.props + ..\build\NuSpecs\build\Umbraco.Cms.StaticAssets.targets = ..\build\NuSpecs\build\Umbraco.Cms.StaticAssets.targets EndProjectSection EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "DocTools", "DocTools", "{53594E5B-64A2-4545-8367-E3627D266AE8}" From baacedd57d1db616ea429d5f90f0f9859eab4de0 Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Fri, 19 Mar 2021 08:09:00 +0100 Subject: [PATCH 080/188] Fixed bug where indexes was build before application was in run state + Moved the logic to the application started event --- .../Search/ExamineFinalComponent.cs | 33 ------------------- .../Search/ExamineFinalComposer.cs | 11 ------- .../Search/ExamineNotificationHandler.cs | 10 ++++-- 3 files changed, 8 insertions(+), 46 deletions(-) delete mode 100644 src/Umbraco.Infrastructure/Search/ExamineFinalComponent.cs delete mode 100644 src/Umbraco.Infrastructure/Search/ExamineFinalComposer.cs diff --git a/src/Umbraco.Infrastructure/Search/ExamineFinalComponent.cs b/src/Umbraco.Infrastructure/Search/ExamineFinalComponent.cs deleted file mode 100644 index 441d8af038..0000000000 --- a/src/Umbraco.Infrastructure/Search/ExamineFinalComponent.cs +++ /dev/null @@ -1,33 +0,0 @@ -using System; -using Umbraco.Cms.Core.Composing; -using Umbraco.Cms.Core.Runtime; - -namespace Umbraco.Cms.Infrastructure.Search -{ - /// - /// Executes after all other examine components have executed - /// - public sealed class ExamineFinalComponent : IComponent - { - BackgroundIndexRebuilder _indexRebuilder; - private readonly IMainDom _mainDom; - - public ExamineFinalComponent(BackgroundIndexRebuilder indexRebuilder, IMainDom mainDom) - { - _indexRebuilder = indexRebuilder; - _mainDom = mainDom; - } - - public void Initialize() - { - if (!_mainDom.IsMainDom) return; - - // TODO: Instead of waiting 5000 ms, we could add an event handler on to fulfilling the first request, then start? - _indexRebuilder.RebuildIndexes(true, TimeSpan.FromSeconds(5)); - } - - public void Terminate() - { - } - } -} diff --git a/src/Umbraco.Infrastructure/Search/ExamineFinalComposer.cs b/src/Umbraco.Infrastructure/Search/ExamineFinalComposer.cs deleted file mode 100644 index 037a3d1622..0000000000 --- a/src/Umbraco.Infrastructure/Search/ExamineFinalComposer.cs +++ /dev/null @@ -1,11 +0,0 @@ -using Umbraco.Cms.Core.Composing; - -namespace Umbraco.Cms.Infrastructure.Search -{ - // examine's final composer composes after all user composers - // and *also* after ICoreComposer (in case IUserComposer is disabled) - [ComposeAfter(typeof(IUserComposer))] - [ComposeAfter(typeof(ICoreComposer))] - public class ExamineFinalComposer : ComponentComposer - { } -} diff --git a/src/Umbraco.Infrastructure/Search/ExamineNotificationHandler.cs b/src/Umbraco.Infrastructure/Search/ExamineNotificationHandler.cs index b10bf70c10..d22acb87e2 100644 --- a/src/Umbraco.Infrastructure/Search/ExamineNotificationHandler.cs +++ b/src/Umbraco.Infrastructure/Search/ExamineNotificationHandler.cs @@ -35,6 +35,7 @@ namespace Umbraco.Cms.Infrastructure.Search private readonly IValueSetBuilder _memberValueSetBuilder; private readonly BackgroundIndexRebuilder _backgroundIndexRebuilder; private readonly TaskHelper _taskHelper; + private readonly IRuntimeState _runtimeState; private readonly IScopeProvider _scopeProvider; private readonly ServiceContext _services; private readonly IMainDom _mainDom; @@ -60,7 +61,8 @@ namespace Umbraco.Cms.Infrastructure.Search IValueSetBuilder mediaValueSetBuilder, IValueSetBuilder memberValueSetBuilder, BackgroundIndexRebuilder backgroundIndexRebuilder, - TaskHelper taskHelper) + TaskHelper taskHelper, + IRuntimeState runtimeState) { _services = services; _scopeProvider = scopeProvider; @@ -71,6 +73,7 @@ namespace Umbraco.Cms.Infrastructure.Search _memberValueSetBuilder = memberValueSetBuilder; _backgroundIndexRebuilder = backgroundIndexRebuilder; _taskHelper = taskHelper; + _runtimeState = runtimeState; _mainDom = mainDom; _profilingLogger = profilingLogger; _logger = logger; @@ -114,7 +117,10 @@ namespace Umbraco.Cms.Infrastructure.Search s_deactivate_handlers = true; } - + if (_mainDom.IsMainDom && _runtimeState.Level >= RuntimeLevel.Run) + { + _backgroundIndexRebuilder.RebuildIndexes(true); + } } From 95f42487d4f4675cfebadc37d468b3f4b5496047 Mon Sep 17 00:00:00 2001 From: Mole Date: Fri, 19 Mar 2021 08:37:23 +0100 Subject: [PATCH 081/188] Wrap dumping dictionaries in a method. --- src/Umbraco.Core/Scoping/Scope.cs | 54 +++++++++++++++---------------- 1 file changed, 26 insertions(+), 28 deletions(-) diff --git a/src/Umbraco.Core/Scoping/Scope.cs b/src/Umbraco.Core/Scoping/Scope.cs index 04e1934bcc..8075d9b165 100644 --- a/src/Umbraco.Core/Scoping/Scope.cs +++ b/src/Umbraco.Core/Scoping/Scope.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.Data; -using System.Linq; using System.Text; using Umbraco.Core.Cache; using Umbraco.Core.Composing; @@ -367,38 +366,15 @@ namespace Umbraco.Core.Scoping { // We're the parent scope, make sure that locks of all scopes has been cleared // Since we're only reading we don't have to be in a lock - if (ReadLocks is not null && ReadLocks.Any() || WriteLocks is not null && WriteLocks.Any()) + if (ReadLocks?.Count > 0 || WriteLocks?.Count > 0) { // Dump the dicts into a message for the locks. StringBuilder builder = new StringBuilder(); builder.AppendLine($"Lock counters aren't empty, suggesting a scope hasn't been properly disposed, parent id: {InstanceId}"); - if (ReadLocks is not null && ReadLocks.Any()) - { - builder.AppendLine("Remaining ReadLocks:"); - foreach (var locksValues in ReadLocks) - { - builder.AppendLine($"Scope {locksValues.Key}:"); - foreach (var lockCounter in locksValues.Value) - { - builder.AppendLine($"\tLock ID: {lockCounter.Key} - times requested: {lockCounter.Value}"); - } - } - } + WriteLockDictionaryToString(ReadLocks, builder, "read locks"); + WriteLockDictionaryToString(WriteLocks, builder, "write locks"); - if (WriteLocks is not null && WriteLocks.Any()) - { - builder.AppendLine("Remaining WriteLocks:"); - foreach (var locksValues in WriteLocks) - { - builder.AppendLine($"Scope {locksValues.Key}:"); - foreach (var lockCounter in locksValues.Value) - { - builder.AppendLine($"\tLock ID: {lockCounter.Key}, amount requested: {lockCounter.Value}"); - } - } - } - - var exception = new InvalidOperationException($"All scopes has not been disposed from parent scope: {InstanceId}"); + var exception = new InvalidOperationException($"All scopes has not been disposed from parent scope: {InstanceId}, see log for more details."); _logger.Error(exception, builder.ToString()); throw exception; } @@ -423,6 +399,28 @@ namespace Umbraco.Core.Scoping GC.SuppressFinalize(this); } + /// + /// Writes a locks dictionary to a for logging purposes. + /// + /// Lock dictionary to report on. + /// String builder to write to. + /// The name to report the dictionary as. + private void WriteLockDictionaryToString(Dictionary> dict, StringBuilder builder, string dictName) + { + if (dict?.Count > 0) + { + builder.AppendLine($"Remaining {dictName}:"); + foreach (var instance in dict) + { + builder.AppendLine($"Scope {instance.Key}"); + foreach (var lockCounter in instance.Value) + { + builder.AppendLine($"\tLock ID: {lockCounter.Key} - times requested: {lockCounter.Value}"); + } + } + } + } + private void DisposeLastScope() { // figure out completed From 507c821f66877f521ffc52ff079d9c7eb48f54da Mon Sep 17 00:00:00 2001 From: Mole Date: Fri, 19 Mar 2021 13:19:39 +0100 Subject: [PATCH 082/188] Create method for generating log message And remove forgotten comments. --- src/Umbraco.Core/Scoping/Scope.cs | 22 ++++++++++++++------- src/Umbraco.Tests/Scoping/ScopeUnitTests.cs | 2 -- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/src/Umbraco.Core/Scoping/Scope.cs b/src/Umbraco.Core/Scoping/Scope.cs index 8075d9b165..3a11e0661e 100644 --- a/src/Umbraco.Core/Scoping/Scope.cs +++ b/src/Umbraco.Core/Scoping/Scope.cs @@ -368,14 +368,8 @@ namespace Umbraco.Core.Scoping // Since we're only reading we don't have to be in a lock if (ReadLocks?.Count > 0 || WriteLocks?.Count > 0) { - // Dump the dicts into a message for the locks. - StringBuilder builder = new StringBuilder(); - builder.AppendLine($"Lock counters aren't empty, suggesting a scope hasn't been properly disposed, parent id: {InstanceId}"); - WriteLockDictionaryToString(ReadLocks, builder, "read locks"); - WriteLockDictionaryToString(WriteLocks, builder, "write locks"); - var exception = new InvalidOperationException($"All scopes has not been disposed from parent scope: {InstanceId}, see log for more details."); - _logger.Error(exception, builder.ToString()); + _logger.Error(exception, GenerateUnclearedScopesLogMessage()); throw exception; } } @@ -399,6 +393,20 @@ namespace Umbraco.Core.Scoping GC.SuppressFinalize(this); } + /// + /// Generates a log message with all scopes that hasn't cleared their locks, including how many, and what locks they have requested. + /// + /// Log message. + private string GenerateUnclearedScopesLogMessage() + { + // Dump the dicts into a message for the locks. + StringBuilder builder = new StringBuilder(); + builder.AppendLine($"Lock counters aren't empty, suggesting a scope hasn't been properly disposed, parent id: {InstanceId}"); + WriteLockDictionaryToString(ReadLocks, builder, "read locks"); + WriteLockDictionaryToString(WriteLocks, builder, "write locks"); + return builder.ToString(); + } + /// /// Writes a locks dictionary to a for logging purposes. /// diff --git a/src/Umbraco.Tests/Scoping/ScopeUnitTests.cs b/src/Umbraco.Tests/Scoping/ScopeUnitTests.cs index cb0a1c1bea..038376f71c 100644 --- a/src/Umbraco.Tests/Scoping/ScopeUnitTests.cs +++ b/src/Umbraco.Tests/Scoping/ScopeUnitTests.cs @@ -94,7 +94,6 @@ namespace Umbraco.Tests.Scoping outerScope.Complete(); } - // Since we request the ReadLock after the innerScope has been dispose, the key has been removed from the dictionary, and we fetch it again. syntaxProviderMock.Verify(x => x.WriteLock(It.IsAny(), Constants.Locks.Languages), Times.Once); syntaxProviderMock.Verify(x => x.WriteLock(It.IsAny(), Constants.Locks.ContentTree), Times.Once); } @@ -223,7 +222,6 @@ namespace Umbraco.Tests.Scoping outerScope.Complete(); } - // Since we request the ReadLock after the innerScope has been dispose, the key has been removed from the dictionary, and we fetch it again. syntaxProviderMock.Verify(x => x.ReadLock(It.IsAny(), Constants.Locks.Languages), Times.Once); syntaxProviderMock.Verify(x => x.ReadLock(It.IsAny(), Constants.Locks.ContentTree), Times.Once); } From 260d0dc1dac9dd4f270a64f7ef7cb56b3c78be80 Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Fri, 19 Mar 2021 22:06:54 +0100 Subject: [PATCH 083/188] Added a new package template to the dotnet template, nad added better info for cli and vs --- .../.template.config/dotnetcli.host.json | 6 ++ .../UmbracoPackage/.template.config/icon.png | Bin 0 -> 33516 bytes .../.template.config/ide.host.json | 13 ++++ .../.template.config/template.json | 59 ++++++++++++++++++ .../UmbracoPackage/package.manifest | 2 + .../UmbracoPackage/UmbracoPackage.csproj | 22 +++++++ .../build/UmbracoPackage.targets | 27 ++++++++ .../.template.config/dotnetcli.host.json | 13 ++++ .../UmbracoSolution/.template.config/icon.png | Bin 0 -> 33516 bytes .../.template.config/ide.host.json | 26 ++++++++ .../.template.config/template.json | 32 +++++++++- .../UmbracoSolution/UmbracoSolution.csproj | 5 ++ 12 files changed, 202 insertions(+), 3 deletions(-) create mode 100644 build/templates/UmbracoPackage/.template.config/dotnetcli.host.json create mode 100644 build/templates/UmbracoPackage/.template.config/icon.png create mode 100644 build/templates/UmbracoPackage/.template.config/ide.host.json create mode 100644 build/templates/UmbracoPackage/.template.config/template.json create mode 100644 build/templates/UmbracoPackage/App_Plugins/UmbracoPackage/package.manifest create mode 100644 build/templates/UmbracoPackage/UmbracoPackage.csproj create mode 100644 build/templates/UmbracoPackage/build/UmbracoPackage.targets create mode 100644 build/templates/UmbracoSolution/.template.config/dotnetcli.host.json create mode 100644 build/templates/UmbracoSolution/.template.config/icon.png create mode 100644 build/templates/UmbracoSolution/.template.config/ide.host.json diff --git a/build/templates/UmbracoPackage/.template.config/dotnetcli.host.json b/build/templates/UmbracoPackage/.template.config/dotnetcli.host.json new file mode 100644 index 0000000000..141f7bf97c --- /dev/null +++ b/build/templates/UmbracoPackage/.template.config/dotnetcli.host.json @@ -0,0 +1,6 @@ +{ + "$schema": "http://json.schemastore.org/dotnetcli.host", + "symbolInfo": { + + } +} diff --git a/build/templates/UmbracoPackage/.template.config/icon.png b/build/templates/UmbracoPackage/.template.config/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..9500140b38d4f82d63df3d7ea7fd543cd5041874 GIT binary patch literal 33516 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
(hJD8Qi3D=-iKHQWqyqBDdv%2YvR6z9OHK{?PF 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 E57oya?EnA( literal 0 HcmV?d00001 diff --git a/build/templates/UmbracoPackage/.template.config/ide.host.json b/build/templates/UmbracoPackage/.template.config/ide.host.json new file mode 100644 index 0000000000..231c6f5d47 --- /dev/null +++ b/build/templates/UmbracoPackage/.template.config/ide.host.json @@ -0,0 +1,13 @@ +{ + "$schema": "http://json.schemastore.org/vs-2017.3.host", + "order" : 0, + "icon": "icon.png", + "description": { + "id": "UmbracoPackage", + "text": "Umbraco Package" + }, + "symbolInfo": [ + + ] + +} diff --git a/build/templates/UmbracoPackage/.template.config/template.json b/build/templates/UmbracoPackage/.template.config/template.json new file mode 100644 index 0000000000..c87137013f --- /dev/null +++ b/build/templates/UmbracoPackage/.template.config/template.json @@ -0,0 +1,59 @@ +{ + "$schema": "http://json.schemastore.org/template", + "author": "Umbraco HQ", + "description": "An empty Umbraco Package/Plugin ready to get started", + "classifications": [ "Web", "CMS", "Umbraco", "Package", "Plugin"], + "groupIdentity": "Umbraco.TemplatesUmbracoPackage", + "identity": "Umbraco.Templates.UmbracoPackage.CSharp", + "name": "Umbraco Package", + "shortName": "umbracopackage", + "defaultName": "UmbracoPackage1", + "preferNameDirectory": true, + "tags": { + "language": "C#", + "type": "project" + }, + "primaryOutputs": [ + { + "path": "UmbracoPackage.csproj" + } + ], + "sourceName": "UmbracoPackage", + "preferNameDirectory": true, + "symbols": { + "version": { + "type": "parameter", + "datatype": "string", + "defaultValue": "9.0.0-alpha004", + "description": "The version of Umbraco to load using NuGet", + "replaces": "UMBRACO_VERSION_FROM_TEMPLATE" + }, + "namespaceReplacer": { + "type": "generated", + "generator": "coalesce", + "parameters": { + "sourceVariableName": "name", + "defaultValue": "UmbracoPackage", + "fallbackVariableName": "name" + }, + "replaces":"UmbracoPackage" + }, + "Framework": { + "type": "parameter", + "description": "The target framework for the project.", + "datatype": "choice", + "choices": [ + { + "choice": "net5.0", + "description": "Target net5.0" + }, + { + "choice": "net6.0", + "description": "Target net6.0" + } + ], + "replaces": "net5.0", + "defaultValue": "net5.0" + } + } +} diff --git a/build/templates/UmbracoPackage/App_Plugins/UmbracoPackage/package.manifest b/build/templates/UmbracoPackage/App_Plugins/UmbracoPackage/package.manifest new file mode 100644 index 0000000000..8593c62d96 --- /dev/null +++ b/build/templates/UmbracoPackage/App_Plugins/UmbracoPackage/package.manifest @@ -0,0 +1,2 @@ +{ +} \ No newline at end of file diff --git a/build/templates/UmbracoPackage/UmbracoPackage.csproj b/build/templates/UmbracoPackage/UmbracoPackage.csproj new file mode 100644 index 0000000000..bbb04526bd --- /dev/null +++ b/build/templates/UmbracoPackage/UmbracoPackage.csproj @@ -0,0 +1,22 @@ + + + net5.0 + . + + + + + + + + + + true + Always + + + True + build + + + diff --git a/build/templates/UmbracoPackage/build/UmbracoPackage.targets b/build/templates/UmbracoPackage/build/UmbracoPackage.targets new file mode 100644 index 0000000000..bf6e19dfee --- /dev/null +++ b/build/templates/UmbracoPackage/build/UmbracoPackage.targets @@ -0,0 +1,27 @@ + + + + $(MSBuildThisFileDirectory)..\App_Plugins\UmbracoPackage\**\*.* + + + + + + + + + + + + + + + + + + + + diff --git a/build/templates/UmbracoSolution/.template.config/dotnetcli.host.json b/build/templates/UmbracoSolution/.template.config/dotnetcli.host.json new file mode 100644 index 0000000000..d8ae0f76c7 --- /dev/null +++ b/build/templates/UmbracoSolution/.template.config/dotnetcli.host.json @@ -0,0 +1,13 @@ +{ + "$schema": "http://json.schemastore.org/dotnetcli.host", + "symbolInfo": { + "PackageTestSiteName": { + "longName": "PackageTestSiteName", + "shortName": "p" + }, + "UseSqlCe": { + "longName": "SqlCe", + "shortName": "ce" + } + } +} diff --git a/build/templates/UmbracoSolution/.template.config/icon.png b/build/templates/UmbracoSolution/.template.config/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..9500140b38d4f82d63df3d7ea7fd543cd5041874 GIT binary patch literal 33516 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
(hJD8Qi3D=-iKHQWqyqBDdv%2YvR6z9OHK{?PF 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 E57oya?EnA( literal 0 HcmV?d00001 diff --git a/build/templates/UmbracoSolution/.template.config/ide.host.json b/build/templates/UmbracoSolution/.template.config/ide.host.json new file mode 100644 index 0000000000..3f36657420 --- /dev/null +++ b/build/templates/UmbracoSolution/.template.config/ide.host.json @@ -0,0 +1,26 @@ +{ + "$schema": "http://json.schemastore.org/vs-2017.3.host", + "order" : 0, + "icon": "icon.png", + "description": { + "id": "UmbracoSolution", + "text": "Umbraco" + }, + "symbolInfo": [ + { + "id": "PackageTestSiteName", + "name": { + "text": "Package TestSite Name" + }, + "isVisible": "true" + }, + { + "id": "UseSqlCe", + "name": { + "text": "Use Sql Compact Edition (SqlCE)" + }, + "isVisible": "true" + } + ] + +} diff --git a/build/templates/UmbracoSolution/.template.config/template.json b/build/templates/UmbracoSolution/.template.config/template.json index a85a4f4af8..8485a21418 100644 --- a/build/templates/UmbracoSolution/.template.config/template.json +++ b/build/templates/UmbracoSolution/.template.config/template.json @@ -1,14 +1,23 @@ { "$schema": "http://json.schemastore.org/template", "author": "Umbraco HQ", + "description": "An empty Umbraco Solution ready to get started", "classifications": [ "Web", "CMS", "Umbraco"], - "identity": "Umbraco.Templates", - "name": "Umbraco - Empty Solution", + "groupIdentity": "Umbraco.Templates.UmbracoSolution", + "identity": "Umbraco.Templates.UmbracoSolution.CSharp", + "name": "Umbraco Solution", "shortName": "umbraco", + "defaultName": "UmbracoSolution1", + "preferNameDirectory": true, "tags": { "language": "C#", "type": "project" }, + "primaryOutputs": [ + { + "path": "UmbracoSolution.csproj" + } + ], "sourceName": "UmbracoSolution", "preferNameDirectory": true, "symbols": { @@ -29,10 +38,23 @@ }, "replaces":"Umbraco.Web.UI.NetCore" }, + "PackageTestSiteName": { + "type": "parameter", + "datatype":"text", + "defaultValue": "", + "replaces":"PackageTestSiteName", + "description": "The name of the package this should be a test site for (Default: '')" + }, + "PackageTestSite": { + "type": "computed", + "datatype":"bool", + "value": "(PackageTestSiteName != \"\" && PackageTestSiteName != null)" + }, "UseSqlCe":{ "type": "parameter", "datatype":"bool", - "defaultValue": "false" + "defaultValue": "false", + "description": "Adds the required dependencies to use SqlCE (Windows only) (Default: false)" }, "Framework": { "type": "parameter", @@ -42,6 +64,10 @@ { "choice": "net5.0", "description": "Target net5.0" + }, + { + "choice": "net6.0", + "description": "Target net6.0" } ], "replaces": "net5.0", diff --git a/build/templates/UmbracoSolution/UmbracoSolution.csproj b/build/templates/UmbracoSolution/UmbracoSolution.csproj index 278720afef..d62283fba8 100644 --- a/build/templates/UmbracoSolution/UmbracoSolution.csproj +++ b/build/templates/UmbracoSolution/UmbracoSolution.csproj @@ -9,6 +9,11 @@ + + + + + From 2a134cf4256b26923ae3440dc7a240cd6ae0de7e Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Sun, 21 Mar 2021 12:55:33 +0100 Subject: [PATCH 084/188] Clean up --- .../.template.config/ide.host.json | 2 +- .../.template.config/template.json | 2 +- .../.template.config/ide.host.json | 4 +- .../.template.config/template.json | 5 - .../UmbracoSolution/UmbracoSolution.csproj | 4 +- src/Umbraco.Web.UI.Client/package-lock.json | 198 +++++------------- 6 files changed, 55 insertions(+), 160 deletions(-) diff --git a/build/templates/UmbracoPackage/.template.config/ide.host.json b/build/templates/UmbracoPackage/.template.config/ide.host.json index 231c6f5d47..cf222f8839 100644 --- a/build/templates/UmbracoPackage/.template.config/ide.host.json +++ b/build/templates/UmbracoPackage/.template.config/ide.host.json @@ -4,7 +4,7 @@ "icon": "icon.png", "description": { "id": "UmbracoPackage", - "text": "Umbraco Package" + "text": "An empty Umbraco Package/Plugin ready to get started." }, "symbolInfo": [ diff --git a/build/templates/UmbracoPackage/.template.config/template.json b/build/templates/UmbracoPackage/.template.config/template.json index c87137013f..b097e74c4c 100644 --- a/build/templates/UmbracoPackage/.template.config/template.json +++ b/build/templates/UmbracoPackage/.template.config/template.json @@ -3,7 +3,7 @@ "author": "Umbraco HQ", "description": "An empty Umbraco Package/Plugin ready to get started", "classifications": [ "Web", "CMS", "Umbraco", "Package", "Plugin"], - "groupIdentity": "Umbraco.TemplatesUmbracoPackage", + "groupIdentity": "Umbraco.Templates.UmbracoPackage", "identity": "Umbraco.Templates.UmbracoPackage.CSharp", "name": "Umbraco Package", "shortName": "umbracopackage", diff --git a/build/templates/UmbracoSolution/.template.config/ide.host.json b/build/templates/UmbracoSolution/.template.config/ide.host.json index 3f36657420..ee9e86f6d4 100644 --- a/build/templates/UmbracoSolution/.template.config/ide.host.json +++ b/build/templates/UmbracoSolution/.template.config/ide.host.json @@ -4,13 +4,13 @@ "icon": "icon.png", "description": { "id": "UmbracoSolution", - "text": "Umbraco" + "text": "An empty Umbraco Solution ready to get started" }, "symbolInfo": [ { "id": "PackageTestSiteName", "name": { - "text": "Package TestSite Name" + "text": "Optional: Specific the name of a package that this should be a test site for.", }, "isVisible": "true" }, diff --git a/build/templates/UmbracoSolution/.template.config/template.json b/build/templates/UmbracoSolution/.template.config/template.json index 8485a21418..8a0b168594 100644 --- a/build/templates/UmbracoSolution/.template.config/template.json +++ b/build/templates/UmbracoSolution/.template.config/template.json @@ -45,11 +45,6 @@ "replaces":"PackageTestSiteName", "description": "The name of the package this should be a test site for (Default: '')" }, - "PackageTestSite": { - "type": "computed", - "datatype":"bool", - "value": "(PackageTestSiteName != \"\" && PackageTestSiteName != null)" - }, "UseSqlCe":{ "type": "parameter", "datatype":"bool", diff --git a/build/templates/UmbracoSolution/UmbracoSolution.csproj b/build/templates/UmbracoSolution/UmbracoSolution.csproj index d62283fba8..d1bfead7a8 100644 --- a/build/templates/UmbracoSolution/UmbracoSolution.csproj +++ b/build/templates/UmbracoSolution/UmbracoSolution.csproj @@ -9,8 +9,8 @@ - - + + diff --git a/src/Umbraco.Web.UI.Client/package-lock.json b/src/Umbraco.Web.UI.Client/package-lock.json index 6f2edfa2ae..9bcf82dee7 100644 --- a/src/Umbraco.Web.UI.Client/package-lock.json +++ b/src/Umbraco.Web.UI.Client/package-lock.json @@ -1842,8 +1842,7 @@ "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "dev": true, - "optional": true + "dev": true }, "base64id": { "version": "1.0.0", @@ -2052,8 +2051,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", "integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ=", - "dev": true, - "optional": true + "dev": true }, "got": { "version": "8.3.2", @@ -2131,7 +2129,6 @@ "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-2.0.1.tgz", "integrity": "sha1-2N0ZeVldLcATnh/ka4tkbLPN8Dg=", "dev": true, - "optional": true, "requires": { "p-finally": "^1.0.0" } @@ -2173,7 +2170,6 @@ "resolved": "https://registry.npmjs.org/bl/-/bl-1.2.3.tgz", "integrity": "sha512-pvcNpa0UU69UT341rO6AYy4FVAIkUHuZXRIWbq+zHnsVcRzDDjIAhGuuYoi0d//cwIwtt4pkpKycWEfjdV+vww==", "dev": true, - "optional": true, "requires": { "readable-stream": "^2.3.5", "safe-buffer": "^5.1.1" @@ -2183,15 +2179,13 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", - "dev": true, - "optional": true + "dev": true }, "readable-stream": { "version": "2.3.7", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", "dev": true, - "optional": true, "requires": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", @@ -2207,7 +2201,6 @@ "resolved": "http://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", "dev": true, - "optional": true, "requires": { "safe-buffer": "~5.1.0" } @@ -2348,7 +2341,6 @@ "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", "dev": true, - "optional": true, "requires": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" @@ -2374,8 +2366,7 @@ "version": "0.2.13", "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", "integrity": "sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI=", - "dev": true, - "optional": true + "dev": true }, "buffer-equal": { "version": "1.0.0", @@ -2572,7 +2563,6 @@ "resolved": "https://registry.npmjs.org/caw/-/caw-2.0.1.tgz", "integrity": "sha1-bDygcfwZRyCIPC3F2psHS/x+npU=", "dev": true, - "optional": true, "requires": { "get-proxy": "^2.0.0", "isurl": "^1.0.0-alpha5", @@ -3096,7 +3086,6 @@ "resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.12.tgz", "integrity": "sha1-D96NCRIA616AjK8l/mGMAvSOTvo=", "dev": true, - "optional": true, "requires": { "ini": "^1.3.4", "proto-list": "~1.2.1" @@ -3152,7 +3141,6 @@ "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.3.tgz", "integrity": "sha1-4TDK9+cnkIfFYWwgB9BIVpiYT70=", "dev": true, - "optional": true, "requires": { "safe-buffer": "5.1.2" } @@ -3594,7 +3582,6 @@ "resolved": "https://registry.npmjs.org/decompress/-/decompress-4.2.1.tgz", "integrity": "sha512-e48kc2IjU+2Zw8cTb6VZcJQ3lgVbS4uuB1TfCHbiZIP/haNXm+SVyhu+87jts5/3ROpd82GSVCoNs/z8l4ZOaQ==", "dev": true, - "optional": true, "requires": { "decompress-tar": "^4.0.0", "decompress-tarbz2": "^4.0.0", @@ -3611,7 +3598,6 @@ "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-1.3.0.tgz", "integrity": "sha1-ecEDO4BRW9bSTsmTPoYMp17ifww=", "dev": true, - "optional": true, "requires": { "pify": "^3.0.0" }, @@ -3620,8 +3606,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", - "dev": true, - "optional": true + "dev": true } } } @@ -3632,7 +3617,6 @@ "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-3.3.0.tgz", "integrity": "sha1-gKTdMjdIOEv6JICDYirt7Jgq3/M=", "dev": true, - "optional": true, "requires": { "mimic-response": "^1.0.0" } @@ -3642,7 +3626,6 @@ "resolved": "https://registry.npmjs.org/decompress-tar/-/decompress-tar-4.1.1.tgz", "integrity": "sha1-cYy9P8sWIJcW5womuE57pFkuWvE=", "dev": true, - "optional": true, "requires": { "file-type": "^5.2.0", "is-stream": "^1.1.0", @@ -3653,8 +3636,7 @@ "version": "5.2.0", "resolved": "https://registry.npmjs.org/file-type/-/file-type-5.2.0.tgz", "integrity": "sha1-LdvqfHP/42No365J3DOMBYwritY=", - "dev": true, - "optional": true + "dev": true } } }, @@ -3663,7 +3645,6 @@ "resolved": "https://registry.npmjs.org/decompress-tarbz2/-/decompress-tarbz2-4.1.1.tgz", "integrity": "sha1-MIKluIDqQEOBY0nzeLVsUWvho5s=", "dev": true, - "optional": true, "requires": { "decompress-tar": "^4.1.0", "file-type": "^6.1.0", @@ -3676,8 +3657,7 @@ "version": "6.2.0", "resolved": "https://registry.npmjs.org/file-type/-/file-type-6.2.0.tgz", "integrity": "sha1-5QzXXTVv/tTjBtxPW89Sp5kDqRk=", - "dev": true, - "optional": true + "dev": true } } }, @@ -3686,7 +3666,6 @@ "resolved": "https://registry.npmjs.org/decompress-targz/-/decompress-targz-4.1.1.tgz", "integrity": "sha1-wJvDXE0R894J8tLaU+neI+fOHu4=", "dev": true, - "optional": true, "requires": { "decompress-tar": "^4.1.1", "file-type": "^5.2.0", @@ -3697,8 +3676,7 @@ "version": "5.2.0", "resolved": "https://registry.npmjs.org/file-type/-/file-type-5.2.0.tgz", "integrity": "sha1-LdvqfHP/42No365J3DOMBYwritY=", - "dev": true, - "optional": true + "dev": true } } }, @@ -3707,7 +3685,6 @@ "resolved": "https://registry.npmjs.org/decompress-unzip/-/decompress-unzip-4.0.1.tgz", "integrity": "sha1-3qrM39FK6vhVePczroIQ+bSEj2k=", "dev": true, - "optional": true, "requires": { "file-type": "^3.8.0", "get-stream": "^2.2.0", @@ -3719,15 +3696,13 @@ "version": "3.9.0", "resolved": "https://registry.npmjs.org/file-type/-/file-type-3.9.0.tgz", "integrity": "sha1-JXoHg4TR24CHvESdEH1SpSZyuek=", - "dev": true, - "optional": true + "dev": true }, "get-stream": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-2.3.1.tgz", "integrity": "sha1-Xzj5PzRgCWZu4BUKBUFn+Rvdld4=", "dev": true, - "optional": true, "requires": { "object-assign": "^4.0.1", "pinkie-promise": "^2.0.0" @@ -3737,8 +3712,7 @@ "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", - "dev": true, - "optional": true + "dev": true } } }, @@ -4026,8 +4000,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", - "dev": true, - "optional": true + "dev": true } } }, @@ -4044,8 +4017,7 @@ "version": "0.1.4", "resolved": "https://registry.npmjs.org/duplexer3/-/duplexer3-0.1.4.tgz", "integrity": "sha1-7gHdHKwO08vH/b6jfcCo8c4ALOI=", - "dev": true, - "optional": true + "dev": true }, "duplexify": { "version": "3.7.1", @@ -4690,7 +4662,6 @@ "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz", "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==", "dev": true, - "optional": true, "requires": { "cross-spawn": "^6.0.0", "get-stream": "^4.0.0", @@ -4832,7 +4803,6 @@ "resolved": "https://registry.npmjs.org/ext-list/-/ext-list-2.2.2.tgz", "integrity": "sha1-C5jmTtgvWs8PKTG6v2khLvUt3Tc=", "dev": true, - "optional": true, "requires": { "mime-db": "^1.28.0" } @@ -4842,7 +4812,6 @@ "resolved": "https://registry.npmjs.org/ext-name/-/ext-name-5.0.0.tgz", "integrity": "sha1-cHgZgdGD7hXROZPIgiBFxQbI8KY=", "dev": true, - "optional": true, "requires": { "ext-list": "^2.0.0", "sort-keys-length": "^1.0.0" @@ -5080,7 +5049,6 @@ "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", "integrity": "sha1-JcfInLH5B3+IkbvmHY85Dq4lbx4=", "dev": true, - "optional": true, "requires": { "pend": "~1.2.0" } @@ -5119,15 +5087,13 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/filename-reserved-regex/-/filename-reserved-regex-2.0.0.tgz", "integrity": "sha1-q/c9+rc10EVECr/qLZHzieu/oik=", - "dev": true, - "optional": true + "dev": true }, "filenamify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/filenamify/-/filenamify-2.1.0.tgz", "integrity": "sha1-iPr0lfsbR6v9YSMAACoWIoxnfuk=", "dev": true, - "optional": true, "requires": { "filename-reserved-regex": "^2.0.0", "strip-outer": "^1.0.0", @@ -5476,8 +5442,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", "integrity": "sha1-a+Dem+mYzhavivwkSXue6bfM2a0=", - "dev": true, - "optional": true + "dev": true }, "fs-mkdirp-stream": { "version": "1.0.0", @@ -5524,8 +5489,7 @@ "ansi-regex": { "version": "2.1.1", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "aproba": { "version": "1.2.0", @@ -5546,14 +5510,12 @@ "balanced-match": { "version": "1.0.0", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "brace-expansion": { "version": "1.1.11", "bundled": true, "dev": true, - "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -5568,20 +5530,17 @@ "code-point-at": { "version": "1.1.0", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "concat-map": { "version": "0.0.1", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "console-control-strings": { "version": "1.1.0", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "core-util-is": { "version": "1.0.2", @@ -5698,8 +5657,7 @@ "inherits": { "version": "2.0.3", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "ini": { "version": "1.3.5", @@ -5711,7 +5669,6 @@ "version": "1.0.0", "bundled": true, "dev": true, - "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -5726,7 +5683,6 @@ "version": "3.0.4", "bundled": true, "dev": true, - "optional": true, "requires": { "brace-expansion": "^1.1.7" } @@ -5734,14 +5690,12 @@ "minimist": { "version": "0.0.8", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "minipass": { "version": "2.3.5", "bundled": true, "dev": true, - "optional": true, "requires": { "safe-buffer": "^5.1.2", "yallist": "^3.0.0" @@ -5760,7 +5714,6 @@ "version": "0.5.1", "bundled": true, "dev": true, - "optional": true, "requires": { "minimist": "0.0.8" } @@ -5841,8 +5794,7 @@ "number-is-nan": { "version": "1.0.1", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "object-assign": { "version": "4.1.1", @@ -5854,7 +5806,6 @@ "version": "1.4.0", "bundled": true, "dev": true, - "optional": true, "requires": { "wrappy": "1" } @@ -5940,8 +5891,7 @@ "safe-buffer": { "version": "5.1.2", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "safer-buffer": { "version": "2.1.2", @@ -5977,7 +5927,6 @@ "version": "1.0.2", "bundled": true, "dev": true, - "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -5997,7 +5946,6 @@ "version": "3.0.1", "bundled": true, "dev": true, - "optional": true, "requires": { "ansi-regex": "^2.0.0" } @@ -6041,14 +5989,12 @@ "wrappy": { "version": "1.0.2", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "yallist": { "version": "3.0.3", "bundled": true, - "dev": true, - "optional": true + "dev": true } } }, @@ -6075,7 +6021,6 @@ "resolved": "https://registry.npmjs.org/get-proxy/-/get-proxy-2.1.0.tgz", "integrity": "sha1-NJ8rTZHUTE1NTpy6KtkBQ/rF75M=", "dev": true, - "optional": true, "requires": { "npm-conf": "^1.1.0" } @@ -6084,15 +6029,13 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-4.0.1.tgz", "integrity": "sha1-uWjGsKBDhDJJAui/Gl3zJXmkUP4=", - "dev": true, - "optional": true + "dev": true }, "get-stream": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", "dev": true, - "optional": true, "requires": { "pump": "^3.0.0" }, @@ -6102,7 +6045,6 @@ "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", "dev": true, - "optional": true, "requires": { "end-of-stream": "^1.1.0", "once": "^1.3.1" @@ -6215,8 +6157,7 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, - "optional": true + "dev": true }, "pump": { "version": "3.0.0", @@ -7419,8 +7360,7 @@ "version": "1.4.2", "resolved": "https://registry.npmjs.org/has-symbol-support-x/-/has-symbol-support-x-1.4.2.tgz", "integrity": "sha1-FAn5i8ACR9pF2mfO4KNvKC/yZFU=", - "dev": true, - "optional": true + "dev": true }, "has-symbols": { "version": "1.0.0", @@ -7433,7 +7373,6 @@ "resolved": "https://registry.npmjs.org/has-to-string-tag-x/-/has-to-string-tag-x-1.4.1.tgz", "integrity": "sha1-oEWrOD17SyASoAFIqwql8pAETU0=", "dev": true, - "optional": true, "requires": { "has-symbol-support-x": "^1.4.1" } @@ -7639,8 +7578,7 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", - "dev": true, - "optional": true + "dev": true }, "ignore": { "version": "4.0.6", @@ -7780,8 +7718,7 @@ "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "optional": true + "dev": true }, "svgo": { "version": "1.3.2", @@ -7853,7 +7790,6 @@ "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-2.1.0.tgz", "integrity": "sha1-ji1INIdCEhtKghi3oTfppSBJ3IA=", "dev": true, - "optional": true, "requires": { "repeating": "^2.0.0" } @@ -8180,8 +8116,7 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/is-finite/-/is-finite-1.1.0.tgz", "integrity": "sha512-cdyMtqX/BOqqNBBiKlIVkytNHm49MtMlYyn1zxzvJKWmFMlGzm+ry5BBfYyeY9YmNKbRSo/o7OX9w9ale0wg3w==", - "dev": true, - "optional": true + "dev": true }, "is-fullwidth-code-point": { "version": "1.0.0", @@ -8231,8 +8166,7 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/is-natural-number/-/is-natural-number-4.0.1.tgz", "integrity": "sha1-q5124dtM7VHjXeDHLr7PCfc0zeg=", - "dev": true, - "optional": true + "dev": true }, "is-negated-glob": { "version": "1.0.0", @@ -8270,15 +8204,13 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-object/-/is-object-1.0.2.tgz", "integrity": "sha512-2rRIahhZr2UWb45fIOuvZGpFtz0TyOZLf32KxBbSoUCeZR495zCKlWUKKUByk3geS2eAs7ZAABt0Y/Rx0GiQGA==", - "dev": true, - "optional": true + "dev": true }, "is-plain-obj": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz", "integrity": "sha1-caUMhCnfync8kqOQpKA7OfzVHT4=", - "dev": true, - "optional": true + "dev": true }, "is-plain-object": { "version": "2.0.4", @@ -8348,15 +8280,13 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/is-retry-allowed/-/is-retry-allowed-1.2.0.tgz", "integrity": "sha1-13hIi9CkZmo76KFIK58rqv7eqLQ=", - "dev": true, - "optional": true + "dev": true }, "is-stream": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=", - "dev": true, - "optional": true + "dev": true }, "is-svg": { "version": "3.0.0", @@ -8451,7 +8381,6 @@ "resolved": "https://registry.npmjs.org/isurl/-/isurl-1.0.0.tgz", "integrity": "sha1-sn9PSfPNqj6kSgpbfzRi5u3DnWc=", "dev": true, - "optional": true, "requires": { "has-to-string-tag-x": "^1.2.0", "is-object": "^1.0.1" @@ -9347,8 +9276,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-1.0.1.tgz", "integrity": "sha1-b54wtHCE2XGnyCD/FabFFnt0wm8=", - "dev": true, - "optional": true + "dev": true }, "lpad-align": { "version": "1.1.2", @@ -9418,8 +9346,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz", "integrity": "sha1-2TPOuSBdgr3PSIb2dCvcK03qFG0=", - "dev": true, - "optional": true + "dev": true }, "map-visit": { "version": "1.0.0", @@ -9587,8 +9514,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", "integrity": "sha1-SSNTiHju9CBjy4o+OweYeBSHqxs=", - "dev": true, - "optional": true + "dev": true }, "minimatch": { "version": "3.0.4", @@ -12935,7 +12861,6 @@ "resolved": "https://registry.npmjs.org/npm-conf/-/npm-conf-1.1.3.tgz", "integrity": "sha1-JWzEe9DiGMJZxOlVC/QTvCGSr/k=", "dev": true, - "optional": true, "requires": { "config-chain": "^1.1.11", "pify": "^3.0.0" @@ -12945,8 +12870,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", - "dev": true, - "optional": true + "dev": true } } }, @@ -12955,7 +12879,6 @@ "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", "integrity": "sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=", "dev": true, - "optional": true, "requires": { "path-key": "^2.0.0" } @@ -13324,8 +13247,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=", - "dev": true, - "optional": true + "dev": true }, "p-is-promise": { "version": "1.1.0", @@ -13362,7 +13284,6 @@ "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-1.2.1.tgz", "integrity": "sha1-XrOzU7f86Z8QGhA4iAuwVOu+o4Y=", "dev": true, - "optional": true, "requires": { "p-finally": "^1.0.0" } @@ -13553,8 +13474,7 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", "integrity": "sha1-elfrVQpng/kRUzH89GY9XI4AelA=", - "dev": true, - "optional": true + "dev": true }, "performance-now": { "version": "2.1.0", @@ -14061,8 +13981,7 @@ "version": "1.2.4", "resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz", "integrity": "sha1-IS1b/hMYMGpCD2QCuOJv85ZHqEk=", - "dev": true, - "optional": true + "dev": true }, "prr": { "version": "1.0.1", @@ -14420,7 +14339,6 @@ "resolved": "https://registry.npmjs.org/repeating/-/repeating-2.0.1.tgz", "integrity": "sha1-UhTFOpJtNVJwdSf7q0FdvAjQbdo=", "dev": true, - "optional": true, "requires": { "is-finite": "^1.0.0" } @@ -14775,7 +14693,6 @@ "resolved": "https://registry.npmjs.org/seek-bzip/-/seek-bzip-1.0.6.tgz", "integrity": "sha512-e1QtP3YL5tWww8uKaOCQ18UxIT2laNBXHjV/S2WYCiK4udiv8lkG89KRIoCjUagnAmCBurjF4zEVX2ByBbnCjQ==", "dev": true, - "optional": true, "requires": { "commander": "^2.8.1" } @@ -15170,7 +15087,6 @@ "resolved": "https://registry.npmjs.org/sort-keys/-/sort-keys-1.1.2.tgz", "integrity": "sha1-RBttTTRnmPG05J6JIK37oOVD+a0=", "dev": true, - "optional": true, "requires": { "is-plain-obj": "^1.0.0" } @@ -15180,7 +15096,6 @@ "resolved": "https://registry.npmjs.org/sort-keys-length/-/sort-keys-length-1.0.1.tgz", "integrity": "sha1-nLb09OnkgVWmqgZx7dM2/xR5oYg=", "dev": true, - "optional": true, "requires": { "sort-keys": "^1.0.0" } @@ -15528,7 +15443,6 @@ "resolved": "https://registry.npmjs.org/strip-dirs/-/strip-dirs-2.1.0.tgz", "integrity": "sha1-SYdzYmT8NEzyD2w0rKnRPR1O1sU=", "dev": true, - "optional": true, "requires": { "is-natural-number": "^4.0.1" } @@ -15537,8 +15451,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=", - "dev": true, - "optional": true + "dev": true }, "strip-final-newline": { "version": "2.0.0", @@ -15568,7 +15481,6 @@ "resolved": "https://registry.npmjs.org/strip-outer/-/strip-outer-1.0.1.tgz", "integrity": "sha1-sv0qv2YEudHmATBXGV34Nrip1jE=", "dev": true, - "optional": true, "requires": { "escape-string-regexp": "^1.0.2" } @@ -15694,7 +15606,6 @@ "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-1.6.2.tgz", "integrity": "sha1-jqVdqzeXIlPZqa+Q/c1VmuQ1xVU=", "dev": true, - "optional": true, "requires": { "bl": "^1.0.0", "buffer-alloc": "^1.2.0", @@ -15709,15 +15620,13 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", - "dev": true, - "optional": true + "dev": true }, "readable-stream": { "version": "2.3.7", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", "dev": true, - "optional": true, "requires": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", @@ -15733,7 +15642,6 @@ "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", "dev": true, - "optional": true, "requires": { "safe-buffer": "~5.1.0" } @@ -15744,15 +15652,13 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/temp-dir/-/temp-dir-1.0.0.tgz", "integrity": "sha1-CnwOom06Oa+n4OvqnB/AvE2qAR0=", - "dev": true, - "optional": true + "dev": true }, "tempfile": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/tempfile/-/tempfile-2.0.0.tgz", "integrity": "sha1-awRGhWqbERTRhW/8vlCczLCXcmU=", "dev": true, - "optional": true, "requires": { "temp-dir": "^1.0.0", "uuid": "^3.0.1" @@ -15866,8 +15772,7 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/timed-out/-/timed-out-4.0.1.tgz", "integrity": "sha1-8y6srFoXW+ol1/q1Zas+2HQe9W8=", - "dev": true, - "optional": true + "dev": true }, "timers-ext": { "version": "0.1.7", @@ -15924,8 +15829,7 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.1.1.tgz", "integrity": "sha1-STvUj2LXxD/N7TE6A9ytsuEhOoA=", - "dev": true, - "optional": true + "dev": true }, "to-fast-properties": { "version": "2.0.0", @@ -16027,7 +15931,6 @@ "resolved": "https://registry.npmjs.org/trim-repeated/-/trim-repeated-1.0.0.tgz", "integrity": "sha1-42RqLqTokTEr9+rObPsFOAvAHCE=", "dev": true, - "optional": true, "requires": { "escape-string-regexp": "^1.0.2" } @@ -16163,7 +16066,6 @@ "resolved": "https://registry.npmjs.org/unbzip2-stream/-/unbzip2-stream-1.4.3.tgz", "integrity": "sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg==", "dev": true, - "optional": true, "requires": { "buffer": "^5.2.1", "through": "^2.3.8" @@ -16372,8 +16274,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/url-to-options/-/url-to-options-1.0.1.tgz", "integrity": "sha1-FQWgOiiaSMvXpDTvuu7FBV9WM6k=", - "dev": true, - "optional": true + "dev": true }, "use": { "version": "3.1.1", @@ -16867,7 +16768,6 @@ "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", "integrity": "sha1-x+sXyT4RLLEIb6bY5R+wZnt5pfk=", "dev": true, - "optional": true, "requires": { "buffer-crc32": "~0.2.3", "fd-slicer": "~1.1.0" From 0e4da70e23ba93c37a26ef6c76ef72a289adf04e Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Sun, 21 Mar 2021 12:58:39 +0100 Subject: [PATCH 085/188] Added nuget info --- build/templates/UmbracoPackage/UmbracoPackage.csproj | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/build/templates/UmbracoPackage/UmbracoPackage.csproj b/build/templates/UmbracoPackage/UmbracoPackage.csproj index bbb04526bd..e688419184 100644 --- a/build/templates/UmbracoPackage/UmbracoPackage.csproj +++ b/build/templates/UmbracoPackage/UmbracoPackage.csproj @@ -2,6 +2,12 @@ net5.0 . + UmbracoPackage + UmbracoPackage + UmbracoPackage + ... + ... + umbraco plugin package From a55a0c9e2d9ae17e0b68ea653f0607fc7b5873aa Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Mon, 22 Mar 2021 08:29:31 +0100 Subject: [PATCH 086/188] Do not minify bundled CSS at runtime --- .../WebAssets/BackOfficeWebAssets.cs | 13 ++++++------- src/Umbraco.Web.UI.Client/gulp/config.js | 6 +++--- .../gulp/tasks/dependencies.js | 4 ++-- .../components/umbcolorpicker.directive.js | 4 ++-- .../components/umbdatetimepicker.directive.js | 2 +- src/Umbraco.Web.UI.Client/src/index.html | 4 ++-- .../src/views/content/umbpreview.html | 2 +- .../umbraco/UmbracoInstall/Index.cshtml | 2 +- 8 files changed, 18 insertions(+), 19 deletions(-) diff --git a/src/Umbraco.Infrastructure/WebAssets/BackOfficeWebAssets.cs b/src/Umbraco.Infrastructure/WebAssets/BackOfficeWebAssets.cs index 05171988cf..4ffa549c3e 100644 --- a/src/Umbraco.Infrastructure/WebAssets/BackOfficeWebAssets.cs +++ b/src/Umbraco.Infrastructure/WebAssets/BackOfficeWebAssets.cs @@ -48,19 +48,18 @@ namespace Umbraco.Cms.Infrastructure.WebAssets { // Create bundles - // TODO: I think we don't want to optimize these css if/when we get gulp to do that all for us - _runtimeMinifier.CreateCssBundle(UmbracoInitCssBundleName, true, + _runtimeMinifier.CreateCssBundle(UmbracoInitCssBundleName, false, FormatPaths("lib/bootstrap-social/bootstrap-social.css", - "assets/css/umbraco.css", + "assets/css/umbraco.min.css", "lib/font-awesome/css/font-awesome.min.css")); - _runtimeMinifier.CreateCssBundle(UmbracoUpgradeCssBundleName, true, - FormatPaths("assets/css/umbraco.css", + _runtimeMinifier.CreateCssBundle(UmbracoUpgradeCssBundleName, false, + FormatPaths("assets/css/umbraco.min.css", "lib/bootstrap-social/bootstrap-social.css", "lib/font-awesome/css/font-awesome.min.css")); - _runtimeMinifier.CreateCssBundle(UmbracoPreviewCssBundleName, true, - FormatPaths("assets/css/canvasdesigner.css")); + _runtimeMinifier.CreateCssBundle(UmbracoPreviewCssBundleName, false, + FormatPaths("assets/css/canvasdesigner.min.css")); _runtimeMinifier.CreateJsBundle(UmbracoPreviewJsBundleName, false, FormatPaths(GetScriptsForPreview())); diff --git a/src/Umbraco.Web.UI.Client/gulp/config.js b/src/Umbraco.Web.UI.Client/gulp/config.js index bd4c406e0f..50d9ab80bc 100755 --- a/src/Umbraco.Web.UI.Client/gulp/config.js +++ b/src/Umbraco.Web.UI.Client/gulp/config.js @@ -23,10 +23,10 @@ module.exports = { // less files used by backoffice and preview // processed in the less task less: { - installer: { files: "./src/less/installer.less", watch: "./src/less/**/*.less", out: "installer.css" }, + installer: { files: "./src/less/installer.less", watch: "./src/less/**/*.less", out: "installer.min.css" }, nonodes: { files: "./src/less/pages/nonodes.less", watch: "./src/less/**/*.less", out: "nonodes.style.min.css"}, - preview: { files: "./src/less/canvas-designer.less", watch: "./src/less/**/*.less", out: "canvasdesigner.css" }, - umbraco: { files: "./src/less/belle.less", watch: "./src/**/*.less", out: "umbraco.css" }, + preview: { files: "./src/less/canvas-designer.less", watch: "./src/less/**/*.less", out: "canvasdesigner.min.css" }, + umbraco: { files: "./src/less/belle.less", watch: "./src/**/*.less", out: "umbraco.min.css" }, rteContent: { files: "./src/less/rte-content.less", watch: "./src/less/**/*.less", out: "rte-content.css" } }, diff --git a/src/Umbraco.Web.UI.Client/gulp/tasks/dependencies.js b/src/Umbraco.Web.UI.Client/gulp/tasks/dependencies.js index fea1e8255b..fdee5e7785 100644 --- a/src/Umbraco.Web.UI.Client/gulp/tasks/dependencies.js +++ b/src/Umbraco.Web.UI.Client/gulp/tasks/dependencies.js @@ -178,7 +178,7 @@ function dependencies() { "name": "flatpickr", "src": [ "./node_modules/flatpickr/dist/flatpickr.min.js", - "./node_modules/flatpickr/dist/flatpickr.css", + "./node_modules/flatpickr/dist/flatpickr.min.css", "./node_modules/flatpickr/dist/l10n/*.js" ], "base": "./node_modules/flatpickr/dist" @@ -248,7 +248,7 @@ function dependencies() { "name": "spectrum", "src": [ "./node_modules/spectrum-colorpicker2/dist/spectrum.js", - "./node_modules/spectrum-colorpicker2/dist/spectrum.css" + "./node_modules/spectrum-colorpicker2/dist/spectrum.min.css" ], "base": "./node_modules/spectrum-colorpicker2/dist" }, diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbcolorpicker.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbcolorpicker.directive.js index b8731c9c51..a6a26dbe57 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbcolorpicker.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbcolorpicker.directive.js @@ -1,4 +1,4 @@ -/** +/** @ngdoc directive @name umbraco.directives.directive:umbColorPicker @restrict E @@ -84,7 +84,7 @@ ctrl.$onInit = function () { // load the separate css for the editor to avoid it blocking our js loading - assetsService.loadCss("lib/spectrum/spectrum.css", $scope); + assetsService.loadCss("lib/spectrum/spectrum.min.css", $scope); // load the js file for the color picker assetsService.load([ diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbdatetimepicker.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbdatetimepicker.directive.js index ed9c011d72..63319fedc2 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbdatetimepicker.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbdatetimepicker.directive.js @@ -100,7 +100,7 @@ Use this directive to render a date time picker ctrl.$onInit = function () { // load css file for the date picker - assetsService.loadCss('lib/flatpickr/flatpickr.css', $scope).then(function () { + assetsService.loadCss('lib/flatpickr/flatpickr.min.css', $scope).then(function () { userService.getCurrentUser().then(function (user) { // init date picker diff --git a/src/Umbraco.Web.UI.Client/src/index.html b/src/Umbraco.Web.UI.Client/src/index.html index ba330853fc..a74540ab74 100644 --- a/src/Umbraco.Web.UI.Client/src/index.html +++ b/src/Umbraco.Web.UI.Client/src/index.html @@ -1,4 +1,4 @@ - + @@ -6,7 +6,7 @@ Umbraco - + diff --git a/src/Umbraco.Web.UI.Client/src/views/content/umbpreview.html b/src/Umbraco.Web.UI.Client/src/views/content/umbpreview.html index 3cdf1450fb..f2693ec57a 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/umbpreview.html +++ b/src/Umbraco.Web.UI.Client/src/views/content/umbpreview.html @@ -2,7 +2,7 @@ Loading - + diff --git a/src/Umbraco.Web.UI.NetCore/umbraco/UmbracoInstall/Index.cshtml b/src/Umbraco.Web.UI.NetCore/umbraco/UmbracoInstall/Index.cshtml index 605e355894..faa649d09d 100644 --- a/src/Umbraco.Web.UI.NetCore/umbraco/UmbracoInstall/Index.cshtml +++ b/src/Umbraco.Web.UI.NetCore/umbraco/UmbracoInstall/Index.cshtml @@ -10,7 +10,7 @@ Install Umbraco - + From 80aaf8b7f6bf3fcd9e36ed2c20377c6405f39b61 Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Mon, 22 Mar 2021 08:52:48 +0100 Subject: [PATCH 087/188] Added new stage --- build/azure-pipelines.yml | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/build/azure-pipelines.yml b/build/azure-pipelines.yml index a41d0f5b7f..80cebd8143 100644 --- a/build/azure-pipelines.yml +++ b/build/azure-pipelines.yml @@ -99,6 +99,39 @@ stages: inputs: command: test projects: '**\Umbraco.Tests.Integration.csproj' + - stage: Acceptance_Tests + displayName: Acceptance Tests + dependsOn: [] + jobs: + - job: Windows + displayName: Windows + pool: + vmImage: windows-latest + steps: + - task: UseDotNet@2 + displayName: Use .Net Core sdk 5.x + inputs: + version: 5.x + - task: DotNetCoreCLI@2 + displayName: dotnet build + inputs: + command: build + projects: '**/Umbraco.Web.UI.Netcore.csproj' + - task: NodeTool@0 + displayName: Use Node 11.x + inputs: + versionSpec: 11.x + - task: Npm@1 + displayName: npm install + inputs: + workingDir: src\Umbraco.Web.UI.Client + verbose: false + - task: gulp@0 + displayName: gulp build + inputs: + gulpFile: src\Umbraco.Web.UI.Client\gulpfile.js + targets: build + workingDirectory: src\Umbraco.Web.UI.Client - stage: Artifacts dependsOn: [] jobs: From c9ba15ebd42c321e0ba22ee1bc94df774f71896c Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Mon, 22 Mar 2021 08:54:27 +0100 Subject: [PATCH 088/188] fix indentation --- build/azure-pipelines.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build/azure-pipelines.yml b/build/azure-pipelines.yml index 80cebd8143..cfa29dfd48 100644 --- a/build/azure-pipelines.yml +++ b/build/azure-pipelines.yml @@ -120,7 +120,7 @@ stages: - task: NodeTool@0 displayName: Use Node 11.x inputs: - versionSpec: 11.x + versionSpec: 11.x - task: Npm@1 displayName: npm install inputs: From f8d50694dd4051c84bd3c9ada70f144accc075f2 Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Mon, 22 Mar 2021 08:59:17 +0100 Subject: [PATCH 089/188] Added TreeAlias to TreeNodesRenderingNotification (#10024) --- src/Umbraco.Web.BackOffice/Trees/TreeControllerBase.cs | 3 +-- .../Trees/TreeNodesRenderingNotification.cs | 8 +++++++- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/Umbraco.Web.BackOffice/Trees/TreeControllerBase.cs b/src/Umbraco.Web.BackOffice/Trees/TreeControllerBase.cs index c0d0550168..ecebb0b041 100644 --- a/src/Umbraco.Web.BackOffice/Trees/TreeControllerBase.cs +++ b/src/Umbraco.Web.BackOffice/Trees/TreeControllerBase.cs @@ -12,7 +12,6 @@ using Umbraco.Cms.Web.BackOffice.Controllers; using Umbraco.Cms.Web.Common.Filters; using Umbraco.Cms.Web.Common.ModelBinders; using Umbraco.Extensions; -using Constants = Umbraco.Cms.Core.Constants; namespace Umbraco.Cms.Web.BackOffice.Trees { @@ -150,7 +149,7 @@ namespace Umbraco.Cms.Web.BackOffice.Trees node.RoutePath = "#"; //raise the event - await _eventAggregator.PublishAsync(new TreeNodesRenderingNotification(nodes, queryStrings)); + await _eventAggregator.PublishAsync(new TreeNodesRenderingNotification(nodes, queryStrings, TreeAlias)); return nodes; } diff --git a/src/Umbraco.Web.BackOffice/Trees/TreeNodesRenderingNotification.cs b/src/Umbraco.Web.BackOffice/Trees/TreeNodesRenderingNotification.cs index 1269d2aeab..3b1b7816d6 100644 --- a/src/Umbraco.Web.BackOffice/Trees/TreeNodesRenderingNotification.cs +++ b/src/Umbraco.Web.BackOffice/Trees/TreeNodesRenderingNotification.cs @@ -22,10 +22,16 @@ namespace Umbraco.Cms.Web.BackOffice.Trees ///
public FormCollection QueryString { get; } - public TreeNodesRenderingNotification(TreeNodeCollection nodes, FormCollection queryString) + /// + /// The alias of the tree rendered + /// + public string TreeAlias { get; } + + public TreeNodesRenderingNotification(TreeNodeCollection nodes, FormCollection queryString, string treeAlias) { Nodes = nodes; QueryString = queryString; + TreeAlias = treeAlias; } } } From 2abdc8178f2b5d40154b3acc8e60c9260ac8b458 Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Mon, 22 Mar 2021 09:26:40 +0100 Subject: [PATCH 090/188] Try to run dotnet run in process --- build/azure-pipelines.yml | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/build/azure-pipelines.yml b/build/azure-pipelines.yml index cfa29dfd48..13ba6b377f 100644 --- a/build/azure-pipelines.yml +++ b/build/azure-pipelines.yml @@ -132,6 +132,19 @@ stages: gulpFile: src\Umbraco.Web.UI.Client\gulpfile.js targets: build workingDirectory: src\Umbraco.Web.UI.Client + - task: PowerShell@1 + displayName: dotnet run + inputs: + scriptType: inlineScript + inlineScript: > + cd src\Umbraco.Web.UI.Netcore + Start-Process -FilePath "dotnet" -ArgumentList "run" + - task: PowerShell@1 + displayName: whatever + inputs: + scriptType: inlineScript + inlineScript: > + Write-Host "Hello World" - stage: Artifacts dependsOn: [] jobs: From 911e0f3837c5495fa1577828756f6028156a3280 Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Mon, 22 Mar 2021 09:58:02 +0100 Subject: [PATCH 091/188] added environment variables --- build/azure-pipelines.yml | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/build/azure-pipelines.yml b/build/azure-pipelines.yml index 13ba6b377f..8aabaa3f9c 100644 --- a/build/azure-pipelines.yml +++ b/build/azure-pipelines.yml @@ -15,6 +15,7 @@ resources: stages: - stage: Unit_Tests displayName: Unit Tests + dependsOn: [] jobs: - job: Linux_Unit_Tests @@ -102,6 +103,17 @@ stages: - stage: Acceptance_Tests displayName: Acceptance Tests dependsOn: [] + variables: + - name: Umbraco__CMS__Unattended__InstallUnattended + value: true + - name: Umbraco__CMS__Unattended__UnattendedUserName + value: Cypress Test + - name: Umbraco__CMS__Unattended__UnattendedUserEmail + value: cypress@umbraco.com + - name: Umbraco__CMS__Unattended__UnattendedUserPassword + value: abc123ABC!!! + - name: ConnectionStrings__umbracoDbDSN + value: Server=(LocalDB)\\MSSQLLocalDB;Database=Cypress;Integrated Security=true jobs: - job: Windows displayName: Windows @@ -137,14 +149,18 @@ stages: inputs: scriptType: inlineScript inlineScript: > - cd src\Umbraco.Web.UI.Netcore - Start-Process -FilePath "dotnet" -ArgumentList "run" + Start-Process -FilePath "dotnet" -ArgumentList "run", "src\Umbraco.Web.UI.Netcore\Umbraco.Web.UI.Netcore.csproj" - task: PowerShell@1 displayName: whatever inputs: scriptType: inlineScript inlineScript: > Write-Host "Hello World" + - task: Npm@1 + displayName: npm install + inputs: + workingDir: src\Umbraco.Tests.AcceptanceTest + verbose: false - stage: Artifacts dependsOn: [] jobs: From 7ce2bc4a561fd1c5373928973ab68f24a406a99d Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Mon, 22 Mar 2021 10:09:50 +0100 Subject: [PATCH 092/188] run cypress --- build/azure-pipelines.yml | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/build/azure-pipelines.yml b/build/azure-pipelines.yml index 8aabaa3f9c..cf06852531 100644 --- a/build/azure-pipelines.yml +++ b/build/azure-pipelines.yml @@ -15,7 +15,6 @@ resources: stages: - stage: Unit_Tests displayName: Unit Tests - dependsOn: [] jobs: - job: Linux_Unit_Tests @@ -125,7 +124,7 @@ stages: inputs: version: 5.x - task: DotNetCoreCLI@2 - displayName: dotnet build + displayName: dotnet build (Netcore) inputs: command: build projects: '**/Umbraco.Web.UI.Netcore.csproj' @@ -134,7 +133,7 @@ stages: inputs: versionSpec: 11.x - task: Npm@1 - displayName: npm install + displayName: npm install (Client) inputs: workingDir: src\Umbraco.Web.UI.Client verbose: false @@ -145,7 +144,7 @@ stages: targets: build workingDirectory: src\Umbraco.Web.UI.Client - task: PowerShell@1 - displayName: dotnet run + displayName: dotnet run (Netcore) inputs: scriptType: inlineScript inlineScript: > @@ -155,12 +154,16 @@ stages: inputs: scriptType: inlineScript inlineScript: > - Write-Host "Hello World" + @{ username = $env:Umbraco__CMS__Unattended__UnattendedUserEmail; password = $env:Umbraco__CMS__Unattended__UnattendedUserPassword } | ConvertTo-Json | Set-Content -Path "src\Umbraco.Tests.AcceptanceTest\cypress.env.json" - task: Npm@1 - displayName: npm install + displayName: npm install (AcceptanceTest) inputs: - workingDir: src\Umbraco.Tests.AcceptanceTest - verbose: false + workingDir: src\Umbraco.Tests.AcceptanceTest + verbose: false + - task: Npm@1 + inputs: + command: 'custom' + customCommand: 'run test' - stage: Artifacts dependsOn: [] jobs: From b2a62c83bb9874dad3cfc73c125035618ce3f332 Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Mon, 22 Mar 2021 10:14:45 +0100 Subject: [PATCH 093/188] Update port --- build/azure-pipelines.yml | 2 +- src/Umbraco.Web.UI.NetCore/Properties/launchSettings.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/build/azure-pipelines.yml b/build/azure-pipelines.yml index cf06852531..38583852d4 100644 --- a/build/azure-pipelines.yml +++ b/build/azure-pipelines.yml @@ -148,7 +148,7 @@ stages: inputs: scriptType: inlineScript inlineScript: > - Start-Process -FilePath "dotnet" -ArgumentList "run", "src\Umbraco.Web.UI.Netcore\Umbraco.Web.UI.Netcore.csproj" + Start-Process -FilePath "dotnet" -ArgumentList "run", "-p", "src\Umbraco.Web.UI.Netcore\Umbraco.Web.UI.Netcore.csproj" - task: PowerShell@1 displayName: whatever inputs: diff --git a/src/Umbraco.Web.UI.NetCore/Properties/launchSettings.json b/src/Umbraco.Web.UI.NetCore/Properties/launchSettings.json index b16945dcb0..4edb56f7fa 100644 --- a/src/Umbraco.Web.UI.NetCore/Properties/launchSettings.json +++ b/src/Umbraco.Web.UI.NetCore/Properties/launchSettings.json @@ -20,7 +20,7 @@ "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" }, - "applicationUrl": "https://localhost:44354;http://localhost:9000" + "applicationUrl": "https://localhost:44331;http://localhost:9000" } } } From a5cf66c25b06b3f51718c1be86ac2869c6a67514 Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Mon, 22 Mar 2021 10:23:45 +0100 Subject: [PATCH 094/188] Better displayNames --- build/azure-pipelines.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/build/azure-pipelines.yml b/build/azure-pipelines.yml index 38583852d4..d78a0a30c2 100644 --- a/build/azure-pipelines.yml +++ b/build/azure-pipelines.yml @@ -150,7 +150,7 @@ stages: inlineScript: > Start-Process -FilePath "dotnet" -ArgumentList "run", "-p", "src\Umbraco.Web.UI.Netcore\Umbraco.Web.UI.Netcore.csproj" - task: PowerShell@1 - displayName: whatever + displayName: Generate Cypress.env.json inputs: scriptType: inlineScript inlineScript: > @@ -161,6 +161,7 @@ stages: workingDir: src\Umbraco.Tests.AcceptanceTest verbose: false - task: Npm@1 + displayName: npm run test (AcceptanceTest) inputs: command: 'custom' customCommand: 'run test' From 64926795bf877b5d70f73a9dbedc43c3f8c16274 Mon Sep 17 00:00:00 2001 From: Mole Date: Mon, 22 Mar 2021 10:29:58 +0100 Subject: [PATCH 095/188] Split Acceptance_Tests into jobs --- build/azure-pipelines.yml | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/build/azure-pipelines.yml b/build/azure-pipelines.yml index 38583852d4..750de75448 100644 --- a/build/azure-pipelines.yml +++ b/build/azure-pipelines.yml @@ -128,6 +128,10 @@ stages: inputs: command: build projects: '**/Umbraco.Web.UI.Netcore.csproj' + - job: Install_client + displayName: Install Client + dependsOn: Windows + steps: - task: NodeTool@0 displayName: Use Node 11.x inputs: @@ -149,8 +153,12 @@ stages: scriptType: inlineScript inlineScript: > Start-Process -FilePath "dotnet" -ArgumentList "run", "-p", "src\Umbraco.Web.UI.Netcore\Umbraco.Web.UI.Netcore.csproj" + - job: Install_Cypress + displayName: Install Cypress + dependsOn: Windows + steps: - task: PowerShell@1 - displayName: whatever + displayName: Update cypress.env inputs: scriptType: inlineScript inlineScript: > @@ -160,6 +168,12 @@ stages: inputs: workingDir: src\Umbraco.Tests.AcceptanceTest verbose: false + - job: Run_Tests + displayName: Run acceptance tests + dependsOn: + - Install_client + - Install_Cypress + steps: - task: Npm@1 inputs: command: 'custom' From 0a76b7fb0333e3ba297768dab4fbd06294d23188 Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Mon, 22 Mar 2021 10:30:45 +0100 Subject: [PATCH 096/188] Right folder --- build/azure-pipelines.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/build/azure-pipelines.yml b/build/azure-pipelines.yml index d78a0a30c2..df077468ab 100644 --- a/build/azure-pipelines.yml +++ b/build/azure-pipelines.yml @@ -163,6 +163,7 @@ stages: - task: Npm@1 displayName: npm run test (AcceptanceTest) inputs: + workingDir: src\Umbraco.Tests.AcceptanceTest command: 'custom' customCommand: 'run test' - stage: Artifacts From dba80e1c8a1396098de905707f0306d8ec5a75dd Mon Sep 17 00:00:00 2001 From: Mole Date: Mon, 22 Mar 2021 10:39:25 +0100 Subject: [PATCH 097/188] Add vmImage --- build/azure-pipelines.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/build/azure-pipelines.yml b/build/azure-pipelines.yml index 99186850a9..ff683eafc3 100644 --- a/build/azure-pipelines.yml +++ b/build/azure-pipelines.yml @@ -131,6 +131,8 @@ stages: - job: Install_client displayName: Install Client dependsOn: Windows + pool: + vmImage: windows-latest steps: - task: NodeTool@0 displayName: Use Node 11.x @@ -156,6 +158,8 @@ stages: - job: Install_Cypress displayName: Install Cypress dependsOn: Windows + pool: + vmImage: windows-latest steps: - task: PowerShell@1 displayName: Generate Cypress.env.json @@ -173,6 +177,8 @@ stages: dependsOn: - Install_client - Install_Cypress + pool: + vmImage: windows-latest steps: - task: Npm@1 displayName: npm run test (AcceptanceTest) From 3936ca070f6575e1acb527fbcba83248a2c888e9 Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Mon, 22 Mar 2021 10:51:33 +0100 Subject: [PATCH 098/188] Start localdb --- build/azure-pipelines.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/build/azure-pipelines.yml b/build/azure-pipelines.yml index ff683eafc3..75c601975d 100644 --- a/build/azure-pipelines.yml +++ b/build/azure-pipelines.yml @@ -114,8 +114,8 @@ stages: - name: ConnectionStrings__umbracoDbDSN value: Server=(LocalDB)\\MSSQLLocalDB;Database=Cypress;Integrated Security=true jobs: - - job: Windows - displayName: Windows + - job: dotnet_build + displayName: Build C# pool: vmImage: windows-latest steps: @@ -128,6 +128,8 @@ stages: inputs: command: build projects: '**/Umbraco.Web.UI.Netcore.csproj' + - powershell: sqllocaldb start mssqllocaldb + displayName: Start MSSQL LocalDb - job: Install_client displayName: Install Client dependsOn: Windows From 0e07a898ce51f3845906f3fcda469047028814d4 Mon Sep 17 00:00:00 2001 From: Mole Date: Mon, 22 Mar 2021 11:05:30 +0100 Subject: [PATCH 099/188] Add start local db and prefix jobs with Windows --- build/azure-pipelines.yml | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/build/azure-pipelines.yml b/build/azure-pipelines.yml index ff683eafc3..76b392a11e 100644 --- a/build/azure-pipelines.yml +++ b/build/azure-pipelines.yml @@ -114,8 +114,8 @@ stages: - name: ConnectionStrings__umbracoDbDSN value: Server=(LocalDB)\\MSSQLLocalDB;Database=Cypress;Integrated Security=true jobs: - - job: Windows - displayName: Windows + - job: Windows_Dotnet_build + displayName: Windows Dotnet build pool: vmImage: windows-latest steps: @@ -123,14 +123,16 @@ stages: displayName: Use .Net Core sdk 5.x inputs: version: 5.x + - powershell: sqllocaldb start mssqllocaldb + displayName: Start MSSQL LocalDb - task: DotNetCoreCLI@2 displayName: dotnet build (Netcore) inputs: command: build projects: '**/Umbraco.Web.UI.Netcore.csproj' - - job: Install_client - displayName: Install Client - dependsOn: Windows + - job: Windows_Install_client + displayName: Windows Install Client + dependsOn: Windows_Dotnet_build pool: vmImage: windows-latest steps: @@ -155,9 +157,9 @@ stages: scriptType: inlineScript inlineScript: > Start-Process -FilePath "dotnet" -ArgumentList "run", "-p", "src\Umbraco.Web.UI.Netcore\Umbraco.Web.UI.Netcore.csproj" - - job: Install_Cypress - displayName: Install Cypress - dependsOn: Windows + - job: Windows_Install_Cypress + displayName: Windows Install Cypress + dependsOn: Windows_Dotnet_build pool: vmImage: windows-latest steps: @@ -172,8 +174,8 @@ stages: inputs: workingDir: src\Umbraco.Tests.AcceptanceTest verbose: false - - job: Run_Tests - displayName: Run acceptance tests + - job: Windows_Run_Tests + displayName: Windows Run Acceptance Tests dependsOn: - Install_client - Install_Cypress From 7dd7d4a5b87c48458bfb175e743ab2ac06b9366f Mon Sep 17 00:00:00 2001 From: Mole Date: Mon, 22 Mar 2021 11:07:09 +0100 Subject: [PATCH 100/188] Fix indentation --- build/azure-pipelines.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build/azure-pipelines.yml b/build/azure-pipelines.yml index 76b392a11e..1a0e76863a 100644 --- a/build/azure-pipelines.yml +++ b/build/azure-pipelines.yml @@ -124,7 +124,7 @@ stages: inputs: version: 5.x - powershell: sqllocaldb start mssqllocaldb - displayName: Start MSSQL LocalDb + displayName: Start MSSQL LocalDb - task: DotNetCoreCLI@2 displayName: dotnet build (Netcore) inputs: From f86275e77d611f9ed6c64bfe41e5e9a20b28bbf1 Mon Sep 17 00:00:00 2001 From: Mole Date: Mon, 22 Mar 2021 11:08:25 +0100 Subject: [PATCH 101/188] Fix dependsOn --- build/azure-pipelines.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/build/azure-pipelines.yml b/build/azure-pipelines.yml index 1a0e76863a..ddce010cbf 100644 --- a/build/azure-pipelines.yml +++ b/build/azure-pipelines.yml @@ -130,7 +130,7 @@ stages: inputs: command: build projects: '**/Umbraco.Web.UI.Netcore.csproj' - - job: Windows_Install_client + - job: Windows_Install_Client displayName: Windows Install Client dependsOn: Windows_Dotnet_build pool: @@ -177,8 +177,8 @@ stages: - job: Windows_Run_Tests displayName: Windows Run Acceptance Tests dependsOn: - - Install_client - - Install_Cypress + - Windows_Install_Client + - Windows_Install_Cypress pool: vmImage: windows-latest steps: From 804e8853a7a38ef1f8d03beb3663c060dd05323d Mon Sep 17 00:00:00 2001 From: Mole Date: Mon, 22 Mar 2021 11:13:31 +0100 Subject: [PATCH 102/188] Run install_client and install_cypress while building --- build/azure-pipelines.yml | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/build/azure-pipelines.yml b/build/azure-pipelines.yml index ddce010cbf..bc068a4e4d 100644 --- a/build/azure-pipelines.yml +++ b/build/azure-pipelines.yml @@ -114,8 +114,8 @@ stages: - name: ConnectionStrings__umbracoDbDSN value: Server=(LocalDB)\\MSSQLLocalDB;Database=Cypress;Integrated Security=true jobs: - - job: Windows_Dotnet_build - displayName: Windows Dotnet build + - job: Windows_Dotnet_Build + displayName: Windows Dotnet Build pool: vmImage: windows-latest steps: @@ -132,7 +132,6 @@ stages: projects: '**/Umbraco.Web.UI.Netcore.csproj' - job: Windows_Install_Client displayName: Windows Install Client - dependsOn: Windows_Dotnet_build pool: vmImage: windows-latest steps: @@ -159,7 +158,6 @@ stages: Start-Process -FilePath "dotnet" -ArgumentList "run", "-p", "src\Umbraco.Web.UI.Netcore\Umbraco.Web.UI.Netcore.csproj" - job: Windows_Install_Cypress displayName: Windows Install Cypress - dependsOn: Windows_Dotnet_build pool: vmImage: windows-latest steps: @@ -177,6 +175,7 @@ stages: - job: Windows_Run_Tests displayName: Windows Run Acceptance Tests dependsOn: + - Windows_Dotnet_Build - Windows_Install_Client - Windows_Install_Cypress pool: From 9317e45dc67446d1cfa836eca8e761802ca4712b Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Mon, 22 Mar 2021 11:20:23 +0100 Subject: [PATCH 103/188] Move dotnet run --- build/azure-pipelines.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/build/azure-pipelines.yml b/build/azure-pipelines.yml index bc068a4e4d..069f8e99fc 100644 --- a/build/azure-pipelines.yml +++ b/build/azure-pipelines.yml @@ -150,12 +150,6 @@ stages: gulpFile: src\Umbraco.Web.UI.Client\gulpfile.js targets: build workingDirectory: src\Umbraco.Web.UI.Client - - task: PowerShell@1 - displayName: dotnet run (Netcore) - inputs: - scriptType: inlineScript - inlineScript: > - Start-Process -FilePath "dotnet" -ArgumentList "run", "-p", "src\Umbraco.Web.UI.Netcore\Umbraco.Web.UI.Netcore.csproj" - job: Windows_Install_Cypress displayName: Windows Install Cypress pool: @@ -181,6 +175,12 @@ stages: pool: vmImage: windows-latest steps: + - task: PowerShell@1 + displayName: dotnet run (Netcore) + inputs: + scriptType: inlineScript + inlineScript: > + Start-Process -FilePath "dotnet" -ArgumentList "run", "--no-build", "-p", "src\Umbraco.Web.UI.Netcore\Umbraco.Web.UI.Netcore.csproj" - task: Npm@1 displayName: npm run test (AcceptanceTest) inputs: From ff51b477ebb58750630595a1bab58a779e2181d4 Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Mon, 22 Mar 2021 11:22:23 +0100 Subject: [PATCH 104/188] Move dotnet run --- build/azure-pipelines.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build/azure-pipelines.yml b/build/azure-pipelines.yml index 069f8e99fc..47e2744c18 100644 --- a/build/azure-pipelines.yml +++ b/build/azure-pipelines.yml @@ -123,8 +123,6 @@ stages: displayName: Use .Net Core sdk 5.x inputs: version: 5.x - - powershell: sqllocaldb start mssqllocaldb - displayName: Start MSSQL LocalDb - task: DotNetCoreCLI@2 displayName: dotnet build (Netcore) inputs: @@ -175,6 +173,8 @@ stages: pool: vmImage: windows-latest steps: + - powershell: sqllocaldb start mssqllocaldb + displayName: Start MSSQL LocalDb - task: PowerShell@1 displayName: dotnet run (Netcore) inputs: From 131f731b4ce634498639c225566c7f68fef06be8 Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Mon, 22 Mar 2021 11:59:50 +0100 Subject: [PATCH 105/188] sleep and invoke web request --- build/azure-pipelines.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/build/azure-pipelines.yml b/build/azure-pipelines.yml index 47e2744c18..27e0e88025 100644 --- a/build/azure-pipelines.yml +++ b/build/azure-pipelines.yml @@ -181,6 +181,8 @@ stages: scriptType: inlineScript inlineScript: > Start-Process -FilePath "dotnet" -ArgumentList "run", "--no-build", "-p", "src\Umbraco.Web.UI.Netcore\Umbraco.Web.UI.Netcore.csproj" + Start-Sleep -s 15 + Invoke-WebRequest https://localhost:44331 -TimeoutSec 120 - task: Npm@1 displayName: npm run test (AcceptanceTest) inputs: From b1e354778d502bb97d39c3dc81dcc16918ad63fb Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Mon, 22 Mar 2021 12:22:08 +0100 Subject: [PATCH 106/188] Update azure-pipelines.yml for Azure Pipelines --- build/azure-pipelines.yml | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/build/azure-pipelines.yml b/build/azure-pipelines.yml index 27e0e88025..603832b08d 100644 --- a/build/azure-pipelines.yml +++ b/build/azure-pipelines.yml @@ -110,7 +110,7 @@ stages: - name: Umbraco__CMS__Unattended__UnattendedUserEmail value: cypress@umbraco.com - name: Umbraco__CMS__Unattended__UnattendedUserPassword - value: abc123ABC!!! + value: UmbracoAcceptance123! - name: ConnectionStrings__umbracoDbDSN value: Server=(LocalDB)\\MSSQLLocalDB;Database=Cypress;Integrated Security=true jobs: @@ -175,14 +175,12 @@ stages: steps: - powershell: sqllocaldb start mssqllocaldb displayName: Start MSSQL LocalDb - - task: PowerShell@1 + - powershell: Start-Process -FilePath "dotnet" -ArgumentList "run", "--no-build", "-p", "src\Umbraco.Web.UI.Netcore\Umbraco.Web.UI.Netcore.csproj" displayName: dotnet run (Netcore) - inputs: - scriptType: inlineScript - inlineScript: > - Start-Process -FilePath "dotnet" -ArgumentList "run", "--no-build", "-p", "src\Umbraco.Web.UI.Netcore\Umbraco.Web.UI.Netcore.csproj" - Start-Sleep -s 15 - Invoke-WebRequest https://localhost:44331 -TimeoutSec 120 + - powershell: Start-Sleep -s 15 + displayName: Sleep 15 sec + - powershell: Invoke-WebRequest https://localhost:44331 -TimeoutSec 120 + displayName: Call website - task: Npm@1 displayName: npm run test (AcceptanceTest) inputs: From b921d5b7709895e110662c36133fdec7dc8de0a4 Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Mon, 22 Mar 2021 12:42:33 +0100 Subject: [PATCH 107/188] Update azure-pipelines.yml for Azure Pipelines --- build/azure-pipelines.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/build/azure-pipelines.yml b/build/azure-pipelines.yml index 603832b08d..48c5780b31 100644 --- a/build/azure-pipelines.yml +++ b/build/azure-pipelines.yml @@ -175,7 +175,9 @@ stages: steps: - powershell: sqllocaldb start mssqllocaldb displayName: Start MSSQL LocalDb - - powershell: Start-Process -FilePath "dotnet" -ArgumentList "run", "--no-build", "-p", "src\Umbraco.Web.UI.Netcore\Umbraco.Web.UI.Netcore.csproj" + #- powershell: Start-Process -FilePath "dotnet" -ArgumentList "run", "--no-build", "-p", "src\Umbraco.Web.UI.Netcore\Umbraco.Web.UI.Netcore.csproj" + # displayName: dotnet run (Netcore) + - powershell: dotnet run --no-build -p src\Umbraco.Web.UI.Netcore\Umbraco.Web.UI.Netcore.csproj displayName: dotnet run (Netcore) - powershell: Start-Sleep -s 15 displayName: Sleep 15 sec From 95724e24f4703c3a9571bd52095329d7e8c1c6cf Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Mon, 22 Mar 2021 13:06:14 +0100 Subject: [PATCH 108/188] remove jobs for acceptance tests --- build/azure-pipelines.yml | 37 ++++++++----------------------------- 1 file changed, 8 insertions(+), 29 deletions(-) diff --git a/build/azure-pipelines.yml b/build/azure-pipelines.yml index 48c5780b31..c51173cd10 100644 --- a/build/azure-pipelines.yml +++ b/build/azure-pipelines.yml @@ -114,8 +114,8 @@ stages: - name: ConnectionStrings__umbracoDbDSN value: Server=(LocalDB)\\MSSQLLocalDB;Database=Cypress;Integrated Security=true jobs: - - job: Windows_Dotnet_Build - displayName: Windows Dotnet Build + - job: Windows_Acceptance_tests + displayName: Windows pool: vmImage: windows-latest steps: @@ -123,16 +123,13 @@ stages: displayName: Use .Net Core sdk 5.x inputs: version: 5.x + - powershell: sqllocaldb start mssqllocaldb + displayName: Start MSSQL LocalDb - task: DotNetCoreCLI@2 displayName: dotnet build (Netcore) inputs: command: build projects: '**/Umbraco.Web.UI.Netcore.csproj' - - job: Windows_Install_Client - displayName: Windows Install Client - pool: - vmImage: windows-latest - steps: - task: NodeTool@0 displayName: Use Node 11.x inputs: @@ -148,11 +145,10 @@ stages: gulpFile: src\Umbraco.Web.UI.Client\gulpfile.js targets: build workingDirectory: src\Umbraco.Web.UI.Client - - job: Windows_Install_Cypress - displayName: Windows Install Cypress - pool: - vmImage: windows-latest - steps: + #- powershell: Start-Process -FilePath "dotnet" -ArgumentList "run", "--no-build", "-p", "src\Umbraco.Web.UI.Netcore\Umbraco.Web.UI.Netcore.csproj" + # displayName: dotnet run (Netcore) + - powershell: dotnet run --no-build -p .\src\Umbraco.Web.UI.NetCore\Umbraco.Web.UI.NetCore.csproj + displayName: dotnet run (Netcore) - task: PowerShell@1 displayName: Generate Cypress.env.json inputs: @@ -164,23 +160,6 @@ stages: inputs: workingDir: src\Umbraco.Tests.AcceptanceTest verbose: false - - job: Windows_Run_Tests - displayName: Windows Run Acceptance Tests - dependsOn: - - Windows_Dotnet_Build - - Windows_Install_Client - - Windows_Install_Cypress - pool: - vmImage: windows-latest - steps: - - powershell: sqllocaldb start mssqllocaldb - displayName: Start MSSQL LocalDb - #- powershell: Start-Process -FilePath "dotnet" -ArgumentList "run", "--no-build", "-p", "src\Umbraco.Web.UI.Netcore\Umbraco.Web.UI.Netcore.csproj" - # displayName: dotnet run (Netcore) - - powershell: dotnet run --no-build -p src\Umbraco.Web.UI.Netcore\Umbraco.Web.UI.Netcore.csproj - displayName: dotnet run (Netcore) - - powershell: Start-Sleep -s 15 - displayName: Sleep 15 sec - powershell: Invoke-WebRequest https://localhost:44331 -TimeoutSec 120 displayName: Call website - task: Npm@1 From 6b79ef18bc39677e66c9ed9a5598ab8fcc5dc37b Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Mon, 22 Mar 2021 13:18:11 +0100 Subject: [PATCH 109/188] casing of local db instance name --- build/azure-pipelines.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build/azure-pipelines.yml b/build/azure-pipelines.yml index c51173cd10..956bb1e40b 100644 --- a/build/azure-pipelines.yml +++ b/build/azure-pipelines.yml @@ -112,7 +112,7 @@ stages: - name: Umbraco__CMS__Unattended__UnattendedUserPassword value: UmbracoAcceptance123! - name: ConnectionStrings__umbracoDbDSN - value: Server=(LocalDB)\\MSSQLLocalDB;Database=Cypress;Integrated Security=true + value: Server=(LocalDB)\\mssqllocaldb;Database=Cypress;Integrated Security=true jobs: - job: Windows_Acceptance_tests displayName: Windows From e9a3ed44cff03d8cb0a4dca79bd0a57f47782ba1 Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Mon, 22 Mar 2021 13:31:15 +0100 Subject: [PATCH 110/188] test other connectionstring --- build/azure-pipelines.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build/azure-pipelines.yml b/build/azure-pipelines.yml index 956bb1e40b..4c5108674d 100644 --- a/build/azure-pipelines.yml +++ b/build/azure-pipelines.yml @@ -112,7 +112,7 @@ stages: - name: Umbraco__CMS__Unattended__UnattendedUserPassword value: UmbracoAcceptance123! - name: ConnectionStrings__umbracoDbDSN - value: Server=(LocalDB)\\mssqllocaldb;Database=Cypress;Integrated Security=true + value: Server=(LocalDB)\\MSSQLLocalDB;Initial Catalog=Cypress;Integrated Security=true jobs: - job: Windows_Acceptance_tests displayName: Windows From cc221c684b712646bb673d6cba7f00b5b17f5474 Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Mon, 22 Mar 2021 14:11:09 +0100 Subject: [PATCH 111/188] nested variables names and create database --- build/azure-pipelines.yml | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/build/azure-pipelines.yml b/build/azure-pipelines.yml index 4c5108674d..d61723a46e 100644 --- a/build/azure-pipelines.yml +++ b/build/azure-pipelines.yml @@ -111,8 +111,12 @@ stages: value: cypress@umbraco.com - name: Umbraco__CMS__Unattended__UnattendedUserPassword value: UmbracoAcceptance123! + - name: UmbracoDatabaseServer + value: (LocalDB)\\MSSQLLocalDB + - name: UmbracoDatabaseName + value: Cypress - name: ConnectionStrings__umbracoDbDSN - value: Server=(LocalDB)\\MSSQLLocalDB;Initial Catalog=Cypress;Integrated Security=true + value: Server=$(UmbracoDatabaseServer);Database=$(UmbracoDatabaseName);Integrated Security=true; jobs: - job: Windows_Acceptance_tests displayName: Windows @@ -125,6 +129,8 @@ stages: version: 5.x - powershell: sqllocaldb start mssqllocaldb displayName: Start MSSQL LocalDb + - powershell: Invoke-Sqlcmd -Query "CREATE DATABASE $env:UmbracoDatabaseName" -ServerInstance $env:UmbracoDatabaseServer + displayName: Create database - task: DotNetCoreCLI@2 displayName: dotnet build (Netcore) inputs: From 204c476486aba2c0fbf0f5b6779814f4619deea7 Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Mon, 22 Mar 2021 14:20:30 +0100 Subject: [PATCH 112/188] syntax fix --- build/azure-pipelines.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/build/azure-pipelines.yml b/build/azure-pipelines.yml index d61723a46e..ddd45ae4bb 100644 --- a/build/azure-pipelines.yml +++ b/build/azure-pipelines.yml @@ -127,9 +127,10 @@ stages: displayName: Use .Net Core sdk 5.x inputs: version: 5.x + - powershell: sqllocaldb start mssqllocaldb displayName: Start MSSQL LocalDb - - powershell: Invoke-Sqlcmd -Query "CREATE DATABASE $env:UmbracoDatabaseName" -ServerInstance $env:UmbracoDatabaseServer + - powershell: Invoke-Sqlcmd -Query "CREATE DATABASE $($env:UmbracoDatabaseName)" -ServerInstance $env:UmbracoDatabaseServer displayName: Create database - task: DotNetCoreCLI@2 displayName: dotnet build (Netcore) From 3c3e22f7ffcf91d652c8124b7aeadf030adb07cf Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Mon, 22 Mar 2021 14:28:29 +0100 Subject: [PATCH 113/188] test with hardcoded values --- build/azure-pipelines.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build/azure-pipelines.yml b/build/azure-pipelines.yml index ddd45ae4bb..d519953c5c 100644 --- a/build/azure-pipelines.yml +++ b/build/azure-pipelines.yml @@ -130,7 +130,7 @@ stages: - powershell: sqllocaldb start mssqllocaldb displayName: Start MSSQL LocalDb - - powershell: Invoke-Sqlcmd -Query "CREATE DATABASE $($env:UmbracoDatabaseName)" -ServerInstance $env:UmbracoDatabaseServer + - powershell: Invoke-Sqlcmd -Query "CREATE DATABASE Cypress" -ServerInstance "(LocalDB)\\MSSQLLocalDB" displayName: Create database - task: DotNetCoreCLI@2 displayName: dotnet build (Netcore) From 0e14ab6b9ffea2725385b4db0553d15e0c566af3 Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Mon, 22 Mar 2021 14:34:03 +0100 Subject: [PATCH 114/188] Update azure-pipelines.yml for Azure Pipelines --- build/azure-pipelines.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build/azure-pipelines.yml b/build/azure-pipelines.yml index d519953c5c..b3895490e4 100644 --- a/build/azure-pipelines.yml +++ b/build/azure-pipelines.yml @@ -130,7 +130,7 @@ stages: - powershell: sqllocaldb start mssqllocaldb displayName: Start MSSQL LocalDb - - powershell: Invoke-Sqlcmd -Query "CREATE DATABASE Cypress" -ServerInstance "(LocalDB)\\MSSQLLocalDB" + - powershell: Invoke-Sqlcmd -Query "CREATE DATABASE Cypress" -ServerInstance "(LocalDB)\MSSQLLocalDB" displayName: Create database - task: DotNetCoreCLI@2 displayName: dotnet build (Netcore) From 74bfe973570ceaa34267ec32157e338038203af6 Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Mon, 22 Mar 2021 14:43:27 +0100 Subject: [PATCH 115/188] are \\ wrong for connectionstring.. --- build/azure-pipelines.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build/azure-pipelines.yml b/build/azure-pipelines.yml index b3895490e4..98c0588448 100644 --- a/build/azure-pipelines.yml +++ b/build/azure-pipelines.yml @@ -112,7 +112,7 @@ stages: - name: Umbraco__CMS__Unattended__UnattendedUserPassword value: UmbracoAcceptance123! - name: UmbracoDatabaseServer - value: (LocalDB)\\MSSQLLocalDB + value: (LocalDB)\MSSQLLocalDB - name: UmbracoDatabaseName value: Cypress - name: ConnectionStrings__umbracoDbDSN @@ -130,7 +130,7 @@ stages: - powershell: sqllocaldb start mssqllocaldb displayName: Start MSSQL LocalDb - - powershell: Invoke-Sqlcmd -Query "CREATE DATABASE Cypress" -ServerInstance "(LocalDB)\MSSQLLocalDB" + - powershell: Invoke-Sqlcmd -Query "CREATE DATABASE $($env:UmbracoDatabaseName)" -ServerInstance $env:UmbracoDatabaseServer displayName: Create database - task: DotNetCoreCLI@2 displayName: dotnet build (Netcore) From 9cd33c1dd428186f77676ed9fa2684f83e96fa0d Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Mon, 22 Mar 2021 14:51:40 +0100 Subject: [PATCH 116/188] Update azure-pipelines.yml for Azure Pipelines --- build/azure-pipelines.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build/azure-pipelines.yml b/build/azure-pipelines.yml index 98c0588448..a0626051e3 100644 --- a/build/azure-pipelines.yml +++ b/build/azure-pipelines.yml @@ -130,7 +130,7 @@ stages: - powershell: sqllocaldb start mssqllocaldb displayName: Start MSSQL LocalDb - - powershell: Invoke-Sqlcmd -Query "CREATE DATABASE $($env:UmbracoDatabaseName)" -ServerInstance $env:UmbracoDatabaseServer + - powershell: Invoke-Sqlcmd -Query "CREATE DATABASE $env:UmbracoDatabaseName" -ServerInstance $env:UmbracoDatabaseServer displayName: Create database - task: DotNetCoreCLI@2 displayName: dotnet build (Netcore) From 7d9efb014cc11e44b14e46c6c5195faa5ba79491 Mon Sep 17 00:00:00 2001 From: Mole Date: Mon, 22 Mar 2021 15:03:17 +0100 Subject: [PATCH 117/188] Run site in separate process again. --- build/azure-pipelines.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/build/azure-pipelines.yml b/build/azure-pipelines.yml index a0626051e3..50e79dbe72 100644 --- a/build/azure-pipelines.yml +++ b/build/azure-pipelines.yml @@ -152,10 +152,10 @@ stages: gulpFile: src\Umbraco.Web.UI.Client\gulpfile.js targets: build workingDirectory: src\Umbraco.Web.UI.Client - #- powershell: Start-Process -FilePath "dotnet" -ArgumentList "run", "--no-build", "-p", "src\Umbraco.Web.UI.Netcore\Umbraco.Web.UI.Netcore.csproj" - # displayName: dotnet run (Netcore) - - powershell: dotnet run --no-build -p .\src\Umbraco.Web.UI.NetCore\Umbraco.Web.UI.NetCore.csproj + - powershell: Start-Process -FilePath "dotnet" -ArgumentList "run", "--no-build", "-p", "src\Umbraco.Web.UI.Netcore\Umbraco.Web.UI.Netcore.csproj" displayName: dotnet run (Netcore) +# - powershell: dotnet run --no-build -p .\src\Umbraco.Web.UI.NetCore\Umbraco.Web.UI.NetCore.csproj +# displayName: dotnet run (Netcore) - task: PowerShell@1 displayName: Generate Cypress.env.json inputs: From 0b2a3753bd993f6e07fd628615f9999fa33d36b1 Mon Sep 17 00:00:00 2001 From: Mole Date: Mon, 22 Mar 2021 15:23:15 +0100 Subject: [PATCH 118/188] Try hack to get around ssl --- build/azure-pipelines.yml | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/build/azure-pipelines.yml b/build/azure-pipelines.yml index 50e79dbe72..6802eb04b5 100644 --- a/build/azure-pipelines.yml +++ b/build/azure-pipelines.yml @@ -167,8 +167,24 @@ stages: inputs: workingDir: src\Umbraco.Tests.AcceptanceTest verbose: false - - powershell: Invoke-WebRequest https://localhost:44331 -TimeoutSec 120 + - task: PowerShell@1 displayName: Call website + inputs: + scriptType: inlineScript + inlineScript: > + add-type @" + using System.Net; + using System.Security.Cryptography.X509Certificates; + public class TrustAllCertsPolicy : ICertificatePolicy { + public bool CheckValidationResult( + ServicePoint srvPoint, X509Certificate certificate, + WebRequest request, int certificateProblem) { + return true; + } + } + "@ + [System.Net.ServicePointManager]::CertificatePolicy = New-Object TrustAllCertsPolicy + Invoke-WebRequest https://localhost:44331 -TimeoutSec 120 - task: Npm@1 displayName: npm run test (AcceptanceTest) inputs: From c1d701efff296be0fc616d94e2d9eb0a69ebbf5d Mon Sep 17 00:00:00 2001 From: Mole Date: Mon, 22 Mar 2021 15:49:16 +0100 Subject: [PATCH 119/188] Try using -SkipCertificateCheck --- build/azure-pipelines.yml | 18 +----------------- 1 file changed, 1 insertion(+), 17 deletions(-) diff --git a/build/azure-pipelines.yml b/build/azure-pipelines.yml index 6802eb04b5..a6c83581f1 100644 --- a/build/azure-pipelines.yml +++ b/build/azure-pipelines.yml @@ -167,24 +167,8 @@ stages: inputs: workingDir: src\Umbraco.Tests.AcceptanceTest verbose: false - - task: PowerShell@1 + - powershell: Invoke-WebRequest https://localhost:44331 -SkipCertificateCheck -TimeoutSec 120 displayName: Call website - inputs: - scriptType: inlineScript - inlineScript: > - add-type @" - using System.Net; - using System.Security.Cryptography.X509Certificates; - public class TrustAllCertsPolicy : ICertificatePolicy { - public bool CheckValidationResult( - ServicePoint srvPoint, X509Certificate certificate, - WebRequest request, int certificateProblem) { - return true; - } - } - "@ - [System.Net.ServicePointManager]::CertificatePolicy = New-Object TrustAllCertsPolicy - Invoke-WebRequest https://localhost:44331 -TimeoutSec 120 - task: Npm@1 displayName: npm run test (AcceptanceTest) inputs: From eeb5c1db7c29572bbc5ea274d289ac67e159b19b Mon Sep 17 00:00:00 2001 From: Mole Date: Mon, 22 Mar 2021 16:34:00 +0100 Subject: [PATCH 120/188] Remove call website --- build/azure-pipelines.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build/azure-pipelines.yml b/build/azure-pipelines.yml index a6c83581f1..2ef5444f07 100644 --- a/build/azure-pipelines.yml +++ b/build/azure-pipelines.yml @@ -167,8 +167,8 @@ stages: inputs: workingDir: src\Umbraco.Tests.AcceptanceTest verbose: false - - powershell: Invoke-WebRequest https://localhost:44331 -SkipCertificateCheck -TimeoutSec 120 - displayName: Call website +# - powershell: Invoke-WebRequest https://localhost:44331 -SkipCertificateCheck -TimeoutSec 120 +# displayName: Call website - task: Npm@1 displayName: npm run test (AcceptanceTest) inputs: From b60fffe8e5b5e7eeaca1172226289b95e1fbf550 Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Mon, 22 Mar 2021 18:29:26 +0100 Subject: [PATCH 121/188] Run cypress with custom commands --- build/azure-pipelines.yml | 46 +++++++++++++++++++++++++++++++-------- 1 file changed, 37 insertions(+), 9 deletions(-) diff --git a/build/azure-pipelines.yml b/build/azure-pipelines.yml index 2ef5444f07..bc58dcb0bc 100644 --- a/build/azure-pipelines.yml +++ b/build/azure-pipelines.yml @@ -165,16 +165,44 @@ stages: - task: Npm@1 displayName: npm install (AcceptanceTest) inputs: - workingDir: src\Umbraco.Tests.AcceptanceTest - verbose: false -# - powershell: Invoke-WebRequest https://localhost:44331 -SkipCertificateCheck -TimeoutSec 120 -# displayName: Call website - - task: Npm@1 - displayName: npm run test (AcceptanceTest) + workingDir: + - task: PowerShell@2 + displayName: Run Cypress (Desktop) inputs: - workingDir: src\Umbraco.Tests.AcceptanceTest - command: 'custom' - customCommand: 'run test' + targetType: 'inline' + errorActionPreference: 'continue' + workingDirectory: 'src\Umbraco.Tests.AcceptanceTest' + script: | + node_modules/.bin/cypress run --reporter junit --reporter-options "mochaFile=results/test-output-D-[hash].xml,toConsole=true" --config videoUploadOnPasses=false,viewportHeight=1600,viewportWidth=2560,screenshotsFolder=cypress/artifacts/desktop/screenshots,videosFolder=cypress/artifacts/desktop/videos + - task: PowerShell@2 + displayName: Run Cypress (Tablet portrait) + inputs: + targetType: 'inline' + errorActionPreference: 'continue' + workingDirectory: 'src\Umbraco.Tests.AcceptanceTest' + script: | + node_modules/.bin/cypress run --reporter junit --reporter-options "mochaFile=results/test-output-T-[hash].xml,toConsole=true" --config videoUploadOnPasses=false,viewportHeight=1366,viewportWidth=1024,screenshotsFolder=cypress/artifacts/tablet/screenshots,videosFolder=cypress/artifacts/tablet/videos + - task: PowerShell@2 + displayName: Run Cypress (Mobile protrait) + inputs: + targetType: 'inline' + errorActionPreference: 'continue' + workingDirectory: 'src\Umbraco.Tests.AcceptanceTest' + script: | + node_modules/.bin/cypress run --reporter junit --reporter-options "mochaFile=results/test-output-M-[hash].xml,toConsole=true" --config videoUploadOnPasses=false,viewportHeight=812,viewportWidth=375,screenshotsFolder=cypress/artifacts/mobile/screenshots,videosFolder=cypress/artifacts/mobile/videos + - task: PublishTestResults@2 + inputs: + testResultsFormat: 'JUnit' + testResultsFiles: 'results/test-output-M-*.xml' + mergeTestResults: true + testRunTitle: "Test results Mobile" + # - task: Npm@1 + # displayName: npm run test (AcceptanceTest) + # inputs: + # workingDir: src\Umbraco.Tests.AcceptanceTest + # command: 'custom' + # customCommand: 'run test' + - stage: Artifacts dependsOn: [] jobs: From 31c786d9fd88e3b0ded35da33eac852edac8540b Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Mon, 22 Mar 2021 18:51:59 +0100 Subject: [PATCH 122/188] changed slash vs backslash --- build/azure-pipelines.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/build/azure-pipelines.yml b/build/azure-pipelines.yml index bc58dcb0bc..363cc2f82a 100644 --- a/build/azure-pipelines.yml +++ b/build/azure-pipelines.yml @@ -173,7 +173,7 @@ stages: errorActionPreference: 'continue' workingDirectory: 'src\Umbraco.Tests.AcceptanceTest' script: | - node_modules/.bin/cypress run --reporter junit --reporter-options "mochaFile=results/test-output-D-[hash].xml,toConsole=true" --config videoUploadOnPasses=false,viewportHeight=1600,viewportWidth=2560,screenshotsFolder=cypress/artifacts/desktop/screenshots,videosFolder=cypress/artifacts/desktop/videos + node_modules\.bin\cypress run --reporter=junit --reporter-options="mochaFile=results/test-output-D-[hash].xml,toConsole=true" --config="videoUploadOnPasses=false,viewportHeight=1600,viewportWidth=2560,screenshotsFolder=cypress/artifacts/desktop/screenshots,videosFolder=cypress/artifacts/desktop/videos" - task: PowerShell@2 displayName: Run Cypress (Tablet portrait) inputs: @@ -181,7 +181,7 @@ stages: errorActionPreference: 'continue' workingDirectory: 'src\Umbraco.Tests.AcceptanceTest' script: | - node_modules/.bin/cypress run --reporter junit --reporter-options "mochaFile=results/test-output-T-[hash].xml,toConsole=true" --config videoUploadOnPasses=false,viewportHeight=1366,viewportWidth=1024,screenshotsFolder=cypress/artifacts/tablet/screenshots,videosFolder=cypress/artifacts/tablet/videos + node_modules\.bin\cypress run --reporter=junit --reporter-options="mochaFile=results/test-output-T-[hash].xml,toConsole=true" --config="videoUploadOnPasses=false,viewportHeight=1366,viewportWidth=1024,screenshotsFolder=cypress/artifacts/tablet/screenshots,videosFolder=cypress/artifacts/tablet/videos" - task: PowerShell@2 displayName: Run Cypress (Mobile protrait) inputs: @@ -189,7 +189,7 @@ stages: errorActionPreference: 'continue' workingDirectory: 'src\Umbraco.Tests.AcceptanceTest' script: | - node_modules/.bin/cypress run --reporter junit --reporter-options "mochaFile=results/test-output-M-[hash].xml,toConsole=true" --config videoUploadOnPasses=false,viewportHeight=812,viewportWidth=375,screenshotsFolder=cypress/artifacts/mobile/screenshots,videosFolder=cypress/artifacts/mobile/videos + node_modules\.bin\cypress run --reporter=junit --reporter-options="mochaFile=results/test-output-M-[hash].xml,toConsole=true" --config="videoUploadOnPasses=false,viewportHeight=812,viewportWidth=375,screenshotsFolder=cypress/artifacts/mobile/screenshots,videosFolder=cypress/artifacts/mobile/videos" - task: PublishTestResults@2 inputs: testResultsFormat: 'JUnit' From 3ac15011c89227cf009bfe4d528c88537e99ccd3 Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Mon, 22 Mar 2021 19:12:31 +0100 Subject: [PATCH 123/188] Update azure-pipelines.yml for Azure Pipelines --- build/azure-pipelines.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/build/azure-pipelines.yml b/build/azure-pipelines.yml index 363cc2f82a..f13256b0b2 100644 --- a/build/azure-pipelines.yml +++ b/build/azure-pipelines.yml @@ -173,7 +173,7 @@ stages: errorActionPreference: 'continue' workingDirectory: 'src\Umbraco.Tests.AcceptanceTest' script: | - node_modules\.bin\cypress run --reporter=junit --reporter-options="mochaFile=results/test-output-D-[hash].xml,toConsole=true" --config="videoUploadOnPasses=false,viewportHeight=1600,viewportWidth=2560,screenshotsFolder=cypress/artifacts/desktop/screenshots,videosFolder=cypress/artifacts/desktop/videos" + .\node_modules\.bin\cypress run --reporter=junit --reporter-options="mochaFile=results/test-output-D-[hash].xml,toConsole=true" --config="videoUploadOnPasses=false,viewportHeight=1600,viewportWidth=2560,screenshotsFolder=cypress/artifacts/desktop/screenshots,videosFolder=cypress/artifacts/desktop/videos" - task: PowerShell@2 displayName: Run Cypress (Tablet portrait) inputs: @@ -181,7 +181,7 @@ stages: errorActionPreference: 'continue' workingDirectory: 'src\Umbraco.Tests.AcceptanceTest' script: | - node_modules\.bin\cypress run --reporter=junit --reporter-options="mochaFile=results/test-output-T-[hash].xml,toConsole=true" --config="videoUploadOnPasses=false,viewportHeight=1366,viewportWidth=1024,screenshotsFolder=cypress/artifacts/tablet/screenshots,videosFolder=cypress/artifacts/tablet/videos" + .\node_modules\.bin\cypress run --reporter=junit --reporter-options="mochaFile=results/test-output-T-[hash].xml,toConsole=true" --config="videoUploadOnPasses=false,viewportHeight=1366,viewportWidth=1024,screenshotsFolder=cypress/artifacts/tablet/screenshots,videosFolder=cypress/artifacts/tablet/videos" - task: PowerShell@2 displayName: Run Cypress (Mobile protrait) inputs: @@ -189,7 +189,7 @@ stages: errorActionPreference: 'continue' workingDirectory: 'src\Umbraco.Tests.AcceptanceTest' script: | - node_modules\.bin\cypress run --reporter=junit --reporter-options="mochaFile=results/test-output-M-[hash].xml,toConsole=true" --config="videoUploadOnPasses=false,viewportHeight=812,viewportWidth=375,screenshotsFolder=cypress/artifacts/mobile/screenshots,videosFolder=cypress/artifacts/mobile/videos" + .\node_modules\.bin\cypress run --reporter=junit --reporter-options="mochaFile=results/test-output-M-[hash].xml,toConsole=true" --config="videoUploadOnPasses=false,viewportHeight=812,viewportWidth=375,screenshotsFolder=cypress/artifacts/mobile/screenshots,videosFolder=cypress/artifacts/mobile/videos" - task: PublishTestResults@2 inputs: testResultsFormat: 'JUnit' From ab9f7bb15172bb015c97dda183f4c3daec4b193e Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Mon, 22 Mar 2021 19:26:59 +0100 Subject: [PATCH 124/188] try using npx --- build/azure-pipelines.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/build/azure-pipelines.yml b/build/azure-pipelines.yml index f13256b0b2..74026e2f56 100644 --- a/build/azure-pipelines.yml +++ b/build/azure-pipelines.yml @@ -173,7 +173,7 @@ stages: errorActionPreference: 'continue' workingDirectory: 'src\Umbraco.Tests.AcceptanceTest' script: | - .\node_modules\.bin\cypress run --reporter=junit --reporter-options="mochaFile=results/test-output-D-[hash].xml,toConsole=true" --config="videoUploadOnPasses=false,viewportHeight=1600,viewportWidth=2560,screenshotsFolder=cypress/artifacts/desktop/screenshots,videosFolder=cypress/artifacts/desktop/videos" + npx cypress run --reporter=junit --reporter-options="mochaFile=results/test-output-D-[hash].xml,toConsole=true" --config="videoUploadOnPasses=false,viewportHeight=1600,viewportWidth=2560,screenshotsFolder=cypress/artifacts/desktop/screenshots,videosFolder=cypress/artifacts/desktop/videos" - task: PowerShell@2 displayName: Run Cypress (Tablet portrait) inputs: @@ -181,7 +181,7 @@ stages: errorActionPreference: 'continue' workingDirectory: 'src\Umbraco.Tests.AcceptanceTest' script: | - .\node_modules\.bin\cypress run --reporter=junit --reporter-options="mochaFile=results/test-output-T-[hash].xml,toConsole=true" --config="videoUploadOnPasses=false,viewportHeight=1366,viewportWidth=1024,screenshotsFolder=cypress/artifacts/tablet/screenshots,videosFolder=cypress/artifacts/tablet/videos" + npx cypress run --reporter=junit --reporter-options="mochaFile=results/test-output-T-[hash].xml,toConsole=true" --config="videoUploadOnPasses=false,viewportHeight=1366,viewportWidth=1024,screenshotsFolder=cypress/artifacts/tablet/screenshots,videosFolder=cypress/artifacts/tablet/videos" - task: PowerShell@2 displayName: Run Cypress (Mobile protrait) inputs: @@ -189,7 +189,7 @@ stages: errorActionPreference: 'continue' workingDirectory: 'src\Umbraco.Tests.AcceptanceTest' script: | - .\node_modules\.bin\cypress run --reporter=junit --reporter-options="mochaFile=results/test-output-M-[hash].xml,toConsole=true" --config="videoUploadOnPasses=false,viewportHeight=812,viewportWidth=375,screenshotsFolder=cypress/artifacts/mobile/screenshots,videosFolder=cypress/artifacts/mobile/videos" + npx cypress run --reporter=junit --reporter-options="mochaFile=results/test-output-M-[hash].xml,toConsole=true" --config="videoUploadOnPasses=false,viewportHeight=812,viewportWidth=375,screenshotsFolder=cypress/artifacts/mobile/screenshots,videosFolder=cypress/artifacts/mobile/videos" - task: PublishTestResults@2 inputs: testResultsFormat: 'JUnit' From 5ef26997a0eaa3590d6726f8baf4b3ebe9adba16 Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Mon, 22 Mar 2021 19:44:59 +0100 Subject: [PATCH 125/188] use npm run again , but with custom params --- build/azure-pipelines.yml | 56 +++++++++++++++++++++------------------ 1 file changed, 30 insertions(+), 26 deletions(-) diff --git a/build/azure-pipelines.yml b/build/azure-pipelines.yml index 74026e2f56..24c5d29c4f 100644 --- a/build/azure-pipelines.yml +++ b/build/azure-pipelines.yml @@ -165,44 +165,48 @@ stages: - task: Npm@1 displayName: npm install (AcceptanceTest) inputs: - workingDir: - - task: PowerShell@2 + workingDir: 'src\Umbraco.Tests.AcceptanceTest' + - task: Npm@1 displayName: Run Cypress (Desktop) inputs: - targetType: 'inline' - errorActionPreference: 'continue' - workingDirectory: 'src\Umbraco.Tests.AcceptanceTest' - script: | - npx cypress run --reporter=junit --reporter-options="mochaFile=results/test-output-D-[hash].xml,toConsole=true" --config="videoUploadOnPasses=false,viewportHeight=1600,viewportWidth=2560,screenshotsFolder=cypress/artifacts/desktop/screenshots,videosFolder=cypress/artifacts/desktop/videos" - - task: PowerShell@2 + workingDir: src\Umbraco.Tests.AcceptanceTest + command: 'custom' + customCommand: 'run test -- --reporter=junit --reporter-options="mochaFile=results/test-output-D-[hash].xml,toConsole=true" --config="videoUploadOnPasses=false,viewportHeight=1600,viewportWidth=2560,screenshotsFolder=cypress/artifacts/desktop/screenshots,videosFolder=cypress/artifacts/desktop/videos"' + - task: PublishTestResults@2 + displayName: Publish Test results (Desktop) + inputs: + testResultsFormat: 'JUnit' + testResultsFiles: 'results/test-output-D-*.xml' + mergeTestResults: true + testRunTitle: "Test results Desktop" + - task: Npm@1 displayName: Run Cypress (Tablet portrait) inputs: - targetType: 'inline' - errorActionPreference: 'continue' - workingDirectory: 'src\Umbraco.Tests.AcceptanceTest' - script: | - npx cypress run --reporter=junit --reporter-options="mochaFile=results/test-output-T-[hash].xml,toConsole=true" --config="videoUploadOnPasses=false,viewportHeight=1366,viewportWidth=1024,screenshotsFolder=cypress/artifacts/tablet/screenshots,videosFolder=cypress/artifacts/tablet/videos" - - task: PowerShell@2 + workingDir: src\Umbraco.Tests.AcceptanceTest + command: 'custom' + customCommand: 'run test -- --reporter=junit --reporter-options="mochaFile=results/test-output-T-[hash].xml,toConsole=true" --config="videoUploadOnPasses=false,viewportHeight=1366,viewportWidth=1024,screenshotsFolder=cypress/artifacts/tablet/screenshots,videosFolder=cypress/artifacts/tablet/videos"' + - task: PublishTestResults@2 + displayName: Publish Test results (Tablet portrait) + inputs: + testResultsFormat: 'JUnit' + testResultsFiles: 'results/test-output-T-*.xml' + mergeTestResults: true + testRunTitle: "Test results Tablet portrait" + - task: Npm@1 displayName: Run Cypress (Mobile protrait) inputs: - targetType: 'inline' - errorActionPreference: 'continue' - workingDirectory: 'src\Umbraco.Tests.AcceptanceTest' - script: | - npx cypress run --reporter=junit --reporter-options="mochaFile=results/test-output-M-[hash].xml,toConsole=true" --config="videoUploadOnPasses=false,viewportHeight=812,viewportWidth=375,screenshotsFolder=cypress/artifacts/mobile/screenshots,videosFolder=cypress/artifacts/mobile/videos" + workingDir: src\Umbraco.Tests.AcceptanceTest + command: 'custom' + customCommand: 'run test -- --reporter-options="mochaFile=results/test-output-M-[hash].xml,toConsole=true" --config="videoUploadOnPasses=false,viewportHeight=812,viewportWidth=375,screenshotsFolder=cypress/artifacts/mobile/screenshots,videosFolder=cypress/artifacts/mobile/videos"' + - task: PublishTestResults@2 + displayName: Publish Test results (Mobile portrait) inputs: testResultsFormat: 'JUnit' testResultsFiles: 'results/test-output-M-*.xml' mergeTestResults: true testRunTitle: "Test results Mobile" - # - task: Npm@1 - # displayName: npm run test (AcceptanceTest) - # inputs: - # workingDir: src\Umbraco.Tests.AcceptanceTest - # command: 'custom' - # customCommand: 'run test' - + - stage: Artifacts dependsOn: [] jobs: From f5bd29a8a353c9101b23dac6a3654f76f8e5c883 Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Mon, 22 Mar 2021 20:09:02 +0100 Subject: [PATCH 126/188] continue on error --- build/azure-pipelines.yml | 36 ++++++++++++------------------------ 1 file changed, 12 insertions(+), 24 deletions(-) diff --git a/build/azure-pipelines.yml b/build/azure-pipelines.yml index 24c5d29c4f..0dd1723969 100644 --- a/build/azure-pipelines.yml +++ b/build/azure-pipelines.yml @@ -168,44 +168,32 @@ stages: workingDir: 'src\Umbraco.Tests.AcceptanceTest' - task: Npm@1 displayName: Run Cypress (Desktop) + continueOnError: true inputs: workingDir: src\Umbraco.Tests.AcceptanceTest command: 'custom' - customCommand: 'run test -- --reporter=junit --reporter-options="mochaFile=results/test-output-D-[hash].xml,toConsole=true" --config="videoUploadOnPasses=false,viewportHeight=1600,viewportWidth=2560,screenshotsFolder=cypress/artifacts/desktop/screenshots,videosFolder=cypress/artifacts/desktop/videos"' - - task: PublishTestResults@2 - displayName: Publish Test results (Desktop) - inputs: - testResultsFormat: 'JUnit' - testResultsFiles: 'results/test-output-D-*.xml' - mergeTestResults: true - testRunTitle: "Test results Desktop" + customCommand: 'run test -- --config="videoUploadOnPasses=false,viewportHeight=1600,viewportWidth=2560,screenshotsFolder=cypress/artifacts/desktop/screenshots,videosFolder=cypress/artifacts/desktop/videos"' + - task: Npm@1 displayName: Run Cypress (Tablet portrait) + continueOnError: true inputs: workingDir: src\Umbraco.Tests.AcceptanceTest command: 'custom' - customCommand: 'run test -- --reporter=junit --reporter-options="mochaFile=results/test-output-T-[hash].xml,toConsole=true" --config="videoUploadOnPasses=false,viewportHeight=1366,viewportWidth=1024,screenshotsFolder=cypress/artifacts/tablet/screenshots,videosFolder=cypress/artifacts/tablet/videos"' - - task: PublishTestResults@2 - displayName: Publish Test results (Tablet portrait) - inputs: - testResultsFormat: 'JUnit' - testResultsFiles: 'results/test-output-T-*.xml' - mergeTestResults: true - testRunTitle: "Test results Tablet portrait" + customCommand: 'run test -- --config="videoUploadOnPasses=false,viewportHeight=1366,viewportWidth=1024,screenshotsFolder=cypress/artifacts/tablet/screenshots,videosFolder=cypress/artifacts/tablet/videos"' + - task: Npm@1 displayName: Run Cypress (Mobile protrait) + continueOnError: true inputs: workingDir: src\Umbraco.Tests.AcceptanceTest command: 'custom' - customCommand: 'run test -- --reporter-options="mochaFile=results/test-output-M-[hash].xml,toConsole=true" --config="videoUploadOnPasses=false,viewportHeight=812,viewportWidth=375,screenshotsFolder=cypress/artifacts/mobile/screenshots,videosFolder=cypress/artifacts/mobile/videos"' - - - task: PublishTestResults@2 - displayName: Publish Test results (Mobile portrait) + customCommand: 'run test -- --config="videoUploadOnPasses=false,viewportHeight=812,viewportWidth=375,screenshotsFolder=cypress/artifacts/mobile/screenshots,videosFolder=cypress/artifacts/mobile/videos"' + - task: PublishPipelineArtifact@1 + displayName: "Publish test artifacts" inputs: - testResultsFormat: 'JUnit' - testResultsFiles: 'results/test-output-M-*.xml' - mergeTestResults: true - testRunTitle: "Test results Mobile" + targetPath: '$(Build.SourcesDirectory)/cypress/artifacts' + artifact: 'Test artifacts' - stage: Artifacts dependsOn: [] From 124dbd528be81e05511d7142b881faa38b53db0e Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Mon, 22 Mar 2021 21:03:44 +0100 Subject: [PATCH 127/188] fix path --- build/azure-pipelines.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build/azure-pipelines.yml b/build/azure-pipelines.yml index 0dd1723969..28567ef236 100644 --- a/build/azure-pipelines.yml +++ b/build/azure-pipelines.yml @@ -192,7 +192,7 @@ stages: - task: PublishPipelineArtifact@1 displayName: "Publish test artifacts" inputs: - targetPath: '$(Build.SourcesDirectory)/cypress/artifacts' + targetPath: '$(Build.SourcesDirectory)/src/Umbraco.Tests.AcceptanceTest/cypress/artifacts' artifact: 'Test artifacts' - stage: Artifacts From b84e3e3bbc85430e72b8d8a6624d6d52328fe135 Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Tue, 23 Mar 2021 06:47:51 +0100 Subject: [PATCH 128/188] use condition istead of continueonerror --- build/azure-pipelines.yml | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/build/azure-pipelines.yml b/build/azure-pipelines.yml index 28567ef236..a22bdbccda 100644 --- a/build/azure-pipelines.yml +++ b/build/azure-pipelines.yml @@ -163,34 +163,36 @@ stages: inlineScript: > @{ username = $env:Umbraco__CMS__Unattended__UnattendedUserEmail; password = $env:Umbraco__CMS__Unattended__UnattendedUserPassword } | ConvertTo-Json | Set-Content -Path "src\Umbraco.Tests.AcceptanceTest\cypress.env.json" - task: Npm@1 + name: PrepareTask displayName: npm install (AcceptanceTest) inputs: workingDir: 'src\Umbraco.Tests.AcceptanceTest' - task: Npm@1 displayName: Run Cypress (Desktop) - continueOnError: true + condition: eq(PrepareTask.result,'Succeeded') inputs: workingDir: src\Umbraco.Tests.AcceptanceTest command: 'custom' - customCommand: 'run test -- --config="videoUploadOnPasses=false,viewportHeight=1600,viewportWidth=2560,screenshotsFolder=cypress/artifacts/desktop/screenshots,videosFolder=cypress/artifacts/desktop/videos"' + customCommand: 'run test -- --config="viewportHeight=1600,viewportWidth=2560,screenshotsFolder=cypress/artifacts/desktop/screenshots,videosFolder=cypress/artifacts/desktop/videos,videoUploadOnPasses=false"' - task: Npm@1 displayName: Run Cypress (Tablet portrait) - continueOnError: true + condition: eq(PrepareTask.result,'Succeeded') inputs: workingDir: src\Umbraco.Tests.AcceptanceTest command: 'custom' - customCommand: 'run test -- --config="videoUploadOnPasses=false,viewportHeight=1366,viewportWidth=1024,screenshotsFolder=cypress/artifacts/tablet/screenshots,videosFolder=cypress/artifacts/tablet/videos"' + customCommand: 'run test -- --config="viewportHeight=1366,viewportWidth=1024,screenshotsFolder=cypress/artifacts/tablet/screenshots,videosFolder=cypress/artifacts/tablet/videos,videoUploadOnPasses=false"' - task: Npm@1 displayName: Run Cypress (Mobile protrait) - continueOnError: true + condition: eq(PrepareTask.result,'Succeeded') inputs: workingDir: src\Umbraco.Tests.AcceptanceTest command: 'custom' - customCommand: 'run test -- --config="videoUploadOnPasses=false,viewportHeight=812,viewportWidth=375,screenshotsFolder=cypress/artifacts/mobile/screenshots,videosFolder=cypress/artifacts/mobile/videos"' + customCommand: 'run test -- --config="viewportHeight=812,viewportWidth=375,screenshotsFolder=cypress/artifacts/mobile/screenshots,videosFolder=cypress/artifacts/mobile/videos,videoUploadOnPasses=false"' - task: PublishPipelineArtifact@1 displayName: "Publish test artifacts" + condition: eq(PrepareTask.result,'Succeeded') inputs: targetPath: '$(Build.SourcesDirectory)/src/Umbraco.Tests.AcceptanceTest/cypress/artifacts' artifact: 'Test artifacts' From df43f9139693d3f1b8abd5745f6f585cd3cdb788 Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Tue, 23 Mar 2021 06:50:57 +0100 Subject: [PATCH 129/188] Update azure-pipelines.yml for Azure Pipelines --- build/azure-pipelines.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/build/azure-pipelines.yml b/build/azure-pipelines.yml index a22bdbccda..8284d631cc 100644 --- a/build/azure-pipelines.yml +++ b/build/azure-pipelines.yml @@ -169,7 +169,7 @@ stages: workingDir: 'src\Umbraco.Tests.AcceptanceTest' - task: Npm@1 displayName: Run Cypress (Desktop) - condition: eq(PrepareTask.result,'Succeeded') + condition: always() inputs: workingDir: src\Umbraco.Tests.AcceptanceTest command: 'custom' @@ -177,7 +177,7 @@ stages: - task: Npm@1 displayName: Run Cypress (Tablet portrait) - condition: eq(PrepareTask.result,'Succeeded') + condition: always() inputs: workingDir: src\Umbraco.Tests.AcceptanceTest command: 'custom' @@ -185,14 +185,14 @@ stages: - task: Npm@1 displayName: Run Cypress (Mobile protrait) - condition: eq(PrepareTask.result,'Succeeded') + condition: always() inputs: workingDir: src\Umbraco.Tests.AcceptanceTest command: 'custom' customCommand: 'run test -- --config="viewportHeight=812,viewportWidth=375,screenshotsFolder=cypress/artifacts/mobile/screenshots,videosFolder=cypress/artifacts/mobile/videos,videoUploadOnPasses=false"' - task: PublishPipelineArtifact@1 displayName: "Publish test artifacts" - condition: eq(PrepareTask.result,'Succeeded') + condition: always() inputs: targetPath: '$(Build.SourcesDirectory)/src/Umbraco.Tests.AcceptanceTest/cypress/artifacts' artifact: 'Test artifacts' From fa37ebc1078685a0e5bf70017575b5ad9a95550b Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Tue, 23 Mar 2021 08:39:03 +0100 Subject: [PATCH 130/188] Changed default value of FlagOutOfDateModels to true (Like in v8) --- src/Umbraco.Core/Configuration/Models/ModelsBuilderSettings.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Core/Configuration/Models/ModelsBuilderSettings.cs b/src/Umbraco.Core/Configuration/Models/ModelsBuilderSettings.cs index 33d5bf534d..2d9f3121de 100644 --- a/src/Umbraco.Core/Configuration/Models/ModelsBuilderSettings.cs +++ b/src/Umbraco.Core/Configuration/Models/ModelsBuilderSettings.cs @@ -10,7 +10,7 @@ namespace Umbraco.Cms.Core.Configuration.Models ///
public class ModelsBuilderSettings { - private bool _flagOutOfDateModels; + private bool _flagOutOfDateModels = true; private static string DefaultModelsDirectory => "~/umbraco/models"; From d8555d501c1cc6889fc77d9e3d6f4a868be79507 Mon Sep 17 00:00:00 2001 From: Mole Date: Tue, 23 Mar 2021 08:51:48 +0100 Subject: [PATCH 131/188] Add retries, fix two failing tests, and uncomment tablet & mobile tests --- build/azure-pipelines.yml | 34 +++++++++---------- src/Umbraco.Tests.AcceptanceTest/cypress.json | 6 +++- .../cypress/integration/Settings/languages.ts | 2 +- .../cypress/integration/Settings/templates.ts | 3 ++ 4 files changed, 26 insertions(+), 19 deletions(-) diff --git a/build/azure-pipelines.yml b/build/azure-pipelines.yml index 8284d631cc..25967e1c23 100644 --- a/build/azure-pipelines.yml +++ b/build/azure-pipelines.yml @@ -174,29 +174,29 @@ stages: workingDir: src\Umbraco.Tests.AcceptanceTest command: 'custom' customCommand: 'run test -- --config="viewportHeight=1600,viewportWidth=2560,screenshotsFolder=cypress/artifacts/desktop/screenshots,videosFolder=cypress/artifacts/desktop/videos,videoUploadOnPasses=false"' - - - task: Npm@1 - displayName: Run Cypress (Tablet portrait) - condition: always() - inputs: - workingDir: src\Umbraco.Tests.AcceptanceTest - command: 'custom' - customCommand: 'run test -- --config="viewportHeight=1366,viewportWidth=1024,screenshotsFolder=cypress/artifacts/tablet/screenshots,videosFolder=cypress/artifacts/tablet/videos,videoUploadOnPasses=false"' - - - task: Npm@1 - displayName: Run Cypress (Mobile protrait) - condition: always() - inputs: - workingDir: src\Umbraco.Tests.AcceptanceTest - command: 'custom' - customCommand: 'run test -- --config="viewportHeight=812,viewportWidth=375,screenshotsFolder=cypress/artifacts/mobile/screenshots,videosFolder=cypress/artifacts/mobile/videos,videoUploadOnPasses=false"' + +# - task: Npm@1 +# displayName: Run Cypress (Tablet portrait) +# condition: always() +# inputs: +# workingDir: src\Umbraco.Tests.AcceptanceTest +# command: 'custom' +# customCommand: 'run test -- --config="viewportHeight=1366,viewportWidth=1024,screenshotsFolder=cypress/artifacts/tablet/screenshots,videosFolder=cypress/artifacts/tablet/videos,videoUploadOnPasses=false"' +# +# - task: Npm@1 +# displayName: Run Cypress (Mobile protrait) +# condition: always() +# inputs: +# workingDir: src\Umbraco.Tests.AcceptanceTest +# command: 'custom' +# customCommand: 'run test -- --config="viewportHeight=812,viewportWidth=375,screenshotsFolder=cypress/artifacts/mobile/screenshots,videosFolder=cypress/artifacts/mobile/videos,videoUploadOnPasses=false"' - task: PublishPipelineArtifact@1 displayName: "Publish test artifacts" condition: always() inputs: targetPath: '$(Build.SourcesDirectory)/src/Umbraco.Tests.AcceptanceTest/cypress/artifacts' artifact: 'Test artifacts' - + - stage: Artifacts dependsOn: [] jobs: diff --git a/src/Umbraco.Tests.AcceptanceTest/cypress.json b/src/Umbraco.Tests.AcceptanceTest/cypress.json index 33978211ed..d563625cb8 100644 --- a/src/Umbraco.Tests.AcceptanceTest/cypress.json +++ b/src/Umbraco.Tests.AcceptanceTest/cypress.json @@ -7,5 +7,9 @@ "password": "" }, "supportFile": "cypress/support/index.ts", - "videoUploadOnPasses" : false + "videoUploadOnPasses" : false, + "retries": { + "runMode": 2, + "openMode": 1 + } } diff --git a/src/Umbraco.Tests.AcceptanceTest/cypress/integration/Settings/languages.ts b/src/Umbraco.Tests.AcceptanceTest/cypress/integration/Settings/languages.ts index 541c6d213d..b3e7f2f2e2 100644 --- a/src/Umbraco.Tests.AcceptanceTest/cypress/integration/Settings/languages.ts +++ b/src/Umbraco.Tests.AcceptanceTest/cypress/integration/Settings/languages.ts @@ -6,7 +6,7 @@ context('Languages', () => { }); it('Add language', () => { - const name = "Afrikaans"; // Must be an option in the select box + const name = "afrikaans"; // Must be an option in the select box cy.umbracoEnsureLanguageNameNotExists(name); diff --git a/src/Umbraco.Tests.AcceptanceTest/cypress/integration/Settings/templates.ts b/src/Umbraco.Tests.AcceptanceTest/cypress/integration/Settings/templates.ts index 3122c3ebf7..8e0eed9154 100644 --- a/src/Umbraco.Tests.AcceptanceTest/cypress/integration/Settings/templates.ts +++ b/src/Umbraco.Tests.AcceptanceTest/cypress/integration/Settings/templates.ts @@ -23,6 +23,9 @@ context('Templates', () => { cy.umbracoEnsureTemplateNameNotExists(name); createTemplate(); + // We have to wait for the ace editor to load, because when the editor is loading it will "steal" the focus briefly, + // which causes the save event to fire if we've added something to the header field, causing errors. + cy.wait(500); //Type name cy.umbracoEditorHeaderName(name); // Save From ab9d31e01cdaca7b8910b4d19b0b038e4f2b9621 Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Tue, 23 Mar 2021 08:58:18 +0100 Subject: [PATCH 132/188] https://github.com/umbraco/Umbraco-CMS/issues/10030 - Fixed the two issues reported. --- src/Umbraco.Web.BackOffice/Controllers/ContentController.cs | 3 +-- src/Umbraco.Web.BackOffice/Controllers/MediaController.cs | 5 ++--- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/src/Umbraco.Web.BackOffice/Controllers/ContentController.cs b/src/Umbraco.Web.BackOffice/Controllers/ContentController.cs index b314cb4fa1..c3690b4863 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/ContentController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/ContentController.cs @@ -36,7 +36,6 @@ using Umbraco.Cms.Web.Common.ActionsResults; using Umbraco.Cms.Web.Common.Attributes; using Umbraco.Cms.Web.Common.Authorization; using Umbraco.Extensions; -using Constants = Umbraco.Cms.Core.Constants; namespace Umbraco.Cms.Web.BackOffice.Controllers { @@ -158,7 +157,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers // Authorize... var resource = new ContentPermissionsResource(content, ActionRights.ActionLetter); - var authorizationResult = await _authorizationService.AuthorizeAsync(User, content, AuthorizationPolicies.ContentPermissionByResource); + var authorizationResult = await _authorizationService.AuthorizeAsync(User, resource, AuthorizationPolicies.ContentPermissionByResource); if (!authorizationResult.Succeeded) { return Forbid(); diff --git a/src/Umbraco.Web.BackOffice/Controllers/MediaController.cs b/src/Umbraco.Web.BackOffice/Controllers/MediaController.cs index 3eb8d740bc..3bb23e47e9 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/MediaController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/MediaController.cs @@ -24,7 +24,6 @@ using Umbraco.Cms.Core.Media; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.ContentEditing; using Umbraco.Cms.Core.Models.Entities; -using Umbraco.Cms.Core.Models.Membership; using Umbraco.Cms.Core.Models.Validation; using Umbraco.Cms.Core.Persistence.Querying; using Umbraco.Cms.Core.PropertyEditors; @@ -42,7 +41,6 @@ using Umbraco.Cms.Web.Common.ActionsResults; using Umbraco.Cms.Web.Common.Attributes; using Umbraco.Cms.Web.Common.Authorization; using Umbraco.Extensions; -using Constants = Umbraco.Cms.Core.Constants; namespace Umbraco.Cms.Web.BackOffice.Controllers { @@ -647,7 +645,8 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers // Authorize... var requirement = new MediaPermissionsResourceRequirement(); - var authorizationResult = await _authorizationService.AuthorizeAsync(User, _mediaService.GetById(sorted.ParentId), requirement); + var resource = new MediaPermissionsResource(sorted.ParentId); + var authorizationResult = await _authorizationService.AuthorizeAsync(User, resource, requirement); if (!authorizationResult.Succeeded) { return Forbid(); From f3aa3812c49821302d964199b4a507eb71b46352 Mon Sep 17 00:00:00 2001 From: Mole Date: Tue, 23 Mar 2021 09:30:30 +0100 Subject: [PATCH 133/188] Remove videos if no test failed --- .../cypress/plugins/index.js | 9 +++++++++ src/Umbraco.Tests.AcceptanceTest/package.json | 3 ++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/src/Umbraco.Tests.AcceptanceTest/cypress/plugins/index.js b/src/Umbraco.Tests.AcceptanceTest/cypress/plugins/index.js index 59283feec5..51b79a1fef 100644 --- a/src/Umbraco.Tests.AcceptanceTest/cypress/plugins/index.js +++ b/src/Umbraco.Tests.AcceptanceTest/cypress/plugins/index.js @@ -11,6 +11,7 @@ // This function is called when a project is opened or re-opened (e.g. due to // the project's config changing) +const del = require('del') /** * @type {Cypress.PluginConfig} @@ -24,5 +25,13 @@ module.exports = (on, config) => { config.baseUrl = baseUrl; } + on('after:spec', (spec, results) => { + if(results.stats.failures === 0 && results.video) { + // `del()` returns a promise, so it's important to return it to ensure + // deleting the video is finished before moving + return del(results.video) + } + }) + return config; } diff --git a/src/Umbraco.Tests.AcceptanceTest/package.json b/src/Umbraco.Tests.AcceptanceTest/package.json index 378fe719fc..b443390d1e 100644 --- a/src/Umbraco.Tests.AcceptanceTest/package.json +++ b/src/Umbraco.Tests.AcceptanceTest/package.json @@ -7,7 +7,8 @@ }, "devDependencies": { "cross-env": "^7.0.2", - "cypress": "^6.0.1", + "cypress": "^6.7.0", + "del": "^6.0.0", "ncp": "^2.0.0", "prompt": "^1.0.0", "umbraco-cypress-testhelpers": "^1.0.0-beta-52" From 25de77a8bee1c1cd2f3d78ece586205d02aad033 Mon Sep 17 00:00:00 2001 From: Mole Date: Tue, 23 Mar 2021 09:55:14 +0100 Subject: [PATCH 134/188] Up retries to 5 and set language name back to Afrikaans --- src/Umbraco.Tests.AcceptanceTest/cypress.json | 2 +- .../cypress/integration/Settings/languages.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Tests.AcceptanceTest/cypress.json b/src/Umbraco.Tests.AcceptanceTest/cypress.json index d563625cb8..340eede2a0 100644 --- a/src/Umbraco.Tests.AcceptanceTest/cypress.json +++ b/src/Umbraco.Tests.AcceptanceTest/cypress.json @@ -9,7 +9,7 @@ "supportFile": "cypress/support/index.ts", "videoUploadOnPasses" : false, "retries": { - "runMode": 2, + "runMode": 5, "openMode": 1 } } diff --git a/src/Umbraco.Tests.AcceptanceTest/cypress/integration/Settings/languages.ts b/src/Umbraco.Tests.AcceptanceTest/cypress/integration/Settings/languages.ts index b3e7f2f2e2..541c6d213d 100644 --- a/src/Umbraco.Tests.AcceptanceTest/cypress/integration/Settings/languages.ts +++ b/src/Umbraco.Tests.AcceptanceTest/cypress/integration/Settings/languages.ts @@ -6,7 +6,7 @@ context('Languages', () => { }); it('Add language', () => { - const name = "afrikaans"; // Must be an option in the select box + const name = "Afrikaans"; // Must be an option in the select box cy.umbracoEnsureLanguageNameNotExists(name); From 347ab9ffec4ff027807190caedbf9cb01c9e79b8 Mon Sep 17 00:00:00 2001 From: Mole Date: Tue, 23 Mar 2021 11:39:13 +0100 Subject: [PATCH 135/188] Only publish artifacts on failed --- build/azure-pipelines.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build/azure-pipelines.yml b/build/azure-pipelines.yml index 25967e1c23..995ce3bd1c 100644 --- a/build/azure-pipelines.yml +++ b/build/azure-pipelines.yml @@ -192,7 +192,7 @@ stages: # customCommand: 'run test -- --config="viewportHeight=812,viewportWidth=375,screenshotsFolder=cypress/artifacts/mobile/screenshots,videosFolder=cypress/artifacts/mobile/videos,videoUploadOnPasses=false"' - task: PublishPipelineArtifact@1 displayName: "Publish test artifacts" - condition: always() + condition: failed() inputs: targetPath: '$(Build.SourcesDirectory)/src/Umbraco.Tests.AcceptanceTest/cypress/artifacts' artifact: 'Test artifacts' From 0f3bed13d5b04924ee570cfb2c875438394ea03a Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Tue, 23 Mar 2021 11:54:02 +0100 Subject: [PATCH 136/188] Potential optimization - Build as part of run (in background process) --- build/azure-pipelines.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/build/azure-pipelines.yml b/build/azure-pipelines.yml index 995ce3bd1c..13a9de8f70 100644 --- a/build/azure-pipelines.yml +++ b/build/azure-pipelines.yml @@ -132,11 +132,11 @@ stages: displayName: Start MSSQL LocalDb - powershell: Invoke-Sqlcmd -Query "CREATE DATABASE $env:UmbracoDatabaseName" -ServerInstance $env:UmbracoDatabaseServer displayName: Create database - - task: DotNetCoreCLI@2 - displayName: dotnet build (Netcore) - inputs: - command: build - projects: '**/Umbraco.Web.UI.Netcore.csproj' +# - task: DotNetCoreCLI@2 +# displayName: dotnet build (Netcore) +# inputs: +# command: build +# projects: '**/Umbraco.Web.UI.Netcore.csproj' - task: NodeTool@0 displayName: Use Node 11.x inputs: @@ -152,7 +152,7 @@ stages: gulpFile: src\Umbraco.Web.UI.Client\gulpfile.js targets: build workingDirectory: src\Umbraco.Web.UI.Client - - powershell: Start-Process -FilePath "dotnet" -ArgumentList "run", "--no-build", "-p", "src\Umbraco.Web.UI.Netcore\Umbraco.Web.UI.Netcore.csproj" + - powershell: Start-Process -FilePath "dotnet" -ArgumentList "run", "-p", "src\Umbraco.Web.UI.Netcore\Umbraco.Web.UI.Netcore.csproj" displayName: dotnet run (Netcore) # - powershell: dotnet run --no-build -p .\src\Umbraco.Web.UI.NetCore\Umbraco.Web.UI.NetCore.csproj # displayName: dotnet run (Netcore) From 04df87984f82ede0abbb5d49d03d369d2923e6bd Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Tue, 23 Mar 2021 11:59:57 +0100 Subject: [PATCH 137/188] Change test reporter to junit --- build/azure-pipelines.yml | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/build/azure-pipelines.yml b/build/azure-pipelines.yml index 13a9de8f70..ed02988980 100644 --- a/build/azure-pipelines.yml +++ b/build/azure-pipelines.yml @@ -173,8 +173,14 @@ stages: inputs: workingDir: src\Umbraco.Tests.AcceptanceTest command: 'custom' - customCommand: 'run test -- --config="viewportHeight=1600,viewportWidth=2560,screenshotsFolder=cypress/artifacts/desktop/screenshots,videosFolder=cypress/artifacts/desktop/videos,videoUploadOnPasses=false"' + customCommand: 'run test -- --reporter junit --reporter-options "mochaFile=results/test-output-D-[hash].xml,toConsole=true" --config="viewportHeight=1600,viewportWidth=2560,screenshotsFolder=cypress/artifacts/desktop/screenshots,videosFolder=cypress/artifacts/desktop/videos,videoUploadOnPasses=false"' + - task: PublishTestResults@2 + inputs: + testResultsFormat: 'JUnit' + testResultsFiles: 'src/Umbraco.Tests.AcceptanceTest/results/test-output-M-*.xml' + mergeTestResults: true + testRunTitle: "Test results Desktop" # - task: Npm@1 # displayName: Run Cypress (Tablet portrait) # condition: always() From cad4e9a0fe3db6af39ba49b2c17c6056de026441 Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Tue, 23 Mar 2021 12:29:34 +0100 Subject: [PATCH 138/188] Fix wrong test result output file --- build/azure-pipelines.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build/azure-pipelines.yml b/build/azure-pipelines.yml index ed02988980..4b3d2d0cce 100644 --- a/build/azure-pipelines.yml +++ b/build/azure-pipelines.yml @@ -178,7 +178,7 @@ stages: - task: PublishTestResults@2 inputs: testResultsFormat: 'JUnit' - testResultsFiles: 'src/Umbraco.Tests.AcceptanceTest/results/test-output-M-*.xml' + testResultsFiles: 'src/Umbraco.Tests.AcceptanceTest/results/test-output-D-*.xml' mergeTestResults: true testRunTitle: "Test results Desktop" # - task: Npm@1 From da539b27c927afb3648cf818a452a482762abf8a Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Tue, 23 Mar 2021 12:48:37 +0100 Subject: [PATCH 139/188] Updated targets to allow clear --- build/NuSpecs/build/Umbraco.Cms.StaticAssets.targets | 10 ++++++++++ .../UmbracoPackage/build/UmbracoPackage.targets | 8 ++++---- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/build/NuSpecs/build/Umbraco.Cms.StaticAssets.targets b/build/NuSpecs/build/Umbraco.Cms.StaticAssets.targets index 9f40bdf125..47c8e6bb94 100644 --- a/build/NuSpecs/build/Umbraco.Cms.StaticAssets.targets +++ b/build/NuSpecs/build/Umbraco.Cms.StaticAssets.targets @@ -17,4 +17,14 @@ + + + + + + + + + + diff --git a/build/templates/UmbracoPackage/build/UmbracoPackage.targets b/build/templates/UmbracoPackage/build/UmbracoPackage.targets index bf6e19dfee..7a0dc0338a 100644 --- a/build/templates/UmbracoPackage/build/UmbracoPackage.targets +++ b/build/templates/UmbracoPackage/build/UmbracoPackage.targets @@ -4,11 +4,11 @@ $(MSBuildThisFileDirectory)..\App_Plugins\UmbracoPackage\**\*.* - + - + - + - + From 14c2bb4aa2a22ddc4d8a35e8eb61b7592c222fce Mon Sep 17 00:00:00 2001 From: Mole Date: Tue, 23 Mar 2021 13:03:01 +0100 Subject: [PATCH 140/188] Update cypress and fix tests --- .../cypress/integration/Settings/templates.ts | 8 ++++++-- .../cypress/integration/Tour/backofficeTour.ts | 2 +- src/Umbraco.Tests.AcceptanceTest/package.json | 2 +- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/Umbraco.Tests.AcceptanceTest/cypress/integration/Settings/templates.ts b/src/Umbraco.Tests.AcceptanceTest/cypress/integration/Settings/templates.ts index c586384af7..65d03e5a78 100644 --- a/src/Umbraco.Tests.AcceptanceTest/cypress/integration/Settings/templates.ts +++ b/src/Umbraco.Tests.AcceptanceTest/cypress/integration/Settings/templates.ts @@ -23,14 +23,18 @@ context('Templates', () => { cy.umbracoEnsureTemplateNameNotExists(name); createTemplate(); + // We have to wait for the ace editor to load, because when the editor is loading it will "steal" the focus briefly, + // which causes the save event to fire if we've added something to the header field, causing errors. + cy.wait(500); + //Type name cy.umbracoEditorHeaderName(name); // Save // We must drop focus for the auto save event to occur. cy.get('.btn-success').focus(); // And then wait for the auto save event to finish by finding the page in the tree view. - // This is a bit of a roundabout way to find items in a treev view since we dont use umbracoTreeItem - // but we must be able to wait for the save evnent to finish, and we can't do that with umbracoTreeItem + // This is a bit of a roundabout way to find items in a tree view since we dont use umbracoTreeItem + // but we must be able to wait for the save event to finish, and we can't do that with umbracoTreeItem cy.get('[data-element="tree-item-templates"] > :nth-child(2) > .umb-animated > .umb-tree-item__inner > .umb-tree-item__label') .contains(name).should('be.visible', { timeout: 10000 }); // Now that the auto save event has finished we can save diff --git a/src/Umbraco.Tests.AcceptanceTest/cypress/integration/Tour/backofficeTour.ts b/src/Umbraco.Tests.AcceptanceTest/cypress/integration/Tour/backofficeTour.ts index 9bc1fff488..d3950d7d19 100644 --- a/src/Umbraco.Tests.AcceptanceTest/cypress/integration/Tour/backofficeTour.ts +++ b/src/Umbraco.Tests.AcceptanceTest/cypress/integration/Tour/backofficeTour.ts @@ -49,7 +49,7 @@ function resetTourData() { { "alias": "umbIntroIntroduction", "completed": false, - "disabled": false + "disabled": true }; cy.getCookie('UMB-XSRF-TOKEN', { log: false }).then((token) => { diff --git a/src/Umbraco.Tests.AcceptanceTest/package.json b/src/Umbraco.Tests.AcceptanceTest/package.json index 378fe719fc..069a1d85b7 100644 --- a/src/Umbraco.Tests.AcceptanceTest/package.json +++ b/src/Umbraco.Tests.AcceptanceTest/package.json @@ -7,7 +7,7 @@ }, "devDependencies": { "cross-env": "^7.0.2", - "cypress": "^6.0.1", + "cypress": "^6.8.0", "ncp": "^2.0.0", "prompt": "^1.0.0", "umbraco-cypress-testhelpers": "^1.0.0-beta-52" From c6eefeb2d7b785fc2dcb09e725cd077dfb88118c Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Tue, 23 Mar 2021 14:04:55 +0100 Subject: [PATCH 141/188] Always post test result --- build/azure-pipelines.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/build/azure-pipelines.yml b/build/azure-pipelines.yml index 4b3d2d0cce..43a6f09d46 100644 --- a/build/azure-pipelines.yml +++ b/build/azure-pipelines.yml @@ -176,6 +176,7 @@ stages: customCommand: 'run test -- --reporter junit --reporter-options "mochaFile=results/test-output-D-[hash].xml,toConsole=true" --config="viewportHeight=1600,viewportWidth=2560,screenshotsFolder=cypress/artifacts/desktop/screenshots,videosFolder=cypress/artifacts/desktop/videos,videoUploadOnPasses=false"' - task: PublishTestResults@2 + condition: always() inputs: testResultsFormat: 'JUnit' testResultsFiles: 'src/Umbraco.Tests.AcceptanceTest/results/test-output-D-*.xml' From 4a8d033d248793f12c8f81c19dc98a2db4ac2093 Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Tue, 23 Mar 2021 18:29:07 +0100 Subject: [PATCH 142/188] Update version in both templates --- build/azure-pipelines.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/build/azure-pipelines.yml b/build/azure-pipelines.yml index a41d0f5b7f..9ba7e5efa1 100644 --- a/build/azure-pipelines.yml +++ b/build/azure-pipelines.yml @@ -144,17 +144,17 @@ stages: $ubuild.SetUmbracoVersion($continuous) - #Update the version in template also - - $templatePath = - 'build/templates/UmbracoSolution/.template.config/template.json' + #Update the version in templates also + $templatePath ='build/templates/UmbracoSolution/.template.config/template.json' $a = Get-Content $templatePath -raw | ConvertFrom-Json - $a.symbols.version.defaultValue = $continuous - $a | ConvertTo-Json -depth 32| set-content $templatePath + $templatePath = 'build/templates/UmbracoPackage/.template.config/template.json' + $a = Get-Content $templatePath -raw | ConvertFrom-Json + $a.symbols.version.defaultValue = $continuous + $a | ConvertTo-Json -depth 32| set-content $templatePath Write-Host "Building: $continuous" - task: PowerShell@1 From 5be59da0a89d50365003ea6b6eb0af816956f6ae Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Tue, 23 Mar 2021 18:40:20 +0100 Subject: [PATCH 143/188] Dont reuse variable. --- build/azure-pipelines.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/build/azure-pipelines.yml b/build/azure-pipelines.yml index 9ba7e5efa1..e668e8acdc 100644 --- a/build/azure-pipelines.yml +++ b/build/azure-pipelines.yml @@ -151,10 +151,10 @@ stages: $a.symbols.version.defaultValue = $continuous $a | ConvertTo-Json -depth 32| set-content $templatePath - $templatePath = 'build/templates/UmbracoPackage/.template.config/template.json' - $a = Get-Content $templatePath -raw | ConvertFrom-Json - $a.symbols.version.defaultValue = $continuous - $a | ConvertTo-Json -depth 32| set-content $templatePath + $template2Path = 'build/templates/UmbracoPackage/.template.config/template.json' + $a2 = Get-Content $template2Path -raw | ConvertFrom-Json + $a2.symbols.version.defaultValue = $continuous + $a2 | ConvertTo-Json -depth 32| set-content $template2Path Write-Host "Building: $continuous" - task: PowerShell@1 From 876f4be7e67c8d291fd4c5c6654a265f0d1b4ddf Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Tue, 23 Mar 2021 19:13:04 +0100 Subject: [PATCH 144/188] Dont reuse variable. --- build/azure-pipelines.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/build/azure-pipelines.yml b/build/azure-pipelines.yml index e668e8acdc..36dada748c 100644 --- a/build/azure-pipelines.yml +++ b/build/azure-pipelines.yml @@ -152,9 +152,9 @@ stages: $a | ConvertTo-Json -depth 32| set-content $templatePath $template2Path = 'build/templates/UmbracoPackage/.template.config/template.json' - $a2 = Get-Content $template2Path -raw | ConvertFrom-Json - $a2.symbols.version.defaultValue = $continuous - $a2 | ConvertTo-Json -depth 32| set-content $template2Path + $b = Get-Content $template2Path -raw | ConvertFrom-Json + $b.symbols.version.defaultValue = $continuous + $b | ConvertTo-Json -depth 32| set-content $template2Path Write-Host "Building: $continuous" - task: PowerShell@1 From 782e7dfc90cd8fda212d7428249ea2bf9748296c Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Tue, 23 Mar 2021 19:16:44 +0100 Subject: [PATCH 145/188] Better descriptions --- build/templates/UmbracoPackage/.template.config/ide.host.json | 2 +- build/templates/UmbracoSolution/.template.config/ide.host.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/build/templates/UmbracoPackage/.template.config/ide.host.json b/build/templates/UmbracoPackage/.template.config/ide.host.json index cf222f8839..8d3bae3e3c 100644 --- a/build/templates/UmbracoPackage/.template.config/ide.host.json +++ b/build/templates/UmbracoPackage/.template.config/ide.host.json @@ -4,7 +4,7 @@ "icon": "icon.png", "description": { "id": "UmbracoPackage", - "text": "An empty Umbraco Package/Plugin ready to get started." + "text": "Umbraco Package - An empty Umbraco CMS package (Plugin)" }, "symbolInfo": [ diff --git a/build/templates/UmbracoSolution/.template.config/ide.host.json b/build/templates/UmbracoSolution/.template.config/ide.host.json index ee9e86f6d4..7604d3b1f4 100644 --- a/build/templates/UmbracoSolution/.template.config/ide.host.json +++ b/build/templates/UmbracoSolution/.template.config/ide.host.json @@ -4,7 +4,7 @@ "icon": "icon.png", "description": { "id": "UmbracoSolution", - "text": "An empty Umbraco Solution ready to get started" + "text": "Umbraco Web Application - An empty Umbraco CMS web application" }, "symbolInfo": [ { From d3d2571477a811a516211aecd2223adf30f6dace Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Tue, 23 Mar 2021 19:28:15 +0100 Subject: [PATCH 146/188] indentation --- build/azure-pipelines.yml | 58 +++++++++++++++++++++++---------------- 1 file changed, 34 insertions(+), 24 deletions(-) diff --git a/build/azure-pipelines.yml b/build/azure-pipelines.yml index 36dada748c..60c7a70e47 100644 --- a/build/azure-pipelines.yml +++ b/build/azure-pipelines.yml @@ -124,39 +124,49 @@ stages: inputs: scriptType: inlineScript inlineScript: > - Write-Host "Working folder: $pwd" + Write-Host "Working folder: $pwd" - $ubuild = build/build.ps1 -get -continue + $ubuild = build/build.ps1 -get -continue - $version = $ubuild.GetUmbracoVersion() + $version = $ubuild.GetUmbracoVersion() - if ($version.Comment -ne "") - { - # 8.0.0-beta.33.1234 - $continuous = "$($version.Semver).$(Build.BuildNumber)" - } - else - { - # 8.0.0-alpha.1234 - $continuous = "$($version.Release)-alpha.$(Build.BuildNumber)" - } - $ubuild.SetUmbracoVersion($continuous) + if ($version.Comment -ne "") + { + # 8.0.0-beta.33.1234 + $continuous = "$($version.Semver).$(Build.BuildNumber)" + } + else + { + # 8.0.0-alpha.1234 + $continuous = "$($version.Release)-alpha.$(Build.BuildNumber)" + } + $ubuild.SetUmbracoVersion($continuous) - #Update the version in templates also + #Update the version in templates also - $templatePath ='build/templates/UmbracoSolution/.template.config/template.json' - $a = Get-Content $templatePath -raw | ConvertFrom-Json - $a.symbols.version.defaultValue = $continuous - $a | ConvertTo-Json -depth 32| set-content $templatePath + $templatePath = + 'build/templates/UmbracoSolution/.template.config/template.json' - $template2Path = 'build/templates/UmbracoPackage/.template.config/template.json' - $b = Get-Content $template2Path -raw | ConvertFrom-Json - $b.symbols.version.defaultValue = $continuous - $b | ConvertTo-Json -depth 32| set-content $template2Path + $a = Get-Content $templatePath -raw | ConvertFrom-Json - Write-Host "Building: $continuous" + $a.symbols.version.defaultValue = $continuous + + $a | ConvertTo-Json -depth 32| set-content $templatePath + + + $templatePath = + 'build/templates/UmbracoPackage/.template.config/template.json' + + $a = Get-Content $templatePath -raw | ConvertFrom-Json + + $a.symbols.version.defaultValue = $continuous + + $a | ConvertTo-Json -depth 32| set-content $templatePath + + + Write-Host "Building: $continuous" - task: PowerShell@1 displayName: Prepare Build inputs: From fdc1635399539a081f3a5f6a28b8f1f0e5dc92e5 Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Tue, 23 Mar 2021 19:53:28 +0100 Subject: [PATCH 147/188] More explicit about what folders to clear --- build/NuSpecs/build/Umbraco.Cms.StaticAssets.targets | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/build/NuSpecs/build/Umbraco.Cms.StaticAssets.targets b/build/NuSpecs/build/Umbraco.Cms.StaticAssets.targets index 47c8e6bb94..37b65e320c 100644 --- a/build/NuSpecs/build/Umbraco.Cms.StaticAssets.targets +++ b/build/NuSpecs/build/Umbraco.Cms.StaticAssets.targets @@ -19,11 +19,19 @@ - + + + + + - + + + + + From e49f8a0edf4a01fa46db8fb83c64a765d466df33 Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Tue, 23 Mar 2021 20:52:45 +0100 Subject: [PATCH 148/188] Add Linux acceptance tests - Trial 1 --- build/azure-pipelines.yml | 104 +++++++++++++++++++++++++++++++++++--- 1 file changed, 98 insertions(+), 6 deletions(-) diff --git a/build/azure-pipelines.yml b/build/azure-pipelines.yml index 43a6f09d46..aa6ecb269d 100644 --- a/build/azure-pipelines.yml +++ b/build/azure-pipelines.yml @@ -111,14 +111,15 @@ stages: value: cypress@umbraco.com - name: Umbraco__CMS__Unattended__UnattendedUserPassword value: UmbracoAcceptance123! - - name: UmbracoDatabaseServer - value: (LocalDB)\MSSQLLocalDB - - name: UmbracoDatabaseName - value: Cypress - - name: ConnectionStrings__umbracoDbDSN - value: Server=$(UmbracoDatabaseServer);Database=$(UmbracoDatabaseName);Integrated Security=true; jobs: - job: Windows_Acceptance_tests + variables: + - name: UmbracoDatabaseServer + value: (LocalDB)\MSSQLLocalDB + - name: UmbracoDatabaseName + value: Cypress + - name: ConnectionStrings__umbracoDbDSN + value: Server=$(UmbracoDatabaseServer);Database=$(UmbracoDatabaseName);Integrated Security=true; displayName: Windows pool: vmImage: windows-latest @@ -203,7 +204,98 @@ stages: inputs: targetPath: '$(Build.SourcesDirectory)/src/Umbraco.Tests.AcceptanceTest/cypress/artifacts' artifact: 'Test artifacts' + - job: Linux_Acceptance_tests + displayName: Linux + variables: + - name: UmbracoDatabaseServer + value: localhost + - name: UmbracoDatabaseName + value: Cypress + - name: ConnectionStrings__umbracoDbDSN + value: Server=localhost,1433;User Id=sa;Password=$(SA_PASSWORD); + services: + mssql: mssql + pool: + vmImage: ubuntu-latest + steps: + - task: UseDotNet@2 + displayName: Use .Net Core sdk 5.x + inputs: + version: 5.x + - powershell: Invoke-Sqlcmd -Query "CREATE DATABASE $env:UmbracoDatabaseName" -ServerInstance $env:UmbracoDatabaseServer + displayName: Create database + # - task: DotNetCoreCLI@2 + # displayName: dotnet build (Netcore) + # inputs: + # command: build + # projects: '**/Umbraco.Web.UI.Netcore.csproj' + - task: NodeTool@0 + displayName: Use Node 11.x + inputs: + versionSpec: 11.x + - task: Npm@1 + displayName: npm install (Client) + inputs: + workingDir: src\Umbraco.Web.UI.Client + verbose: false + - task: gulp@0 + displayName: gulp build + inputs: + gulpFile: src\Umbraco.Web.UI.Client\gulpfile.js + targets: build + workingDirectory: src\Umbraco.Web.UI.Client + - powershell: Start-Process -FilePath "dotnet" -ArgumentList "run", "-p", "src\Umbraco.Web.UI.Netcore\Umbraco.Web.UI.Netcore.csproj" + displayName: dotnet run (Netcore) + # - powershell: dotnet run --no-build -p .\src\Umbraco.Web.UI.NetCore\Umbraco.Web.UI.NetCore.csproj + # displayName: dotnet run (Netcore) + - task: PowerShell@1 + displayName: Generate Cypress.env.json + inputs: + scriptType: inlineScript + inlineScript: > + @{ username = $env:Umbraco__CMS__Unattended__UnattendedUserEmail; password = $env:Umbraco__CMS__Unattended__UnattendedUserPassword } | ConvertTo-Json | Set-Content -Path "src\Umbraco.Tests.AcceptanceTest\cypress.env.json" + - task: Npm@1 + name: PrepareTask + displayName: npm install (AcceptanceTest) + inputs: + workingDir: 'src\Umbraco.Tests.AcceptanceTest' + - task: Npm@1 + displayName: Run Cypress (Desktop) + condition: always() + inputs: + workingDir: src\Umbraco.Tests.AcceptanceTest + command: 'custom' + customCommand: 'run test -- --reporter junit --reporter-options "mochaFile=results/test-output-D-[hash].xml,toConsole=true" --config="viewportHeight=1600,viewportWidth=2560,screenshotsFolder=cypress/artifacts/desktop/screenshots,videosFolder=cypress/artifacts/desktop/videos,videoUploadOnPasses=false"' + + - task: PublishTestResults@2 + condition: always() + inputs: + testResultsFormat: 'JUnit' + testResultsFiles: 'src/Umbraco.Tests.AcceptanceTest/results/test-output-D-*.xml' + mergeTestResults: true + testRunTitle: "Test results Desktop" + # - task: Npm@1 + # displayName: Run Cypress (Tablet portrait) + # condition: always() + # inputs: + # workingDir: src\Umbraco.Tests.AcceptanceTest + # command: 'custom' + # customCommand: 'run test -- --config="viewportHeight=1366,viewportWidth=1024,screenshotsFolder=cypress/artifacts/tablet/screenshots,videosFolder=cypress/artifacts/tablet/videos,videoUploadOnPasses=false"' + # + # - task: Npm@1 + # displayName: Run Cypress (Mobile protrait) + # condition: always() + # inputs: + # workingDir: src\Umbraco.Tests.AcceptanceTest + # command: 'custom' + # customCommand: 'run test -- --config="viewportHeight=812,viewportWidth=375,screenshotsFolder=cypress/artifacts/mobile/screenshots,videosFolder=cypress/artifacts/mobile/videos,videoUploadOnPasses=false"' + - task: PublishPipelineArtifact@1 + displayName: "Publish test artifacts" + condition: failed() + inputs: + targetPath: '$(Build.SourcesDirectory)/src/Umbraco.Tests.AcceptanceTest/cypress/artifacts' + artifact: 'Test artifacts' - stage: Artifacts dependsOn: [] jobs: From d534493c70c6f413dc8948612e08a5de5041c888 Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Tue, 23 Mar 2021 20:58:50 +0100 Subject: [PATCH 149/188] Add Linux acceptance tests - Trial 2 --- build/azure-pipelines.yml | 180 +++++++++++++++++++------------------- 1 file changed, 90 insertions(+), 90 deletions(-) diff --git a/build/azure-pipelines.yml b/build/azure-pipelines.yml index aa6ecb269d..a20d290f95 100644 --- a/build/azure-pipelines.yml +++ b/build/azure-pipelines.yml @@ -204,98 +204,98 @@ stages: inputs: targetPath: '$(Build.SourcesDirectory)/src/Umbraco.Tests.AcceptanceTest/cypress/artifacts' artifact: 'Test artifacts' - - job: Linux_Acceptance_tests - displayName: Linux - variables: - - name: UmbracoDatabaseServer - value: localhost - - name: UmbracoDatabaseName - value: Cypress - - name: ConnectionStrings__umbracoDbDSN - value: Server=localhost,1433;User Id=sa;Password=$(SA_PASSWORD); - services: - mssql: mssql - pool: - vmImage: ubuntu-latest - steps: - - task: UseDotNet@2 - displayName: Use .Net Core sdk 5.x - inputs: - version: 5.x + - job: Linux_Acceptance_tests + displayName: Linux + variables: + - name: UmbracoDatabaseServer + value: localhost + - name: UmbracoDatabaseName + value: Cypress + - name: ConnectionStrings__umbracoDbDSN + value: Server=localhost,1433;User Id=sa;Password=$(SA_PASSWORD); + services: + mssql: mssql + pool: + vmImage: ubuntu-latest + steps: + - task: UseDotNet@2 + displayName: Use .Net Core sdk 5.x + inputs: + version: 5.x - - powershell: Invoke-Sqlcmd -Query "CREATE DATABASE $env:UmbracoDatabaseName" -ServerInstance $env:UmbracoDatabaseServer - displayName: Create database - # - task: DotNetCoreCLI@2 - # displayName: dotnet build (Netcore) - # inputs: - # command: build - # projects: '**/Umbraco.Web.UI.Netcore.csproj' - - task: NodeTool@0 - displayName: Use Node 11.x - inputs: - versionSpec: 11.x - - task: Npm@1 - displayName: npm install (Client) - inputs: - workingDir: src\Umbraco.Web.UI.Client - verbose: false - - task: gulp@0 - displayName: gulp build - inputs: - gulpFile: src\Umbraco.Web.UI.Client\gulpfile.js - targets: build - workingDirectory: src\Umbraco.Web.UI.Client - - powershell: Start-Process -FilePath "dotnet" -ArgumentList "run", "-p", "src\Umbraco.Web.UI.Netcore\Umbraco.Web.UI.Netcore.csproj" - displayName: dotnet run (Netcore) - # - powershell: dotnet run --no-build -p .\src\Umbraco.Web.UI.NetCore\Umbraco.Web.UI.NetCore.csproj - # displayName: dotnet run (Netcore) - - task: PowerShell@1 - displayName: Generate Cypress.env.json - inputs: - scriptType: inlineScript - inlineScript: > - @{ username = $env:Umbraco__CMS__Unattended__UnattendedUserEmail; password = $env:Umbraco__CMS__Unattended__UnattendedUserPassword } | ConvertTo-Json | Set-Content -Path "src\Umbraco.Tests.AcceptanceTest\cypress.env.json" - - task: Npm@1 - name: PrepareTask - displayName: npm install (AcceptanceTest) - inputs: - workingDir: 'src\Umbraco.Tests.AcceptanceTest' - - task: Npm@1 - displayName: Run Cypress (Desktop) - condition: always() - inputs: - workingDir: src\Umbraco.Tests.AcceptanceTest - command: 'custom' - customCommand: 'run test -- --reporter junit --reporter-options "mochaFile=results/test-output-D-[hash].xml,toConsole=true" --config="viewportHeight=1600,viewportWidth=2560,screenshotsFolder=cypress/artifacts/desktop/screenshots,videosFolder=cypress/artifacts/desktop/videos,videoUploadOnPasses=false"' + - powershell: Invoke-Sqlcmd -Query "CREATE DATABASE $env:UmbracoDatabaseName" -ServerInstance $env:UmbracoDatabaseServer + displayName: Create database + # - task: DotNetCoreCLI@2 + # displayName: dotnet build (Netcore) + # inputs: + # command: build + # projects: '**/Umbraco.Web.UI.Netcore.csproj' + - task: NodeTool@0 + displayName: Use Node 11.x + inputs: + versionSpec: 11.x + - task: Npm@1 + displayName: npm install (Client) + inputs: + workingDir: src\Umbraco.Web.UI.Client + verbose: false + - task: gulp@0 + displayName: gulp build + inputs: + gulpFile: src\Umbraco.Web.UI.Client\gulpfile.js + targets: build + workingDirectory: src\Umbraco.Web.UI.Client + - powershell: Start-Process -FilePath "dotnet" -ArgumentList "run", "-p", "src\Umbraco.Web.UI.Netcore\Umbraco.Web.UI.Netcore.csproj" + displayName: dotnet run (Netcore) + # - powershell: dotnet run --no-build -p .\src\Umbraco.Web.UI.NetCore\Umbraco.Web.UI.NetCore.csproj + # displayName: dotnet run (Netcore) + - task: PowerShell@1 + displayName: Generate Cypress.env.json + inputs: + scriptType: inlineScript + inlineScript: > + @{ username = $env:Umbraco__CMS__Unattended__UnattendedUserEmail; password = $env:Umbraco__CMS__Unattended__UnattendedUserPassword } | ConvertTo-Json | Set-Content -Path "src\Umbraco.Tests.AcceptanceTest\cypress.env.json" + - task: Npm@1 + name: PrepareTask + displayName: npm install (AcceptanceTest) + inputs: + workingDir: 'src\Umbraco.Tests.AcceptanceTest' + - task: Npm@1 + displayName: Run Cypress (Desktop) + condition: always() + inputs: + workingDir: src\Umbraco.Tests.AcceptanceTest + command: 'custom' + customCommand: 'run test -- --reporter junit --reporter-options "mochaFile=results/test-output-D-[hash].xml,toConsole=true" --config="viewportHeight=1600,viewportWidth=2560,screenshotsFolder=cypress/artifacts/desktop/screenshots,videosFolder=cypress/artifacts/desktop/videos,videoUploadOnPasses=false"' - - task: PublishTestResults@2 - condition: always() - inputs: - testResultsFormat: 'JUnit' - testResultsFiles: 'src/Umbraco.Tests.AcceptanceTest/results/test-output-D-*.xml' - mergeTestResults: true - testRunTitle: "Test results Desktop" - # - task: Npm@1 - # displayName: Run Cypress (Tablet portrait) - # condition: always() - # inputs: - # workingDir: src\Umbraco.Tests.AcceptanceTest - # command: 'custom' - # customCommand: 'run test -- --config="viewportHeight=1366,viewportWidth=1024,screenshotsFolder=cypress/artifacts/tablet/screenshots,videosFolder=cypress/artifacts/tablet/videos,videoUploadOnPasses=false"' - # - # - task: Npm@1 - # displayName: Run Cypress (Mobile protrait) - # condition: always() - # inputs: - # workingDir: src\Umbraco.Tests.AcceptanceTest - # command: 'custom' - # customCommand: 'run test -- --config="viewportHeight=812,viewportWidth=375,screenshotsFolder=cypress/artifacts/mobile/screenshots,videosFolder=cypress/artifacts/mobile/videos,videoUploadOnPasses=false"' - - task: PublishPipelineArtifact@1 - displayName: "Publish test artifacts" - condition: failed() - inputs: - targetPath: '$(Build.SourcesDirectory)/src/Umbraco.Tests.AcceptanceTest/cypress/artifacts' - artifact: 'Test artifacts' + - task: PublishTestResults@2 + condition: always() + inputs: + testResultsFormat: 'JUnit' + testResultsFiles: 'src/Umbraco.Tests.AcceptanceTest/results/test-output-D-*.xml' + mergeTestResults: true + testRunTitle: "Test results Desktop" + # - task: Npm@1 + # displayName: Run Cypress (Tablet portrait) + # condition: always() + # inputs: + # workingDir: src\Umbraco.Tests.AcceptanceTest + # command: 'custom' + # customCommand: 'run test -- --config="viewportHeight=1366,viewportWidth=1024,screenshotsFolder=cypress/artifacts/tablet/screenshots,videosFolder=cypress/artifacts/tablet/videos,videoUploadOnPasses=false"' + # + # - task: Npm@1 + # displayName: Run Cypress (Mobile protrait) + # condition: always() + # inputs: + # workingDir: src\Umbraco.Tests.AcceptanceTest + # command: 'custom' + # customCommand: 'run test -- --config="viewportHeight=812,viewportWidth=375,screenshotsFolder=cypress/artifacts/mobile/screenshots,videosFolder=cypress/artifacts/mobile/videos,videoUploadOnPasses=false"' + - task: PublishPipelineArtifact@1 + displayName: "Publish test artifacts" + condition: failed() + inputs: + targetPath: '$(Build.SourcesDirectory)/src/Umbraco.Tests.AcceptanceTest/cypress/artifacts' + artifact: 'Test artifacts' - stage: Artifacts dependsOn: [] jobs: From c5692323500b8081ecd79149611a6fbdb8fc3786 Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Tue, 23 Mar 2021 21:00:50 +0100 Subject: [PATCH 150/188] Add Linux acceptance tests - Trial 3 --- build/azure-pipelines.yml | 162 +++++++++++++++++++------------------- 1 file changed, 81 insertions(+), 81 deletions(-) diff --git a/build/azure-pipelines.yml b/build/azure-pipelines.yml index a20d290f95..c9ebc05d0b 100644 --- a/build/azure-pipelines.yml +++ b/build/azure-pipelines.yml @@ -213,89 +213,89 @@ stages: value: Cypress - name: ConnectionStrings__umbracoDbDSN value: Server=localhost,1433;User Id=sa;Password=$(SA_PASSWORD); - services: - mssql: mssql - pool: - vmImage: ubuntu-latest - steps: - - task: UseDotNet@2 - displayName: Use .Net Core sdk 5.x - inputs: - version: 5.x + services: + mssql: mssql + pool: + vmImage: ubuntu-latest + steps: + - task: UseDotNet@2 + displayName: Use .Net Core sdk 5.x + inputs: + version: 5.x - - powershell: Invoke-Sqlcmd -Query "CREATE DATABASE $env:UmbracoDatabaseName" -ServerInstance $env:UmbracoDatabaseServer - displayName: Create database - # - task: DotNetCoreCLI@2 - # displayName: dotnet build (Netcore) - # inputs: - # command: build - # projects: '**/Umbraco.Web.UI.Netcore.csproj' - - task: NodeTool@0 - displayName: Use Node 11.x - inputs: - versionSpec: 11.x - - task: Npm@1 - displayName: npm install (Client) - inputs: - workingDir: src\Umbraco.Web.UI.Client - verbose: false - - task: gulp@0 - displayName: gulp build - inputs: - gulpFile: src\Umbraco.Web.UI.Client\gulpfile.js - targets: build - workingDirectory: src\Umbraco.Web.UI.Client - - powershell: Start-Process -FilePath "dotnet" -ArgumentList "run", "-p", "src\Umbraco.Web.UI.Netcore\Umbraco.Web.UI.Netcore.csproj" - displayName: dotnet run (Netcore) - # - powershell: dotnet run --no-build -p .\src\Umbraco.Web.UI.NetCore\Umbraco.Web.UI.NetCore.csproj - # displayName: dotnet run (Netcore) - - task: PowerShell@1 - displayName: Generate Cypress.env.json - inputs: - scriptType: inlineScript - inlineScript: > - @{ username = $env:Umbraco__CMS__Unattended__UnattendedUserEmail; password = $env:Umbraco__CMS__Unattended__UnattendedUserPassword } | ConvertTo-Json | Set-Content -Path "src\Umbraco.Tests.AcceptanceTest\cypress.env.json" - - task: Npm@1 - name: PrepareTask - displayName: npm install (AcceptanceTest) - inputs: - workingDir: 'src\Umbraco.Tests.AcceptanceTest' - - task: Npm@1 - displayName: Run Cypress (Desktop) - condition: always() - inputs: - workingDir: src\Umbraco.Tests.AcceptanceTest - command: 'custom' - customCommand: 'run test -- --reporter junit --reporter-options "mochaFile=results/test-output-D-[hash].xml,toConsole=true" --config="viewportHeight=1600,viewportWidth=2560,screenshotsFolder=cypress/artifacts/desktop/screenshots,videosFolder=cypress/artifacts/desktop/videos,videoUploadOnPasses=false"' + - powershell: Invoke-Sqlcmd -Query "CREATE DATABASE $env:UmbracoDatabaseName" -ServerInstance $env:UmbracoDatabaseServer + displayName: Create database + # - task: DotNetCoreCLI@2 + # displayName: dotnet build (Netcore) + # inputs: + # command: build + # projects: '**/Umbraco.Web.UI.Netcore.csproj' + - task: NodeTool@0 + displayName: Use Node 11.x + inputs: + versionSpec: 11.x + - task: Npm@1 + displayName: npm install (Client) + inputs: + workingDir: src\Umbraco.Web.UI.Client + verbose: false + - task: gulp@0 + displayName: gulp build + inputs: + gulpFile: src\Umbraco.Web.UI.Client\gulpfile.js + targets: build + workingDirectory: src\Umbraco.Web.UI.Client + - powershell: Start-Process -FilePath "dotnet" -ArgumentList "run", "-p", "src\Umbraco.Web.UI.Netcore\Umbraco.Web.UI.Netcore.csproj" + displayName: dotnet run (Netcore) + # - powershell: dotnet run --no-build -p .\src\Umbraco.Web.UI.NetCore\Umbraco.Web.UI.NetCore.csproj + # displayName: dotnet run (Netcore) + - task: PowerShell@1 + displayName: Generate Cypress.env.json + inputs: + scriptType: inlineScript + inlineScript: > + @{ username = $env:Umbraco__CMS__Unattended__UnattendedUserEmail; password = $env:Umbraco__CMS__Unattended__UnattendedUserPassword } | ConvertTo-Json | Set-Content -Path "src\Umbraco.Tests.AcceptanceTest\cypress.env.json" + - task: Npm@1 + name: PrepareTask + displayName: npm install (AcceptanceTest) + inputs: + workingDir: 'src\Umbraco.Tests.AcceptanceTest' + - task: Npm@1 + displayName: Run Cypress (Desktop) + condition: always() + inputs: + workingDir: src\Umbraco.Tests.AcceptanceTest + command: 'custom' + customCommand: 'run test -- --reporter junit --reporter-options "mochaFile=results/test-output-D-[hash].xml,toConsole=true" --config="viewportHeight=1600,viewportWidth=2560,screenshotsFolder=cypress/artifacts/desktop/screenshots,videosFolder=cypress/artifacts/desktop/videos,videoUploadOnPasses=false"' - - task: PublishTestResults@2 - condition: always() - inputs: - testResultsFormat: 'JUnit' - testResultsFiles: 'src/Umbraco.Tests.AcceptanceTest/results/test-output-D-*.xml' - mergeTestResults: true - testRunTitle: "Test results Desktop" - # - task: Npm@1 - # displayName: Run Cypress (Tablet portrait) - # condition: always() - # inputs: - # workingDir: src\Umbraco.Tests.AcceptanceTest - # command: 'custom' - # customCommand: 'run test -- --config="viewportHeight=1366,viewportWidth=1024,screenshotsFolder=cypress/artifacts/tablet/screenshots,videosFolder=cypress/artifacts/tablet/videos,videoUploadOnPasses=false"' - # - # - task: Npm@1 - # displayName: Run Cypress (Mobile protrait) - # condition: always() - # inputs: - # workingDir: src\Umbraco.Tests.AcceptanceTest - # command: 'custom' - # customCommand: 'run test -- --config="viewportHeight=812,viewportWidth=375,screenshotsFolder=cypress/artifacts/mobile/screenshots,videosFolder=cypress/artifacts/mobile/videos,videoUploadOnPasses=false"' - - task: PublishPipelineArtifact@1 - displayName: "Publish test artifacts" - condition: failed() - inputs: - targetPath: '$(Build.SourcesDirectory)/src/Umbraco.Tests.AcceptanceTest/cypress/artifacts' - artifact: 'Test artifacts' + - task: PublishTestResults@2 + condition: always() + inputs: + testResultsFormat: 'JUnit' + testResultsFiles: 'src/Umbraco.Tests.AcceptanceTest/results/test-output-D-*.xml' + mergeTestResults: true + testRunTitle: "Test results Desktop" + # - task: Npm@1 + # displayName: Run Cypress (Tablet portrait) + # condition: always() + # inputs: + # workingDir: src\Umbraco.Tests.AcceptanceTest + # command: 'custom' + # customCommand: 'run test -- --config="viewportHeight=1366,viewportWidth=1024,screenshotsFolder=cypress/artifacts/tablet/screenshots,videosFolder=cypress/artifacts/tablet/videos,videoUploadOnPasses=false"' + # + # - task: Npm@1 + # displayName: Run Cypress (Mobile protrait) + # condition: always() + # inputs: + # workingDir: src\Umbraco.Tests.AcceptanceTest + # command: 'custom' + # customCommand: 'run test -- --config="viewportHeight=812,viewportWidth=375,screenshotsFolder=cypress/artifacts/mobile/screenshots,videosFolder=cypress/artifacts/mobile/videos,videoUploadOnPasses=false"' + - task: PublishPipelineArtifact@1 + displayName: "Publish test artifacts" + condition: failed() + inputs: + targetPath: '$(Build.SourcesDirectory)/src/Umbraco.Tests.AcceptanceTest/cypress/artifacts' + artifact: 'Test artifacts' - stage: Artifacts dependsOn: [] jobs: From 3d88203d7bee759dacdfb701232173494dc064d9 Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Tue, 23 Mar 2021 21:07:44 +0100 Subject: [PATCH 151/188] Add Linux acceptance tests - Trial 4 --- build/azure-pipelines.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/build/azure-pipelines.yml b/build/azure-pipelines.yml index c9ebc05d0b..e8b8ac0398 100644 --- a/build/azure-pipelines.yml +++ b/build/azure-pipelines.yml @@ -223,6 +223,8 @@ stages: inputs: version: 5.x + - powershell: Install-Module -Name SqlServer + displayName: Install PS Module for MSSql - powershell: Invoke-Sqlcmd -Query "CREATE DATABASE $env:UmbracoDatabaseName" -ServerInstance $env:UmbracoDatabaseServer displayName: Create database # - task: DotNetCoreCLI@2 From fd4c12a448b3a237faa1e31abe12f196060398e8 Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Tue, 23 Mar 2021 21:13:10 +0100 Subject: [PATCH 152/188] Add Linux acceptance tests - Trial 5 --- build/azure-pipelines.yml | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/build/azure-pipelines.yml b/build/azure-pipelines.yml index e8b8ac0398..4d21b39968 100644 --- a/build/azure-pipelines.yml +++ b/build/azure-pipelines.yml @@ -222,10 +222,7 @@ stages: displayName: Use .Net Core sdk 5.x inputs: version: 5.x - - - powershell: Install-Module -Name SqlServer - displayName: Install PS Module for MSSql - - powershell: Invoke-Sqlcmd -Query "CREATE DATABASE $env:UmbracoDatabaseName" -ServerInstance $env:UmbracoDatabaseServer + - powershell: sqlcmd -S $env:UmbracoDatabaseServer -Q "CREATE DATABASE $env:UmbracoDatabaseName" displayName: Create database # - task: DotNetCoreCLI@2 # displayName: dotnet build (Netcore) From 74dff277c3dd27750032f378062fd9ec54303ac2 Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Tue, 23 Mar 2021 21:17:35 +0100 Subject: [PATCH 153/188] Add Linux acceptance tests - Trial 6 --- build/azure-pipelines.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build/azure-pipelines.yml b/build/azure-pipelines.yml index 4d21b39968..6c91e8957f 100644 --- a/build/azure-pipelines.yml +++ b/build/azure-pipelines.yml @@ -222,7 +222,7 @@ stages: displayName: Use .Net Core sdk 5.x inputs: version: 5.x - - powershell: sqlcmd -S $env:UmbracoDatabaseServer -Q "CREATE DATABASE $env:UmbracoDatabaseName" + - powershell: sqlcmd -S $env:UmbracoDatabaseServer -U sa -P $env:SA_PASSWORD -Q "CREATE DATABASE $env:UmbracoDatabaseName" displayName: Create database # - task: DotNetCoreCLI@2 # displayName: dotnet build (Netcore) From d399d2c46c6a4ab888f4085672fc13f6806862d2 Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Tue, 23 Mar 2021 21:22:43 +0100 Subject: [PATCH 154/188] Add Linux acceptance tests - Trial 7 --- build/azure-pipelines.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build/azure-pipelines.yml b/build/azure-pipelines.yml index 6c91e8957f..02ee26c6dc 100644 --- a/build/azure-pipelines.yml +++ b/build/azure-pipelines.yml @@ -222,7 +222,7 @@ stages: displayName: Use .Net Core sdk 5.x inputs: version: 5.x - - powershell: sqlcmd -S $env:UmbracoDatabaseServer -U sa -P $env:SA_PASSWORD -Q "CREATE DATABASE $env:UmbracoDatabaseName" + - powershell: sqlcmd -S="$env:UmbracoDatabaseServer" -U="sa" -P="$env:SA_PASSWORD" -Q="CREATE DATABASE $env:UmbracoDatabaseName" displayName: Create database # - task: DotNetCoreCLI@2 # displayName: dotnet build (Netcore) From 9ce54fa45b17a51878a38f1478e432225e22d8d3 Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Tue, 23 Mar 2021 21:36:58 +0100 Subject: [PATCH 155/188] Add Linux acceptance tests - Trial 8 --- build/azure-pipelines.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build/azure-pipelines.yml b/build/azure-pipelines.yml index 02ee26c6dc..ec58c75724 100644 --- a/build/azure-pipelines.yml +++ b/build/azure-pipelines.yml @@ -212,7 +212,7 @@ stages: - name: UmbracoDatabaseName value: Cypress - name: ConnectionStrings__umbracoDbDSN - value: Server=localhost,1433;User Id=sa;Password=$(SA_PASSWORD); + value: Server=localhost,1433;Database=$(UmbracoDatabaseName);User Id=sa;Password=$(SA_PASSWORD); services: mssql: mssql pool: @@ -222,7 +222,7 @@ stages: displayName: Use .Net Core sdk 5.x inputs: version: 5.x - - powershell: sqlcmd -S="$env:UmbracoDatabaseServer" -U="sa" -P="$env:SA_PASSWORD" -Q="CREATE DATABASE $env:UmbracoDatabaseName" + - powershell: sqlcmd -S="localhost,1433" -U="sa" -P="UmbracoIntegration123!" -Q="CREATE DATABASE $env:UmbracoDatabaseName" displayName: Create database # - task: DotNetCoreCLI@2 # displayName: dotnet build (Netcore) From bb03d244582a2caaf1ad0861741b5e05b96d9845 Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Tue, 23 Mar 2021 21:42:10 +0100 Subject: [PATCH 156/188] Add Linux acceptance tests - Trial 9 --- build/azure-pipelines.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build/azure-pipelines.yml b/build/azure-pipelines.yml index ec58c75724..f013fed3c4 100644 --- a/build/azure-pipelines.yml +++ b/build/azure-pipelines.yml @@ -222,7 +222,7 @@ stages: displayName: Use .Net Core sdk 5.x inputs: version: 5.x - - powershell: sqlcmd -S="localhost,1433" -U="sa" -P="UmbracoIntegration123!" -Q="CREATE DATABASE $env:UmbracoDatabaseName" + - powershell: sqlcmd -S="127.0.0.1,1433" -U="sa" -P="UmbracoIntegration123!" -Q="CREATE DATABASE $env:UmbracoDatabaseName" displayName: Create database # - task: DotNetCoreCLI@2 # displayName: dotnet build (Netcore) From 02cb8084742fa18d1179dd80d2a460741335847b Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Tue, 23 Mar 2021 21:45:20 +0100 Subject: [PATCH 157/188] Add Linux acceptance tests - Trial 10 --- build/azure-pipelines.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build/azure-pipelines.yml b/build/azure-pipelines.yml index f013fed3c4..1950766152 100644 --- a/build/azure-pipelines.yml +++ b/build/azure-pipelines.yml @@ -222,7 +222,7 @@ stages: displayName: Use .Net Core sdk 5.x inputs: version: 5.x - - powershell: sqlcmd -S="127.0.0.1,1433" -U="sa" -P="UmbracoIntegration123!" -Q="CREATE DATABASE $env:UmbracoDatabaseName" + - powershell: sqlcmd -S="." -U="sa" -P="UmbracoIntegration123!" -Q="CREATE DATABASE $env:UmbracoDatabaseName" displayName: Create database # - task: DotNetCoreCLI@2 # displayName: dotnet build (Netcore) From 5a77a097af8707500869604fdf2a24838278c6be Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Tue, 23 Mar 2021 21:46:29 +0100 Subject: [PATCH 158/188] Add Linux acceptance tests - Trial 11 --- build/azure-pipelines.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build/azure-pipelines.yml b/build/azure-pipelines.yml index 1950766152..94d0418c6d 100644 --- a/build/azure-pipelines.yml +++ b/build/azure-pipelines.yml @@ -222,7 +222,7 @@ stages: displayName: Use .Net Core sdk 5.x inputs: version: 5.x - - powershell: sqlcmd -S="." -U="sa" -P="UmbracoIntegration123!" -Q="CREATE DATABASE $env:UmbracoDatabaseName" + - powershell: sqlcmd -S . -U sa -P UmbracoIntegration123! -Q="CREATE DATABASE $env:UmbracoDatabaseName" displayName: Create database # - task: DotNetCoreCLI@2 # displayName: dotnet build (Netcore) From 19ae71ea3fd5285bbdba487d879d0acd022e196c Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Tue, 23 Mar 2021 21:50:54 +0100 Subject: [PATCH 159/188] Add Linux acceptance tests - Trial 12 --- build/azure-pipelines.yml | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/build/azure-pipelines.yml b/build/azure-pipelines.yml index 94d0418c6d..971edab89b 100644 --- a/build/azure-pipelines.yml +++ b/build/azure-pipelines.yml @@ -222,7 +222,7 @@ stages: displayName: Use .Net Core sdk 5.x inputs: version: 5.x - - powershell: sqlcmd -S . -U sa -P UmbracoIntegration123! -Q="CREATE DATABASE $env:UmbracoDatabaseName" + - powershell: sqlcmd -S . -U sa -P $env:SA_PASSWORD -Q="CREATE DATABASE $env:UmbracoDatabaseName" displayName: Create database # - task: DotNetCoreCLI@2 # displayName: dotnet build (Netcore) @@ -236,34 +236,34 @@ stages: - task: Npm@1 displayName: npm install (Client) inputs: - workingDir: src\Umbraco.Web.UI.Client + workingDir: src/Umbraco.Web.UI.Client verbose: false - task: gulp@0 displayName: gulp build inputs: - gulpFile: src\Umbraco.Web.UI.Client\gulpfile.js + gulpFile: src/Umbraco.Web.UI.Client/gulpfile.js targets: build - workingDirectory: src\Umbraco.Web.UI.Client - - powershell: Start-Process -FilePath "dotnet" -ArgumentList "run", "-p", "src\Umbraco.Web.UI.Netcore\Umbraco.Web.UI.Netcore.csproj" + workingDirectory: src/Umbraco.Web.UI.Client + - powershell: Start-Process -FilePath "dotnet" -ArgumentList "run", "-p", "src/Umbraco.Web.UI.Netcore/Umbraco.Web.UI.Netcore.csproj" displayName: dotnet run (Netcore) - # - powershell: dotnet run --no-build -p .\src\Umbraco.Web.UI.NetCore\Umbraco.Web.UI.NetCore.csproj + # - powershell: dotnet run --no-build -p ./src/Umbraco.Web.UI.NetCore/Umbraco.Web.UI.NetCore.csproj # displayName: dotnet run (Netcore) - task: PowerShell@1 displayName: Generate Cypress.env.json inputs: scriptType: inlineScript inlineScript: > - @{ username = $env:Umbraco__CMS__Unattended__UnattendedUserEmail; password = $env:Umbraco__CMS__Unattended__UnattendedUserPassword } | ConvertTo-Json | Set-Content -Path "src\Umbraco.Tests.AcceptanceTest\cypress.env.json" + @{ username = $env:Umbraco__CMS__Unattended__UnattendedUserEmail; password = $env:Umbraco__CMS__Unattended__UnattendedUserPassword } | ConvertTo-Json | Set-Content -Path "src/Umbraco.Tests.AcceptanceTest/cypress.env.json" - task: Npm@1 name: PrepareTask displayName: npm install (AcceptanceTest) inputs: - workingDir: 'src\Umbraco.Tests.AcceptanceTest' + workingDir: 'src/Umbraco.Tests.AcceptanceTest' - task: Npm@1 displayName: Run Cypress (Desktop) condition: always() inputs: - workingDir: src\Umbraco.Tests.AcceptanceTest + workingDir: src/Umbraco.Tests.AcceptanceTest command: 'custom' customCommand: 'run test -- --reporter junit --reporter-options "mochaFile=results/test-output-D-[hash].xml,toConsole=true" --config="viewportHeight=1600,viewportWidth=2560,screenshotsFolder=cypress/artifacts/desktop/screenshots,videosFolder=cypress/artifacts/desktop/videos,videoUploadOnPasses=false"' @@ -278,7 +278,7 @@ stages: # displayName: Run Cypress (Tablet portrait) # condition: always() # inputs: - # workingDir: src\Umbraco.Tests.AcceptanceTest + # workingDir: src/Umbraco.Tests.AcceptanceTest # command: 'custom' # customCommand: 'run test -- --config="viewportHeight=1366,viewportWidth=1024,screenshotsFolder=cypress/artifacts/tablet/screenshots,videosFolder=cypress/artifacts/tablet/videos,videoUploadOnPasses=false"' # @@ -286,7 +286,7 @@ stages: # displayName: Run Cypress (Mobile protrait) # condition: always() # inputs: - # workingDir: src\Umbraco.Tests.AcceptanceTest + # workingDir: src/Umbraco.Tests.AcceptanceTest # command: 'custom' # customCommand: 'run test -- --config="viewportHeight=812,viewportWidth=375,screenshotsFolder=cypress/artifacts/mobile/screenshots,videosFolder=cypress/artifacts/mobile/videos,videoUploadOnPasses=false"' - task: PublishPipelineArtifact@1 From 17689bd6b35361ff26397c905514ec4247842d04 Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Tue, 23 Mar 2021 21:53:53 +0100 Subject: [PATCH 160/188] Add Linux acceptance tests - Trial 13 --- build/azure-pipelines.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build/azure-pipelines.yml b/build/azure-pipelines.yml index 971edab89b..8daa785d21 100644 --- a/build/azure-pipelines.yml +++ b/build/azure-pipelines.yml @@ -222,7 +222,7 @@ stages: displayName: Use .Net Core sdk 5.x inputs: version: 5.x - - powershell: sqlcmd -S . -U sa -P $env:SA_PASSWORD -Q="CREATE DATABASE $env:UmbracoDatabaseName" + - powershell: sqlcmd -S . -U sa -P $env:SA_PASSWORD -Q "CREATE DATABASE $env:UmbracoDatabaseName" displayName: Create database # - task: DotNetCoreCLI@2 # displayName: dotnet build (Netcore) From 7ef572c55511cb96928bae8a7b513aa6c7c3074c Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Wed, 24 Mar 2021 05:50:49 +0100 Subject: [PATCH 161/188] Add Linux acceptance tests - Trial 14 --- build/azure-pipelines.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/build/azure-pipelines.yml b/build/azure-pipelines.yml index 8daa785d21..8dbc3355db 100644 --- a/build/azure-pipelines.yml +++ b/build/azure-pipelines.yml @@ -253,7 +253,8 @@ stages: inputs: scriptType: inlineScript inlineScript: > - @{ username = $env:Umbraco__CMS__Unattended__UnattendedUserEmail; password = $env:Umbraco__CMS__Unattended__UnattendedUserPassword } | ConvertTo-Json | Set-Content -Path "src/Umbraco.Tests.AcceptanceTest/cypress.env.json" + $jsonObject = "{ ""username"": ""$env:Umbraco__CMS__Unattended__UnattendedUserEmail"", ""password"" : ""$env:Umbraco__CMS__Unattended__UnattendedUserPassword"" }" + $jsonObject | ConvertTo-Json | Set-Content -Path "src/Umbraco.Tests.AcceptanceTest/cypress.env.json" - task: Npm@1 name: PrepareTask displayName: npm install (AcceptanceTest) From fae00eb6d29189fecdf906ae1afe7ef1c4c8782e Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Wed, 24 Mar 2021 05:58:40 +0100 Subject: [PATCH 162/188] Add Linux acceptance tests - Trial 15 --- build/azure-pipelines.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/build/azure-pipelines.yml b/build/azure-pipelines.yml index 8dbc3355db..5cc2ca6f1e 100644 --- a/build/azure-pipelines.yml +++ b/build/azure-pipelines.yml @@ -253,8 +253,7 @@ stages: inputs: scriptType: inlineScript inlineScript: > - $jsonObject = "{ ""username"": ""$env:Umbraco__CMS__Unattended__UnattendedUserEmail"", ""password"" : ""$env:Umbraco__CMS__Unattended__UnattendedUserPassword"" }" - $jsonObject | ConvertTo-Json | Set-Content -Path "src/Umbraco.Tests.AcceptanceTest/cypress.env.json" + "{ ""username"": ""$env:Umbraco__CMS__Unattended__UnattendedUserEmail"", ""password"" : ""$env:Umbraco__CMS__Unattended__UnattendedUserPassword"" }" | Set-Content -Path "src/Umbraco.Tests.AcceptanceTest/cypress.env.json" - task: Npm@1 name: PrepareTask displayName: npm install (AcceptanceTest) From 9f9c1ffbb074170b18ca387fe92735ab8d72f605 Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Wed, 24 Mar 2021 06:13:40 +0100 Subject: [PATCH 163/188] Add Linux acceptance tests - Trial 16 --- build/azure-pipelines.yml | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/build/azure-pipelines.yml b/build/azure-pipelines.yml index 5cc2ca6f1e..73546bc404 100644 --- a/build/azure-pipelines.yml +++ b/build/azure-pipelines.yml @@ -248,12 +248,11 @@ stages: displayName: dotnet run (Netcore) # - powershell: dotnet run --no-build -p ./src/Umbraco.Web.UI.NetCore/Umbraco.Web.UI.NetCore.csproj # displayName: dotnet run (Netcore) - - task: PowerShell@1 - displayName: Generate Cypress.env.json + + - task: Bash@3 inputs: - scriptType: inlineScript - inlineScript: > - "{ ""username"": ""$env:Umbraco__CMS__Unattended__UnattendedUserEmail"", ""password"" : ""$env:Umbraco__CMS__Unattended__UnattendedUserPassword"" }" | Set-Content -Path "src/Umbraco.Tests.AcceptanceTest/cypress.env.json" + targetType: 'inline' + script: 'echo ''{ "username": "$Umbraco__CMS__Unattended__UnattendedUserEmail", "password": "$Umbraco__CMS__Unattended__UnattendedUserPassword" }'' > ''src/Umbraco.Tests.AcceptanceTest/cypress.env.json''' - task: Npm@1 name: PrepareTask displayName: npm install (AcceptanceTest) From b4b5f211f32746686c730a613c04ff1de4be4038 Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Wed, 24 Mar 2021 06:24:08 +0100 Subject: [PATCH 164/188] Add Linux acceptance tests - Trial 17 --- build/azure-pipelines.yml | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/build/azure-pipelines.yml b/build/azure-pipelines.yml index 73546bc404..6f015aba5b 100644 --- a/build/azure-pipelines.yml +++ b/build/azure-pipelines.yml @@ -244,11 +244,10 @@ stages: gulpFile: src/Umbraco.Web.UI.Client/gulpfile.js targets: build workingDirectory: src/Umbraco.Web.UI.Client - - powershell: Start-Process -FilePath "dotnet" -ArgumentList "run", "-p", "src/Umbraco.Web.UI.Netcore/Umbraco.Web.UI.Netcore.csproj" - displayName: dotnet run (Netcore) - # - powershell: dotnet run --no-build -p ./src/Umbraco.Web.UI.NetCore/Umbraco.Web.UI.NetCore.csproj - # displayName: dotnet run (Netcore) - + - task: Bash@3 + inputs: + targetType: 'inline' + script: 'dotnet run -p ./src/Umbraco.Web.UI.NetCore/Umbraco.Web.UI.NetCore.csproj &' - task: Bash@3 inputs: targetType: 'inline' From 5fbacaf3c5133edbe3a7a2edfb1b06c56668ed06 Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Wed, 24 Mar 2021 06:52:28 +0100 Subject: [PATCH 165/188] Add Linux acceptance tests - Trial 18 --- build/azure-pipelines.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/build/azure-pipelines.yml b/build/azure-pipelines.yml index 6f015aba5b..8c17ca2516 100644 --- a/build/azure-pipelines.yml +++ b/build/azure-pipelines.yml @@ -245,10 +245,12 @@ stages: targets: build workingDirectory: src/Umbraco.Web.UI.Client - task: Bash@3 + displayName: dotnet run (Netcore) inputs: targetType: 'inline' - script: 'dotnet run -p ./src/Umbraco.Web.UI.NetCore/Umbraco.Web.UI.NetCore.csproj &' + script: 'nohup dotnet run -p ./src/Umbraco.Web.UI.NetCore/Umbraco.Web.UI.NetCore.csproj &' - task: Bash@3 + displayName: Generate Cypress.env.json inputs: targetType: 'inline' script: 'echo ''{ "username": "$Umbraco__CMS__Unattended__UnattendedUserEmail", "password": "$Umbraco__CMS__Unattended__UnattendedUserPassword" }'' > ''src/Umbraco.Tests.AcceptanceTest/cypress.env.json''' From 7210022068dc5cd06d238d895f798c517d317adf Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Wed, 24 Mar 2021 07:06:04 +0100 Subject: [PATCH 166/188] Add Linux acceptance tests - Trial 19 --- build/azure-pipelines.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build/azure-pipelines.yml b/build/azure-pipelines.yml index 8c17ca2516..3d864c630a 100644 --- a/build/azure-pipelines.yml +++ b/build/azure-pipelines.yml @@ -248,7 +248,7 @@ stages: displayName: dotnet run (Netcore) inputs: targetType: 'inline' - script: 'nohup dotnet run -p ./src/Umbraco.Web.UI.NetCore/Umbraco.Web.UI.NetCore.csproj &' + script: 'dotnet run -p ./src/Umbraco.Web.UI.NetCore/Umbraco.Web.UI.NetCore.csproj' - task: Bash@3 displayName: Generate Cypress.env.json inputs: From 3b4a14a6155aeeaad26b4045a73ca0c95bd373e9 Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Wed, 24 Mar 2021 07:13:43 +0100 Subject: [PATCH 167/188] Add Linux acceptance tests - Trial 20 --- build/azure-pipelines.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/build/azure-pipelines.yml b/build/azure-pipelines.yml index 3d864c630a..26544fac09 100644 --- a/build/azure-pipelines.yml +++ b/build/azure-pipelines.yml @@ -222,8 +222,11 @@ stages: displayName: Use .Net Core sdk 5.x inputs: version: 5.x - - powershell: sqlcmd -S . -U sa -P $env:SA_PASSWORD -Q "CREATE DATABASE $env:UmbracoDatabaseName" + - task: Bash@3 displayName: Create database + inputs: + targetType: 'inline' + script: 'sqlcmd -S . -U sa -P $SA_PASSWORD -Q "CREATE DATABASE $UmbracoDatabaseName"' # - task: DotNetCoreCLI@2 # displayName: dotnet build (Netcore) # inputs: From 9394a08c212dea225fb9bbf932b2623ec3f30cc6 Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Wed, 24 Mar 2021 07:16:47 +0100 Subject: [PATCH 168/188] Add Linux acceptance tests - Trial 21 --- build/azure-pipelines.yml | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/build/azure-pipelines.yml b/build/azure-pipelines.yml index 26544fac09..4b8d2b3b6d 100644 --- a/build/azure-pipelines.yml +++ b/build/azure-pipelines.yml @@ -226,12 +226,7 @@ stages: displayName: Create database inputs: targetType: 'inline' - script: 'sqlcmd -S . -U sa -P $SA_PASSWORD -Q "CREATE DATABASE $UmbracoDatabaseName"' - # - task: DotNetCoreCLI@2 - # displayName: dotnet build (Netcore) - # inputs: - # command: build - # projects: '**/Umbraco.Web.UI.Netcore.csproj' + script: 'sqlcmd -S . -U sa -P $SA_PASSWORD -Q "CREATE DATABASE Cypress"' - task: NodeTool@0 displayName: Use Node 11.x inputs: From 6246d8e32daaecb3c8c0497186a86cc45d6492c3 Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Wed, 24 Mar 2021 07:22:31 +0100 Subject: [PATCH 169/188] Add Linux acceptance tests - Trial 22 --- build/azure-pipelines.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build/azure-pipelines.yml b/build/azure-pipelines.yml index 4b8d2b3b6d..4718a826e0 100644 --- a/build/azure-pipelines.yml +++ b/build/azure-pipelines.yml @@ -246,7 +246,7 @@ stages: displayName: dotnet run (Netcore) inputs: targetType: 'inline' - script: 'dotnet run -p ./src/Umbraco.Web.UI.NetCore/Umbraco.Web.UI.NetCore.csproj' + script: 'nohup dotnet run -p ./src/Umbraco.Web.UI.NetCore/Umbraco.Web.UI.NetCore.csproj &' - task: Bash@3 displayName: Generate Cypress.env.json inputs: From 68a3b5bc8c9bd642d615a9f33b3e67d96629d346 Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Wed, 24 Mar 2021 07:47:19 +0100 Subject: [PATCH 170/188] Add Linux acceptance tests - Trial 23 --- build/azure-pipelines.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/build/azure-pipelines.yml b/build/azure-pipelines.yml index 4718a826e0..409e363201 100644 --- a/build/azure-pipelines.yml +++ b/build/azure-pipelines.yml @@ -203,7 +203,7 @@ stages: condition: failed() inputs: targetPath: '$(Build.SourcesDirectory)/src/Umbraco.Tests.AcceptanceTest/cypress/artifacts' - artifact: 'Test artifacts' + artifact: 'Test artifacts - Windows' - job: Linux_Acceptance_tests displayName: Linux variables: @@ -251,7 +251,7 @@ stages: displayName: Generate Cypress.env.json inputs: targetType: 'inline' - script: 'echo ''{ "username": "$Umbraco__CMS__Unattended__UnattendedUserEmail", "password": "$Umbraco__CMS__Unattended__UnattendedUserPassword" }'' > ''src/Umbraco.Tests.AcceptanceTest/cypress.env.json''' + script: 'echo "{ \"username\": \"$Umbraco__CMS__Unattended__UnattendedUserEmail\", \"password\": \"$Umbraco__CMS__Unattended__UnattendedUserPassword\" }" > "src/Umbraco.Tests.AcceptanceTest/cypress.env.json"' - task: Npm@1 name: PrepareTask displayName: npm install (AcceptanceTest) @@ -292,7 +292,7 @@ stages: condition: failed() inputs: targetPath: '$(Build.SourcesDirectory)/src/Umbraco.Tests.AcceptanceTest/cypress/artifacts' - artifact: 'Test artifacts' + artifact: 'Test artifacts - Linux' - stage: Artifacts dependsOn: [] jobs: From 4e2072af63e9f57d9605b0274c79df785214c2ce Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Wed, 24 Mar 2021 08:00:31 +0100 Subject: [PATCH 171/188] Add Linux acceptance tests - Trial 24 --- build/azure-pipelines.yml | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/build/azure-pipelines.yml b/build/azure-pipelines.yml index 409e363201..88af31fccb 100644 --- a/build/azure-pipelines.yml +++ b/build/azure-pipelines.yml @@ -226,7 +226,10 @@ stages: displayName: Create database inputs: targetType: 'inline' - script: 'sqlcmd -S . -U sa -P $SA_PASSWORD -Q "CREATE DATABASE Cypress"' + script: 'sqlcmd -S . -U sa -P $SA_PASSWORD -Q "CREATE DATABASE $DBNAME"' + env: + DBNAME: $(UmbracoDatabaseName) + SA_PASSWORD: $(SA_PASSWORD) - task: NodeTool@0 displayName: Use Node 11.x inputs: @@ -251,7 +254,10 @@ stages: displayName: Generate Cypress.env.json inputs: targetType: 'inline' - script: 'echo "{ \"username\": \"$Umbraco__CMS__Unattended__UnattendedUserEmail\", \"password\": \"$Umbraco__CMS__Unattended__UnattendedUserPassword\" }" > "src/Umbraco.Tests.AcceptanceTest/cypress.env.json"' + script: 'echo "{ \"username\": \"$USERNAME\", \"password\": \"$PASSWORD\" }" > "src/Umbraco.Tests.AcceptanceTest/cypress.env.json"' + env: + USERNAME: $(Umbraco__CMS__Unattended__UnattendedUserEmail) + PASSWORD: $(Umbraco__CMS__Unattended__UnattendedUserPassword) - task: Npm@1 name: PrepareTask displayName: npm install (AcceptanceTest) From 36ed141065e5fc7a727f7390734ad259e63d2cc6 Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Wed, 24 Mar 2021 08:02:35 +0100 Subject: [PATCH 172/188] Add Linux acceptance tests - Trial 25 --- build/azure-pipelines.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/build/azure-pipelines.yml b/build/azure-pipelines.yml index 88af31fccb..bce240e33d 100644 --- a/build/azure-pipelines.yml +++ b/build/azure-pipelines.yml @@ -227,9 +227,9 @@ stages: inputs: targetType: 'inline' script: 'sqlcmd -S . -U sa -P $SA_PASSWORD -Q "CREATE DATABASE $DBNAME"' - env: - DBNAME: $(UmbracoDatabaseName) - SA_PASSWORD: $(SA_PASSWORD) + env: + DBNAME: $(UmbracoDatabaseName) + SA_PASSWORD: $(SA_PASSWORD) - task: NodeTool@0 displayName: Use Node 11.x inputs: @@ -255,9 +255,9 @@ stages: inputs: targetType: 'inline' script: 'echo "{ \"username\": \"$USERNAME\", \"password\": \"$PASSWORD\" }" > "src/Umbraco.Tests.AcceptanceTest/cypress.env.json"' - env: - USERNAME: $(Umbraco__CMS__Unattended__UnattendedUserEmail) - PASSWORD: $(Umbraco__CMS__Unattended__UnattendedUserPassword) + env: + USERNAME: $(Umbraco__CMS__Unattended__UnattendedUserEmail) + PASSWORD: $(Umbraco__CMS__Unattended__UnattendedUserPassword) - task: Npm@1 name: PrepareTask displayName: npm install (AcceptanceTest) From 68430c196eb7175cfaab24bf7255d5afa2de401a Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Wed, 24 Mar 2021 09:41:29 +0100 Subject: [PATCH 173/188] Do not copy umbraco folder content to output folder. Only relevant for publish folder (as far as we know) --- src/Umbraco.Web.UI.NetCore/Umbraco.Web.UI.NetCore.csproj | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/Umbraco.Web.UI.NetCore/Umbraco.Web.UI.NetCore.csproj b/src/Umbraco.Web.UI.NetCore/Umbraco.Web.UI.NetCore.csproj index 2324c4c252..bb98df64d6 100644 --- a/src/Umbraco.Web.UI.NetCore/Umbraco.Web.UI.NetCore.csproj +++ b/src/Umbraco.Web.UI.NetCore/Umbraco.Web.UI.NetCore.csproj @@ -2,7 +2,7 @@ net5.0 - Umbraco.Cms.Web.UI.NetCore + Umbraco.Cms.Web.UI.NetCore bin\Release\Umbraco.Web.UI.NetCore.xml @@ -54,12 +54,10 @@ true - PreserveNewest Always true - PreserveNewest Always From bffe1576d1810f1a5a896af3a1c674c4e7c41532 Mon Sep 17 00:00:00 2001 From: Mole Date: Wed, 24 Mar 2021 10:06:15 +0100 Subject: [PATCH 174/188] Try a fix for language tests --- .../cypress/integration/Settings/languages.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Umbraco.Tests.AcceptanceTest/cypress/integration/Settings/languages.ts b/src/Umbraco.Tests.AcceptanceTest/cypress/integration/Settings/languages.ts index 541c6d213d..43f32a0821 100644 --- a/src/Umbraco.Tests.AcceptanceTest/cypress/integration/Settings/languages.ts +++ b/src/Umbraco.Tests.AcceptanceTest/cypress/integration/Settings/languages.ts @@ -6,7 +6,10 @@ context('Languages', () => { }); it('Add language', () => { - const name = "Afrikaans"; // Must be an option in the select box + // For some reason the languages to chose fom seems to be translated differently than normal, as an example: + // My system is set to EN (US), but most languages are translated into Danish for some reason + // Aghem seems untranslated though? + const name = "Aghem"; // Must be an option in the select box cy.umbracoEnsureLanguageNameNotExists(name); From f0b39ce890e8f31523739a68d317b24df79fdb76 Mon Sep 17 00:00:00 2001 From: Mole Date: Wed, 24 Mar 2021 10:57:02 +0100 Subject: [PATCH 175/188] Align 'Add language' test to netcore --- .../cypress/integration/Settings/languages.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Umbraco.Tests.AcceptanceTest/cypress/integration/Settings/languages.ts b/src/Umbraco.Tests.AcceptanceTest/cypress/integration/Settings/languages.ts index 49bcf94943..336e5793d9 100644 --- a/src/Umbraco.Tests.AcceptanceTest/cypress/integration/Settings/languages.ts +++ b/src/Umbraco.Tests.AcceptanceTest/cypress/integration/Settings/languages.ts @@ -6,7 +6,10 @@ context('Languages', () => { }); it('Add language', () => { - const name = "Kyrgyz (Kyrgyzstan)"; // Must be an option in the select box + // For some reason the languages to chose fom seems to be translated differently than normal, as an example: + // My system is set to EN (US), but most languages are translated into Danish for some reason + // Aghem seems untranslated though? + const name = "Aghem"; // Must be an option in the select box cy.umbracoEnsureLanguageNameNotExists(name); From f59d36624d2767815768a571c801a870107042fa Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Wed, 24 Mar 2021 11:05:50 +0100 Subject: [PATCH 176/188] Do not copy umbraco folder content to output folder. Only relevant for publish folder (as far as we know) --- build/templates/UmbracoSolution/UmbracoSolution.csproj | 2 -- 1 file changed, 2 deletions(-) diff --git a/build/templates/UmbracoSolution/UmbracoSolution.csproj b/build/templates/UmbracoSolution/UmbracoSolution.csproj index d1bfead7a8..08643bb150 100644 --- a/build/templates/UmbracoSolution/UmbracoSolution.csproj +++ b/build/templates/UmbracoSolution/UmbracoSolution.csproj @@ -33,12 +33,10 @@ true - PreserveNewest Always true - PreserveNewest Always From 55354b969bb3feab3e46d98c5d82afeb43ba0e43 Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Wed, 24 Mar 2021 11:43:33 +0100 Subject: [PATCH 177/188] Added continueOnError: true so the acceptance tests are not blocking a merge --- build/azure-pipelines.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/build/azure-pipelines.yml b/build/azure-pipelines.yml index bce240e33d..6dde647e06 100644 --- a/build/azure-pipelines.yml +++ b/build/azure-pipelines.yml @@ -171,6 +171,7 @@ stages: - task: Npm@1 displayName: Run Cypress (Desktop) condition: always() + continueOnError: true inputs: workingDir: src\Umbraco.Tests.AcceptanceTest command: 'custom' @@ -266,6 +267,7 @@ stages: - task: Npm@1 displayName: Run Cypress (Desktop) condition: always() + continueOnError: true inputs: workingDir: src/Umbraco.Tests.AcceptanceTest command: 'custom' From efb9aacfe2f8b495f002e1f0d08bb861376fb628 Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Wed, 24 Mar 2021 12:30:08 +0100 Subject: [PATCH 178/188] Bump version to beta001 --- build/templates/UmbracoPackage/.template.config/template.json | 2 +- build/templates/UmbracoSolution/.template.config/template.json | 2 +- src/Directory.Build.props | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/build/templates/UmbracoPackage/.template.config/template.json b/build/templates/UmbracoPackage/.template.config/template.json index b097e74c4c..56989e3e97 100644 --- a/build/templates/UmbracoPackage/.template.config/template.json +++ b/build/templates/UmbracoPackage/.template.config/template.json @@ -24,7 +24,7 @@ "version": { "type": "parameter", "datatype": "string", - "defaultValue": "9.0.0-alpha004", + "defaultValue": "9.0.0-beta001", "description": "The version of Umbraco to load using NuGet", "replaces": "UMBRACO_VERSION_FROM_TEMPLATE" }, diff --git a/build/templates/UmbracoSolution/.template.config/template.json b/build/templates/UmbracoSolution/.template.config/template.json index 8a0b168594..7e45b01f4d 100644 --- a/build/templates/UmbracoSolution/.template.config/template.json +++ b/build/templates/UmbracoSolution/.template.config/template.json @@ -24,7 +24,7 @@ "version": { "type": "parameter", "datatype": "string", - "defaultValue": "9.0.0-alpha004", + "defaultValue": "9.0.0-beta001", "description": "The version of Umbraco to load using NuGet", "replaces": "UMBRACO_VERSION_FROM_TEMPLATE" }, diff --git a/src/Directory.Build.props b/src/Directory.Build.props index c4346a0603..b16405633f 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -2,7 +2,7 @@ 9.0.0 9.0.0 - 9.0.0-alpha004 + 9.0.0-beta001 9.0.0 9.0 en-US From 7a699c3fa974016f670b7856ea11abbbefdf98b1 Mon Sep 17 00:00:00 2001 From: Mole Date: Wed, 24 Mar 2021 14:29:09 +0100 Subject: [PATCH 179/188] Add notification classes --- .../DictionaryItemDeletedNotification.cs | 15 ++++++++++++++ .../DictionaryItemDeletingNotification.cs | 20 +++++++++++++++++++ .../DictionaryItemSavedNotification.cs | 20 +++++++++++++++++++ .../DictionaryItemSavingNotification.cs | 20 +++++++++++++++++++ .../LanguageDeletedNotification.cs | 15 ++++++++++++++ .../LanguageDeletingNotification.cs | 20 +++++++++++++++++++ .../LanguageSavedNotification.cs | 20 +++++++++++++++++++ .../LanguageSavingNotification.cs | 20 +++++++++++++++++++ 8 files changed, 150 insertions(+) create mode 100644 src/Umbraco.Infrastructure/Services/Notifications/DictionaryItemDeletedNotification.cs create mode 100644 src/Umbraco.Infrastructure/Services/Notifications/DictionaryItemDeletingNotification.cs create mode 100644 src/Umbraco.Infrastructure/Services/Notifications/DictionaryItemSavedNotification.cs create mode 100644 src/Umbraco.Infrastructure/Services/Notifications/DictionaryItemSavingNotification.cs create mode 100644 src/Umbraco.Infrastructure/Services/Notifications/LanguageDeletedNotification.cs create mode 100644 src/Umbraco.Infrastructure/Services/Notifications/LanguageDeletingNotification.cs create mode 100644 src/Umbraco.Infrastructure/Services/Notifications/LanguageSavedNotification.cs create mode 100644 src/Umbraco.Infrastructure/Services/Notifications/LanguageSavingNotification.cs diff --git a/src/Umbraco.Infrastructure/Services/Notifications/DictionaryItemDeletedNotification.cs b/src/Umbraco.Infrastructure/Services/Notifications/DictionaryItemDeletedNotification.cs new file mode 100644 index 0000000000..3d3b9588d0 --- /dev/null +++ b/src/Umbraco.Infrastructure/Services/Notifications/DictionaryItemDeletedNotification.cs @@ -0,0 +1,15 @@ +// Copyright (c) Umbraco. +// See LICENSE for more details. + +using Umbraco.Cms.Core.Events; +using Umbraco.Cms.Core.Models; + +namespace Umbraco.Cms.Infrastructure.Services.Notifications +{ + public class DictionaryItemDeletedNotification : DeletedNotification + { + public DictionaryItemDeletedNotification(IDictionaryItem target, EventMessages messages) : base(target, messages) + { + } + } +} diff --git a/src/Umbraco.Infrastructure/Services/Notifications/DictionaryItemDeletingNotification.cs b/src/Umbraco.Infrastructure/Services/Notifications/DictionaryItemDeletingNotification.cs new file mode 100644 index 0000000000..ff85e00a81 --- /dev/null +++ b/src/Umbraco.Infrastructure/Services/Notifications/DictionaryItemDeletingNotification.cs @@ -0,0 +1,20 @@ +// Copyright (c) Umbraco. +// See LICENSE for more details. + +using System.Collections.Generic; +using Umbraco.Cms.Core.Events; +using Umbraco.Cms.Core.Models; + +namespace Umbraco.Cms.Infrastructure.Services.Notifications +{ + public class DictionaryItemDeletingNotification : DeletingNotification + { + public DictionaryItemDeletingNotification(IDictionaryItem target, EventMessages messages) : base(target, messages) + { + } + + public DictionaryItemDeletingNotification(IEnumerable target, EventMessages messages) : base(target, messages) + { + } + } +} diff --git a/src/Umbraco.Infrastructure/Services/Notifications/DictionaryItemSavedNotification.cs b/src/Umbraco.Infrastructure/Services/Notifications/DictionaryItemSavedNotification.cs new file mode 100644 index 0000000000..3a64e35979 --- /dev/null +++ b/src/Umbraco.Infrastructure/Services/Notifications/DictionaryItemSavedNotification.cs @@ -0,0 +1,20 @@ +// Copyright (c) Umbraco. +// See LICENSE for more details. + +using System.Collections.Generic; +using Umbraco.Cms.Core.Events; +using Umbraco.Cms.Core.Models; + +namespace Umbraco.Cms.Infrastructure.Services.Notifications +{ + public class DictionaryItemSavedNotification : SavedNotification + { + public DictionaryItemSavedNotification(IDictionaryItem target, EventMessages messages) : base(target, messages) + { + } + + public DictionaryItemSavedNotification(IEnumerable target, EventMessages messages) : base(target, messages) + { + } + } +} diff --git a/src/Umbraco.Infrastructure/Services/Notifications/DictionaryItemSavingNotification.cs b/src/Umbraco.Infrastructure/Services/Notifications/DictionaryItemSavingNotification.cs new file mode 100644 index 0000000000..5f3d94697e --- /dev/null +++ b/src/Umbraco.Infrastructure/Services/Notifications/DictionaryItemSavingNotification.cs @@ -0,0 +1,20 @@ +// Copyright (c) Umbraco. +// See LICENSE for more details. + +using System.Collections.Generic; +using Umbraco.Cms.Core.Events; +using Umbraco.Cms.Core.Models; + +namespace Umbraco.Cms.Infrastructure.Services.Notifications +{ + public class DictionaryItemSavingNotification : SavingNotification + { + public DictionaryItemSavingNotification(IDictionaryItem target, EventMessages messages) : base(target, messages) + { + } + + public DictionaryItemSavingNotification(IEnumerable target, EventMessages messages) : base(target, messages) + { + } + } +} diff --git a/src/Umbraco.Infrastructure/Services/Notifications/LanguageDeletedNotification.cs b/src/Umbraco.Infrastructure/Services/Notifications/LanguageDeletedNotification.cs new file mode 100644 index 0000000000..cba1c59406 --- /dev/null +++ b/src/Umbraco.Infrastructure/Services/Notifications/LanguageDeletedNotification.cs @@ -0,0 +1,15 @@ +// Copyright (c) Umbraco. +// See LICENSE for more details. + +using Umbraco.Cms.Core.Events; +using Umbraco.Cms.Core.Models; + +namespace Umbraco.Cms.Infrastructure.Services.Notifications +{ + public class LanguageDeletedNotification : DeletedNotification + { + public LanguageDeletedNotification(ILanguage target, EventMessages messages) : base(target, messages) + { + } + } +} diff --git a/src/Umbraco.Infrastructure/Services/Notifications/LanguageDeletingNotification.cs b/src/Umbraco.Infrastructure/Services/Notifications/LanguageDeletingNotification.cs new file mode 100644 index 0000000000..79215e25af --- /dev/null +++ b/src/Umbraco.Infrastructure/Services/Notifications/LanguageDeletingNotification.cs @@ -0,0 +1,20 @@ +// Copyright (c) Umbraco. +// See LICENSE for more details. + +using System.Collections.Generic; +using Umbraco.Cms.Core.Events; +using Umbraco.Cms.Core.Models; + +namespace Umbraco.Cms.Infrastructure.Services.Notifications +{ + public class LanguageDeletingNotification : DeletingNotification + { + public LanguageDeletingNotification(ILanguage target, EventMessages messages) : base(target, messages) + { + } + + public LanguageDeletingNotification(IEnumerable target, EventMessages messages) : base(target, messages) + { + } + } +} diff --git a/src/Umbraco.Infrastructure/Services/Notifications/LanguageSavedNotification.cs b/src/Umbraco.Infrastructure/Services/Notifications/LanguageSavedNotification.cs new file mode 100644 index 0000000000..87c3644df9 --- /dev/null +++ b/src/Umbraco.Infrastructure/Services/Notifications/LanguageSavedNotification.cs @@ -0,0 +1,20 @@ +// Copyright (c) Umbraco. +// See LICENSE for more details. + +using System.Collections.Generic; +using Umbraco.Cms.Core.Events; +using Umbraco.Cms.Core.Models; + +namespace Umbraco.Cms.Infrastructure.Services.Notifications +{ + public class LanguageSavedNotification : SavedNotification + { + public LanguageSavedNotification(ILanguage target, EventMessages messages) : base(target, messages) + { + } + + public LanguageSavedNotification(IEnumerable target, EventMessages messages) : base(target, messages) + { + } + } +} diff --git a/src/Umbraco.Infrastructure/Services/Notifications/LanguageSavingNotification.cs b/src/Umbraco.Infrastructure/Services/Notifications/LanguageSavingNotification.cs new file mode 100644 index 0000000000..db416799d9 --- /dev/null +++ b/src/Umbraco.Infrastructure/Services/Notifications/LanguageSavingNotification.cs @@ -0,0 +1,20 @@ +// Copyright (c) Umbraco. +// See LICENSE for more details. + +using System.Collections.Generic; +using Umbraco.Cms.Core.Events; +using Umbraco.Cms.Core.Models; + +namespace Umbraco.Cms.Infrastructure.Services.Notifications +{ + public class LanguageSavingNotification : SavingNotification + { + public LanguageSavingNotification(ILanguage target, EventMessages messages) : base(target, messages) + { + } + + public LanguageSavingNotification(IEnumerable target, EventMessages messages) : base(target, messages) + { + } + } +} From 70a582d1f0a4d5554a7b1f16ccf96f602e2e6ed7 Mon Sep 17 00:00:00 2001 From: Mole Date: Wed, 24 Mar 2021 15:04:34 +0100 Subject: [PATCH 180/188] Add new cypress tests --- .../cypress/integration/Content/content.ts | 204 +++++++++++++++++- src/Umbraco.Tests.AcceptanceTest/package.json | 2 +- 2 files changed, 204 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Tests.AcceptanceTest/cypress/integration/Content/content.ts b/src/Umbraco.Tests.AcceptanceTest/cypress/integration/Content/content.ts index 1a40e8451f..6df39be44c 100644 --- a/src/Umbraco.Tests.AcceptanceTest/cypress/integration/Content/content.ts +++ b/src/Umbraco.Tests.AcceptanceTest/cypress/integration/Content/content.ts @@ -1,5 +1,13 @@ /// -import { DocumentTypeBuilder, ContentBuilder, AliasHelper } from 'umbraco-cypress-testhelpers'; +import { + DocumentTypeBuilder, + ContentBuilder, + AliasHelper, + GridDataTypeBuilder, + PartialViewMacroBuilder, + MacroBuilder +} from 'umbraco-cypress-testhelpers'; + context('Content', () => { beforeEach(() => { @@ -14,6 +22,23 @@ context('Content', () => { cy.get('.umb-tree-item__inner').should('exist', {timeout: 10000}); } + function createSimpleMacro(name){ + const insertMacro = new PartialViewMacroBuilder() + .withName(name) + .withContent(`@inherits Umbraco.Web.Macros.PartialViewMacroPage +

Acceptance test

`) + .build(); + + const macroWithPartial = new MacroBuilder() + .withName(name) + .withPartialViewMacro(insertMacro) + .withRenderInEditor() + .withUseInEditor() + .build(); + + cy.saveMacroWithPartial(macroWithPartial); + } + it('Copy content', () => { const rootDocTypeName = "Test document type"; const childDocTypeName = "Child test document type"; @@ -596,4 +621,181 @@ context('Content', () => { cy.umbracoEnsureTemplateNameNotExists(pickerDocTypeName); cy.umbracoEnsureDocumentTypeNameNotExists(pickedDocTypeName); }); + + it('Content with macro in RTE', () => { + const viewMacroName = 'Content with macro in RTE'; + const partialFileName = viewMacroName + '.cshtml'; + + cy.umbracoEnsureMacroNameNotExists(viewMacroName); + cy.umbracoEnsurePartialViewMacroFileNameNotExists(partialFileName); + cy.umbracoEnsureDocumentTypeNameNotExists(viewMacroName); + cy.umbracoEnsureTemplateNameNotExists(viewMacroName); + cy.deleteAllContent(); + + // First thing first we got to create the macro we will be inserting + createSimpleMacro(viewMacroName); + + // Now we need to create a document type with a rich text editor where we can insert the macro + // The document type must have a template as well in order to ensure that the content is displayed correctly + const alias = AliasHelper.toAlias(viewMacroName); + const docType = new DocumentTypeBuilder() + .withName(viewMacroName) + .withAlias(alias) + .withAllowAsRoot(true) + .withDefaultTemplate(alias) + .addGroup() + .addRichTextProperty() + .withAlias('text') + .done() + .done() + .build(); + + cy.saveDocumentType(docType).then((generatedDocType) => { + // Might as wel initally create the content here, the less GUI work during the test the better + const contentNode = new ContentBuilder() + .withContentTypeAlias(generatedDocType["alias"]) + .withAction('saveNew') + .addVariant() + .withName(viewMacroName) + .withSave(true) + .done() + .build(); + + cy.saveContent(contentNode); + }); + + // Edit the macro template in order to have something to verify on when rendered. + cy.editTemplate(viewMacroName, `@inherits Umbraco.Web.Mvc.UmbracoViewPage +@using ContentModels = Umbraco.Web.PublishedModels; +@{ + Layout = null; +} +@{ + if (Model.HasValue("text")){ + @(Model.Value("text")) + } +} `); + + // Enter content + refreshContentTree(); + cy.umbracoTreeItem("content", [viewMacroName]).click(); + + // Insert macro + cy.get('#mceu_13-button').click(); + cy.get('.umb-card-grid-item').contains(viewMacroName).click(); + + // Save and publish + cy.umbracoButtonByLabelKey('buttons_saveAndPublish').click(); + cy.umbracoSuccessNotification().should('be.visible'); + + // Ensure that the view gets rendered correctly + const expected = `

Acceptance test

 

`; + cy.umbracoVerifyRenderedViewContent('/', expected, true).should('be.true'); + + // Cleanup + cy.umbracoEnsureMacroNameNotExists(viewMacroName); + cy.umbracoEnsurePartialViewMacroFileNameNotExists(partialFileName); + cy.umbracoEnsureDocumentTypeNameNotExists(viewMacroName); + cy.umbracoEnsureTemplateNameNotExists(viewMacroName); + }); + + it('Content with macro in grid', () => { + const name = 'Content with macro in grid'; + const macroName = 'Grid macro'; + const macroFileName = macroName + '.cshtml'; + + cy.umbracoEnsureDataTypeNameNotExists(name); + cy.umbracoEnsureDocumentTypeNameNotExists(name); + cy.umbracoEnsureTemplateNameNotExists(name); + cy.umbracoEnsureMacroNameNotExists(macroName); + cy.umbracoEnsurePartialViewMacroFileNameNotExists(macroFileName); + cy.deleteAllContent(); + + createSimpleMacro(macroName); + + const grid = new GridDataTypeBuilder() + .withName(name) + .withDefaultGrid() + .build(); + + const alias = AliasHelper.toAlias(name); + // Save grid and get the ID + cy.saveDataType(grid).then((dataType) => { + // Create a document type using the data type + const docType = new DocumentTypeBuilder() + .withName(name) + .withAlias(alias) + .withAllowAsRoot(true) + .withDefaultTemplate(alias) + .addGroup() + .addCustomProperty(dataType['id']) + .withAlias('grid') + .done() + .done() + .build(); + + cy.saveDocumentType(docType).then((generatedDocType) => { + const contentNode = new ContentBuilder() + .withContentTypeAlias(generatedDocType["alias"]) + .addVariant() + .withName(name) + .withSave(true) + .done() + .build(); + + cy.saveContent(contentNode); + }); + }); + + // Edit the template to allow us to verify the rendered view + cy.editTemplate(name, `@inherits Umbraco.Web.Mvc.UmbracoViewPage +@using ContentModels = Umbraco.Web.PublishedModels; +@{ + Layout = null; +} +@Html.GetGridHtml(Model, "grid")`); + + // Act + // Enter content + refreshContentTree(); + cy.umbracoTreeItem("content", [name]).click(); + // Click add + cy.get(':nth-child(2) > .preview-row > .preview-col > .preview-cell').click(); // Choose 1 column layout. + cy.get('.umb-column > .templates-preview > :nth-child(2) > .ng-binding').click(); // Choose headline + cy.get('.umb-cell-placeholder').click(); + // Click macro + cy.get(':nth-child(4) > .umb-card-grid-item > :nth-child(1)').click(); + // Select the macro + cy.get('.umb-card-grid-item').contains(macroName).click(); + + // Save and publish + cy.umbracoButtonByLabelKey('buttons_saveAndPublish').click(); + cy.umbracoSuccessNotification().should('be.visible'); + + const expected = ` +
+
+
+
+
+
+
+

Acceptance test

+
+
+
+
+
+
+
` + + cy.umbracoVerifyRenderedViewContent('/', expected, true).should('be.true'); + + // Clean + cy.umbracoEnsureDataTypeNameNotExists(name); + cy.umbracoEnsureDocumentTypeNameNotExists(name); + cy.umbracoEnsureTemplateNameNotExists(name); + cy.umbracoEnsureMacroNameNotExists(macroName); + cy.umbracoEnsurePartialViewMacroFileNameNotExists(macroFileName); + }); }); diff --git a/src/Umbraco.Tests.AcceptanceTest/package.json b/src/Umbraco.Tests.AcceptanceTest/package.json index 069a1d85b7..caf75638e6 100644 --- a/src/Umbraco.Tests.AcceptanceTest/package.json +++ b/src/Umbraco.Tests.AcceptanceTest/package.json @@ -10,7 +10,7 @@ "cypress": "^6.8.0", "ncp": "^2.0.0", "prompt": "^1.0.0", - "umbraco-cypress-testhelpers": "^1.0.0-beta-52" + "umbraco-cypress-testhelpers": "^1.0.0-beta-53" }, "dependencies": { "typescript": "^3.9.2" From 64724ac85b9aa362ced6cc33952205f5edb5b8ae Mon Sep 17 00:00:00 2001 From: Mole Date: Wed, 24 Mar 2021 15:11:26 +0100 Subject: [PATCH 181/188] Add indentation --- .../cypress/integration/Content/content.ts | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/Umbraco.Tests.AcceptanceTest/cypress/integration/Content/content.ts b/src/Umbraco.Tests.AcceptanceTest/cypress/integration/Content/content.ts index 6df39be44c..b7f0b40fa6 100644 --- a/src/Umbraco.Tests.AcceptanceTest/cypress/integration/Content/content.ts +++ b/src/Umbraco.Tests.AcceptanceTest/cypress/integration/Content/content.ts @@ -644,9 +644,9 @@ context('Content', () => { .withAllowAsRoot(true) .withDefaultTemplate(alias) .addGroup() - .addRichTextProperty() - .withAlias('text') - .done() + .addRichTextProperty() + .withAlias('text') + .done() .done() .build(); @@ -656,8 +656,8 @@ context('Content', () => { .withContentTypeAlias(generatedDocType["alias"]) .withAction('saveNew') .addVariant() - .withName(viewMacroName) - .withSave(true) + .withName(viewMacroName) + .withSave(true) .done() .build(); @@ -728,9 +728,9 @@ context('Content', () => { .withAllowAsRoot(true) .withDefaultTemplate(alias) .addGroup() - .addCustomProperty(dataType['id']) - .withAlias('grid') - .done() + .addCustomProperty(dataType['id']) + .withAlias('grid') + .done() .done() .build(); @@ -738,8 +738,8 @@ context('Content', () => { const contentNode = new ContentBuilder() .withContentTypeAlias(generatedDocType["alias"]) .addVariant() - .withName(name) - .withSave(true) + .withName(name) + .withSave(true) .done() .build(); From 69548e0b3324d12443816803f6215d5fbb6eed65 Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Wed, 24 Mar 2021 18:37:26 +0100 Subject: [PATCH 182/188] Fixed new tests to match namespaces from v9 --- .../cypress/integration/Content/content.ts | 28 +++++++++---------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/src/Umbraco.Tests.AcceptanceTest/cypress/integration/Content/content.ts b/src/Umbraco.Tests.AcceptanceTest/cypress/integration/Content/content.ts index b978a27317..25f2a83007 100644 --- a/src/Umbraco.Tests.AcceptanceTest/cypress/integration/Content/content.ts +++ b/src/Umbraco.Tests.AcceptanceTest/cypress/integration/Content/content.ts @@ -1,11 +1,11 @@ /// import { - DocumentTypeBuilder, - ContentBuilder, - AliasHelper, - GridDataTypeBuilder, - PartialViewMacroBuilder, - MacroBuilder + AliasHelper, + ContentBuilder, + DocumentTypeBuilder, + GridDataTypeBuilder, + MacroBuilder, + PartialViewMacroBuilder } from 'umbraco-cypress-testhelpers'; context('Content', () => { @@ -25,7 +25,7 @@ context('Content', () => { function createSimpleMacro(name){ const insertMacro = new PartialViewMacroBuilder() .withName(name) - .withContent(`@inherits Umbraco.Web.Macros.PartialViewMacroPage + .withContent(`@inherits Umbraco.Cms.Web.Common.Macros.PartialViewMacroPage

Acceptance test

`) .build(); @@ -660,10 +660,9 @@ context('Content', () => { }); // Edit the macro template in order to have something to verify on when rendered. - cy.editTemplate(viewMacroName, `@inherits Umbraco.Web.Mvc.UmbracoViewPage -@using ContentModels = Umbraco.Web.PublishedModels; + cy.editTemplate(viewMacroName, `@inherits Umbraco.Cms.Web.Common.Views.UmbracoViewPage @{ - Layout = null; + Layout = null; } @{ if (Model.HasValue("text")){ @@ -743,11 +742,10 @@ context('Content', () => { }); // Edit the template to allow us to verify the rendered view - cy.editTemplate(name, `@inherits Umbraco.Web.Mvc.UmbracoViewPage -@using ContentModels = Umbraco.Web.PublishedModels; -@{ - Layout = null; -} + cy.editTemplate(name, `@inherits Umbraco.Cms.Web.Common.Views.UmbracoViewPage + @{ + Layout = null; + } @Html.GetGridHtml(Model, "grid")`); // Act From 8d61b3f067fdecb20403ecf9ea5dfc9179b79635 Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Thu, 25 Mar 2021 07:05:08 +0100 Subject: [PATCH 183/188] https://github.com/umbraco/Umbraco-CMS/issues/10054 - Added TreeAlias to the remaining tree notifications --- .../Trees/MenuRenderingNotification.cs | 8 +++++++- .../Trees/RootNodeRenderingNotification.cs | 8 +++++++- src/Umbraco.Web.BackOffice/Trees/TreeControllerBase.cs | 4 ++-- 3 files changed, 16 insertions(+), 4 deletions(-) diff --git a/src/Umbraco.Web.BackOffice/Trees/MenuRenderingNotification.cs b/src/Umbraco.Web.BackOffice/Trees/MenuRenderingNotification.cs index 562708b377..cd30f2225e 100644 --- a/src/Umbraco.Web.BackOffice/Trees/MenuRenderingNotification.cs +++ b/src/Umbraco.Web.BackOffice/Trees/MenuRenderingNotification.cs @@ -17,6 +17,11 @@ namespace Umbraco.Cms.Web.BackOffice.Trees ///
public string NodeId { get; } + /// + /// The alias of the tree the menu is rendering for + /// + public string TreeAlias { get; } + /// /// The menu being rendered /// @@ -27,11 +32,12 @@ namespace Umbraco.Cms.Web.BackOffice.Trees /// public FormCollection QueryString { get; } - public MenuRenderingNotification(string nodeId, MenuItemCollection menu, FormCollection queryString) + public MenuRenderingNotification(string nodeId, MenuItemCollection menu, FormCollection queryString, string treeAlias) { NodeId = nodeId; Menu = menu; QueryString = queryString; + TreeAlias = treeAlias; } } } diff --git a/src/Umbraco.Web.BackOffice/Trees/RootNodeRenderingNotification.cs b/src/Umbraco.Web.BackOffice/Trees/RootNodeRenderingNotification.cs index fcf6a47c35..28a0d90326 100644 --- a/src/Umbraco.Web.BackOffice/Trees/RootNodeRenderingNotification.cs +++ b/src/Umbraco.Web.BackOffice/Trees/RootNodeRenderingNotification.cs @@ -14,15 +14,21 @@ namespace Umbraco.Cms.Web.BackOffice.Trees /// public TreeNode Node { get; } + /// + /// The alias of the tree the menu is rendering for + /// + public string TreeAlias { get; } + /// /// The query string of the current request /// public FormCollection QueryString { get; } - public RootNodeRenderingNotification(TreeNode node, FormCollection queryString) + public RootNodeRenderingNotification(TreeNode node, FormCollection queryString, string treeAlias) { Node = node; QueryString = queryString; + TreeAlias = treeAlias; } } } diff --git a/src/Umbraco.Web.BackOffice/Trees/TreeControllerBase.cs b/src/Umbraco.Web.BackOffice/Trees/TreeControllerBase.cs index ecebb0b041..87e58de496 100644 --- a/src/Umbraco.Web.BackOffice/Trees/TreeControllerBase.cs +++ b/src/Umbraco.Web.BackOffice/Trees/TreeControllerBase.cs @@ -111,7 +111,7 @@ namespace Umbraco.Cms.Web.BackOffice.Trees if (IsDialog(queryStrings)) node.RoutePath = "#"; - await _eventAggregator.PublishAsync(new RootNodeRenderingNotification(node, queryStrings)); + await _eventAggregator.PublishAsync(new RootNodeRenderingNotification(node, queryStrings, TreeAlias)); return node; } @@ -171,7 +171,7 @@ namespace Umbraco.Cms.Web.BackOffice.Trees var menu = menuResult.Value; //raise the event - await _eventAggregator.PublishAsync(new MenuRenderingNotification(id, menu, queryStrings)); + await _eventAggregator.PublishAsync(new MenuRenderingNotification(id, menu, queryStrings, TreeAlias)); return menu; } From 18083b715513887ee7df5ac2df5d4a116b632900 Mon Sep 17 00:00:00 2001 From: Mole Date: Thu, 25 Mar 2021 09:00:38 +0100 Subject: [PATCH 184/188] Switch LocalizationService over to event aggregator --- .../Services/Implement/LocalizationService.cs | 52 ++++++++++++------- 1 file changed, 32 insertions(+), 20 deletions(-) diff --git a/src/Umbraco.Infrastructure/Services/Implement/LocalizationService.cs b/src/Umbraco.Infrastructure/Services/Implement/LocalizationService.cs index abdda2e68c..f4072ccfb7 100644 --- a/src/Umbraco.Infrastructure/Services/Implement/LocalizationService.cs +++ b/src/Umbraco.Infrastructure/Services/Implement/LocalizationService.cs @@ -6,6 +6,7 @@ using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Persistence.Repositories; using Umbraco.Cms.Core.Scoping; +using Umbraco.Cms.Infrastructure.Services.Notifications; using Umbraco.Extensions; namespace Umbraco.Cms.Core.Services.Implement @@ -17,15 +18,23 @@ namespace Umbraco.Cms.Core.Services.Implement { private readonly IDictionaryRepository _dictionaryRepository; private readonly ILanguageRepository _languageRepository; + private readonly IEventAggregator _eventAggregator; private readonly IAuditRepository _auditRepository; - public LocalizationService(IScopeProvider provider, ILoggerFactory loggerFactory, IEventMessagesFactory eventMessagesFactory, - IDictionaryRepository dictionaryRepository, IAuditRepository auditRepository, ILanguageRepository languageRepository) + public LocalizationService( + IScopeProvider provider, + ILoggerFactory loggerFactory, + IEventMessagesFactory eventMessagesFactory, + IDictionaryRepository dictionaryRepository, + IAuditRepository auditRepository, + ILanguageRepository languageRepository, + IEventAggregator eventAggregator) : base(provider, loggerFactory, eventMessagesFactory) { _dictionaryRepository = dictionaryRepository; _auditRepository = auditRepository; _languageRepository = languageRepository; + _eventAggregator = eventAggregator; } /// @@ -88,9 +97,11 @@ namespace Umbraco.Cms.Core.Services.Implement item.Translations = translations; } - var saveEventArgs = new SaveEventArgs(item); - if (scope.Events.DispatchCancelable(SavingDictionaryItem, this, saveEventArgs)) + EventMessages eventMessages = EventMessagesFactory.Get(); + var savingNotification = new DictionaryItemSavingNotification(item, eventMessages); + + if (_eventAggregator.PublishCancelable(savingNotification)) { scope.Complete(); return item; @@ -100,8 +111,7 @@ namespace Umbraco.Cms.Core.Services.Implement // ensure the lazy Language callback is assigned EnsureDictionaryItemLanguageCallback(item); - saveEventArgs.CanCancel = false; - scope.Events.Dispatch(SavedDictionaryItem, this, saveEventArgs); + _eventAggregator.Publish(new DictionaryItemSavedNotification(item, eventMessages).WithStateFrom(savingNotification)); scope.Complete(); @@ -232,7 +242,9 @@ namespace Umbraco.Cms.Core.Services.Implement { using (var scope = ScopeProvider.CreateScope()) { - if (scope.Events.DispatchCancelable(SavingDictionaryItem, this, new SaveEventArgs(dictionaryItem))) + EventMessages eventMessages = EventMessagesFactory.Get(); + var savingNotification = new DictionaryItemSavingNotification(dictionaryItem, eventMessages); + if (_eventAggregator.PublishCancelable(savingNotification)) { scope.Complete(); return; @@ -244,7 +256,7 @@ namespace Umbraco.Cms.Core.Services.Implement // ensure the lazy Language callback is assigned EnsureDictionaryItemLanguageCallback(dictionaryItem); - scope.Events.Dispatch(SavedDictionaryItem, this, new SaveEventArgs(dictionaryItem, false)); + _eventAggregator.Publish(new DictionaryItemSavedNotification(dictionaryItem, eventMessages).WithStateFrom(savingNotification)); Audit(AuditType.Save, "Save DictionaryItem", userId, dictionaryItem.Id, "DictionaryItem"); scope.Complete(); @@ -261,16 +273,16 @@ namespace Umbraco.Cms.Core.Services.Implement { using (var scope = ScopeProvider.CreateScope()) { - var deleteEventArgs = new DeleteEventArgs(dictionaryItem); - if (scope.Events.DispatchCancelable(DeletingDictionaryItem, this, deleteEventArgs)) + EventMessages eventMessages = EventMessagesFactory.Get(); + var deletingNotification = new DictionaryItemDeletingNotification(dictionaryItem, eventMessages); + if (_eventAggregator.PublishCancelable(deletingNotification)) { scope.Complete(); return; } _dictionaryRepository.Delete(dictionaryItem); - deleteEventArgs.CanCancel = false; - scope.Events.Dispatch(DeletedDictionaryItem, this, deleteEventArgs); + _eventAggregator.Publish(new DictionaryItemDeletedNotification(dictionaryItem, eventMessages).WithStateFrom(deletingNotification)); Audit(AuditType.Delete, "Delete DictionaryItem", userId, dictionaryItem.Id, "DictionaryItem"); @@ -374,16 +386,16 @@ namespace Umbraco.Cms.Core.Services.Implement throw new InvalidOperationException($"Cannot save language {language.IsoCode} with fallback {languages[language.FallbackLanguageId.Value].IsoCode} as it would create a fallback cycle."); } - var saveEventArgs = new SaveEventArgs(language); - if (scope.Events.DispatchCancelable(SavingLanguage, this, saveEventArgs)) + EventMessages eventMessages = EventMessagesFactory.Get(); + var savingNotification = new LanguageSavingNotification(language, eventMessages); + if (_eventAggregator.PublishCancelable(savingNotification)) { scope.Complete(); return; } _languageRepository.Save(language); - saveEventArgs.CanCancel = false; - scope.Events.Dispatch(SavedLanguage, this, saveEventArgs); + _eventAggregator.Publish(new LanguageSavedNotification(language, eventMessages).WithStateFrom(savingNotification)); Audit(AuditType.Save, "Save Language", userId, language.Id, ObjectTypes.GetName(UmbracoObjectTypes.Language)); @@ -417,8 +429,9 @@ namespace Umbraco.Cms.Core.Services.Implement // write-lock languages to guard against race conds when dealing with default language scope.WriteLock(Cms.Core.Constants.Locks.Languages); - var deleteEventArgs = new DeleteEventArgs(language); - if (scope.Events.DispatchCancelable(DeletingLanguage, this, deleteEventArgs)) + EventMessages eventMessages = EventMessagesFactory.Get(); + var deletingLanguageNotification = new LanguageDeletingNotification(language, eventMessages); + if (_eventAggregator.PublishCancelable(deletingLanguageNotification)) { scope.Complete(); return; @@ -426,9 +439,8 @@ namespace Umbraco.Cms.Core.Services.Implement // NOTE: Other than the fall-back language, there aren't any other constraints in the db, so possible references aren't deleted _languageRepository.Delete(language); - deleteEventArgs.CanCancel = false; - scope.Events.Dispatch(DeletedLanguage, this, deleteEventArgs); + _eventAggregator.Publish(new LanguageDeletedNotification(language, eventMessages).WithStateFrom(deletingLanguageNotification)); Audit(AuditType.Delete, "Delete Language", userId, language.Id, ObjectTypes.GetName(UmbracoObjectTypes.Language)); scope.Complete(); From c8471b096cc288b5d8d42dbc489b2b520230f128 Mon Sep 17 00:00:00 2001 From: Mole Date: Thu, 25 Mar 2021 14:51:00 +0100 Subject: [PATCH 185/188] Switch to INotificationHandler --- .../Cache/DistributedCacheBinder_Handlers.cs | 50 +++++++------- .../Compose/NotificationsComposer.cs | 8 +++ .../Services/Implement/LocalizationService.cs | 67 +++---------------- .../Compose/NotificationsComposer.cs | 17 +++++ .../PublishedSnapshotServiceEventHandler.cs | 12 ++-- .../Scoping/ScopedRepositoryTests.cs | 22 ++++-- 6 files changed, 82 insertions(+), 94 deletions(-) create mode 100644 src/Umbraco.PublishedCache.NuCache/Compose/NotificationsComposer.cs diff --git a/src/Umbraco.Infrastructure/Cache/DistributedCacheBinder_Handlers.cs b/src/Umbraco.Infrastructure/Cache/DistributedCacheBinder_Handlers.cs index 1aa4906029..de75ac0905 100644 --- a/src/Umbraco.Infrastructure/Cache/DistributedCacheBinder_Handlers.cs +++ b/src/Umbraco.Infrastructure/Cache/DistributedCacheBinder_Handlers.cs @@ -11,6 +11,7 @@ using Umbraco.Cms.Core.Models.Membership; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Services.Changes; using Umbraco.Cms.Core.Services.Implement; +using Umbraco.Cms.Infrastructure.Services.Notifications; using Umbraco.Extensions; namespace Umbraco.Cms.Core.Cache @@ -18,7 +19,11 @@ namespace Umbraco.Cms.Core.Cache /// /// Default implementation. /// - public partial class DistributedCacheBinder + public partial class DistributedCacheBinder : + INotificationHandler, + INotificationHandler, + INotificationHandler, + INotificationHandler { private List _unbinders; @@ -61,12 +66,6 @@ namespace Umbraco.Cms.Core.Cache Bind(() => UserService.UserGroupPermissionsAssigned += UserService_UserGroupPermissionsAssigned, () => UserService.UserGroupPermissionsAssigned -= UserService_UserGroupPermissionsAssigned); - // bind to dictionary events - Bind(() => LocalizationService.DeletedDictionaryItem += LocalizationService_DeletedDictionaryItem, - () => LocalizationService.DeletedDictionaryItem -= LocalizationService_DeletedDictionaryItem); - Bind(() => LocalizationService.SavedDictionaryItem += LocalizationService_SavedDictionaryItem, - () => LocalizationService.SavedDictionaryItem -= LocalizationService_SavedDictionaryItem); - // bind to data type events Bind(() => DataTypeService.Deleted += DataTypeService_Deleted, () => DataTypeService.Deleted -= DataTypeService_Deleted); @@ -85,12 +84,6 @@ namespace Umbraco.Cms.Core.Cache Bind(() => DomainService.Deleted += DomainService_Deleted, () => DomainService.Deleted -= DomainService_Deleted); - // bind to language events - Bind(() => LocalizationService.SavedLanguage += LocalizationService_SavedLanguage, - () => LocalizationService.SavedLanguage -= LocalizationService_SavedLanguage); - Bind(() => LocalizationService.DeletedLanguage += LocalizationService_DeletedLanguage, - () => LocalizationService.DeletedLanguage -= LocalizationService_DeletedLanguage); - // bind to content type events Bind(() => ContentTypeService.Changed += ContentTypeService_Changed, () => ContentTypeService.Changed -= ContentTypeService_Changed); @@ -196,17 +189,20 @@ namespace Umbraco.Cms.Core.Cache #endregion #region LocalizationService / Dictionary - - private void LocalizationService_SavedDictionaryItem(ILocalizationService sender, SaveEventArgs e) + public void Handle(DictionaryItemSavedNotification notification) { - foreach (var entity in e.SavedEntities) + foreach (IDictionaryItem entity in notification.SavedEntities) + { _distributedCache.RefreshDictionaryCache(entity.Id); + } } - private void LocalizationService_DeletedDictionaryItem(ILocalizationService sender, DeleteEventArgs e) + public void Handle(DictionaryItemDeletedNotification notification) { - foreach (var entity in e.DeletedEntities) + foreach (IDictionaryItem entity in notification.DeletedEntities) + { _distributedCache.RemoveDictionaryCache(entity.Id); + } } #endregion @@ -248,23 +244,25 @@ namespace Umbraco.Cms.Core.Cache /// /// Fires when a language is deleted /// - /// - /// - private void LocalizationService_DeletedLanguage(ILocalizationService sender, DeleteEventArgs e) + /// + public void Handle(LanguageDeletedNotification notification) { - foreach (var entity in e.DeletedEntities) + foreach (ILanguage entity in notification.DeletedEntities) + { _distributedCache.RemoveLanguageCache(entity); + } } /// /// Fires when a language is saved /// - /// - /// - private void LocalizationService_SavedLanguage(ILocalizationService sender, SaveEventArgs e) + /// + public void Handle(LanguageSavedNotification notification) { - foreach (var entity in e.SavedEntities) + foreach (ILanguage entity in notification.SavedEntities) + { _distributedCache.RefreshLanguageCache(entity); + } } #endregion diff --git a/src/Umbraco.Infrastructure/Compose/NotificationsComposer.cs b/src/Umbraco.Infrastructure/Compose/NotificationsComposer.cs index c760c33b71..2e3403e3dd 100644 --- a/src/Umbraco.Infrastructure/Compose/NotificationsComposer.cs +++ b/src/Umbraco.Infrastructure/Compose/NotificationsComposer.cs @@ -1,6 +1,7 @@ // Copyright (c) Umbraco. // See LICENSE for more details. +using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Composing; using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.Events; @@ -62,6 +63,13 @@ namespace Umbraco.Cms.Core.Compose .AddNotificationHandler() .AddNotificationHandler() .AddNotificationHandler(); + + // Add notification handlers for DistributedCache + builder + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler(); } } } diff --git a/src/Umbraco.Infrastructure/Services/Implement/LocalizationService.cs b/src/Umbraco.Infrastructure/Services/Implement/LocalizationService.cs index f4072ccfb7..b8c36f1ed9 100644 --- a/src/Umbraco.Infrastructure/Services/Implement/LocalizationService.cs +++ b/src/Umbraco.Infrastructure/Services/Implement/LocalizationService.cs @@ -18,7 +18,6 @@ namespace Umbraco.Cms.Core.Services.Implement { private readonly IDictionaryRepository _dictionaryRepository; private readonly ILanguageRepository _languageRepository; - private readonly IEventAggregator _eventAggregator; private readonly IAuditRepository _auditRepository; public LocalizationService( @@ -27,14 +26,12 @@ namespace Umbraco.Cms.Core.Services.Implement IEventMessagesFactory eventMessagesFactory, IDictionaryRepository dictionaryRepository, IAuditRepository auditRepository, - ILanguageRepository languageRepository, - IEventAggregator eventAggregator) + ILanguageRepository languageRepository) : base(provider, loggerFactory, eventMessagesFactory) { _dictionaryRepository = dictionaryRepository; _auditRepository = auditRepository; _languageRepository = languageRepository; - _eventAggregator = eventAggregator; } /// @@ -101,7 +98,7 @@ namespace Umbraco.Cms.Core.Services.Implement EventMessages eventMessages = EventMessagesFactory.Get(); var savingNotification = new DictionaryItemSavingNotification(item, eventMessages); - if (_eventAggregator.PublishCancelable(savingNotification)) + if (scope.Notifications.PublishCancelable(savingNotification)) { scope.Complete(); return item; @@ -111,7 +108,7 @@ namespace Umbraco.Cms.Core.Services.Implement // ensure the lazy Language callback is assigned EnsureDictionaryItemLanguageCallback(item); - _eventAggregator.Publish(new DictionaryItemSavedNotification(item, eventMessages).WithStateFrom(savingNotification)); + scope.Notifications.Publish(new DictionaryItemSavedNotification(item, eventMessages).WithStateFrom(savingNotification)); scope.Complete(); @@ -244,7 +241,7 @@ namespace Umbraco.Cms.Core.Services.Implement { EventMessages eventMessages = EventMessagesFactory.Get(); var savingNotification = new DictionaryItemSavingNotification(dictionaryItem, eventMessages); - if (_eventAggregator.PublishCancelable(savingNotification)) + if (scope.Notifications.PublishCancelable(savingNotification)) { scope.Complete(); return; @@ -256,7 +253,7 @@ namespace Umbraco.Cms.Core.Services.Implement // ensure the lazy Language callback is assigned EnsureDictionaryItemLanguageCallback(dictionaryItem); - _eventAggregator.Publish(new DictionaryItemSavedNotification(dictionaryItem, eventMessages).WithStateFrom(savingNotification)); + scope.Notifications.Publish(new DictionaryItemSavedNotification(dictionaryItem, eventMessages).WithStateFrom(savingNotification)); Audit(AuditType.Save, "Save DictionaryItem", userId, dictionaryItem.Id, "DictionaryItem"); scope.Complete(); @@ -275,14 +272,14 @@ namespace Umbraco.Cms.Core.Services.Implement { EventMessages eventMessages = EventMessagesFactory.Get(); var deletingNotification = new DictionaryItemDeletingNotification(dictionaryItem, eventMessages); - if (_eventAggregator.PublishCancelable(deletingNotification)) + if (scope.Notifications.PublishCancelable(deletingNotification)) { scope.Complete(); return; } _dictionaryRepository.Delete(dictionaryItem); - _eventAggregator.Publish(new DictionaryItemDeletedNotification(dictionaryItem, eventMessages).WithStateFrom(deletingNotification)); + scope.Notifications.Publish(new DictionaryItemDeletedNotification(dictionaryItem, eventMessages).WithStateFrom(deletingNotification)); Audit(AuditType.Delete, "Delete DictionaryItem", userId, dictionaryItem.Id, "DictionaryItem"); @@ -388,14 +385,14 @@ namespace Umbraco.Cms.Core.Services.Implement EventMessages eventMessages = EventMessagesFactory.Get(); var savingNotification = new LanguageSavingNotification(language, eventMessages); - if (_eventAggregator.PublishCancelable(savingNotification)) + if (scope.Notifications.PublishCancelable(savingNotification)) { scope.Complete(); return; } _languageRepository.Save(language); - _eventAggregator.Publish(new LanguageSavedNotification(language, eventMessages).WithStateFrom(savingNotification)); + scope.Notifications.Publish(new LanguageSavedNotification(language, eventMessages).WithStateFrom(savingNotification)); Audit(AuditType.Save, "Save Language", userId, language.Id, ObjectTypes.GetName(UmbracoObjectTypes.Language)); @@ -431,7 +428,7 @@ namespace Umbraco.Cms.Core.Services.Implement EventMessages eventMessages = EventMessagesFactory.Get(); var deletingLanguageNotification = new LanguageDeletingNotification(language, eventMessages); - if (_eventAggregator.PublishCancelable(deletingLanguageNotification)) + if (scope.Notifications.PublishCancelable(deletingLanguageNotification)) { scope.Complete(); return; @@ -440,7 +437,7 @@ namespace Umbraco.Cms.Core.Services.Implement // NOTE: Other than the fall-back language, there aren't any other constraints in the db, so possible references aren't deleted _languageRepository.Delete(language); - _eventAggregator.Publish(new LanguageDeletedNotification(language, eventMessages).WithStateFrom(deletingLanguageNotification)); + scope.Notifications.Publish(new LanguageDeletedNotification(language, eventMessages).WithStateFrom(deletingLanguageNotification)); Audit(AuditType.Delete, "Delete Language", userId, language.Id, ObjectTypes.GetName(UmbracoObjectTypes.Language)); scope.Complete(); @@ -475,47 +472,5 @@ namespace Umbraco.Cms.Core.Services.Implement return _dictionaryRepository.GetDictionaryItemKeyMap(); } } - - #region Events - /// - /// Occurs before Delete - /// - public static event TypedEventHandler> DeletingLanguage; - - /// - /// Occurs after Delete - /// - public static event TypedEventHandler> DeletedLanguage; - - /// - /// Occurs before Delete - /// - public static event TypedEventHandler> DeletingDictionaryItem; - - /// - /// Occurs after Delete - /// - public static event TypedEventHandler> DeletedDictionaryItem; - - /// - /// Occurs before Save - /// - public static event TypedEventHandler> SavingDictionaryItem; - - /// - /// Occurs after Save - /// - public static event TypedEventHandler> SavedDictionaryItem; - - /// - /// Occurs before Save - /// - public static event TypedEventHandler> SavingLanguage; - - /// - /// Occurs after Save - /// - public static event TypedEventHandler> SavedLanguage; - #endregion } } diff --git a/src/Umbraco.PublishedCache.NuCache/Compose/NotificationsComposer.cs b/src/Umbraco.PublishedCache.NuCache/Compose/NotificationsComposer.cs new file mode 100644 index 0000000000..df84759793 --- /dev/null +++ b/src/Umbraco.PublishedCache.NuCache/Compose/NotificationsComposer.cs @@ -0,0 +1,17 @@ +using Umbraco.Cms.Core.Compose; +using Umbraco.Cms.Core.Composing; +using Umbraco.Cms.Core.DependencyInjection; +using Umbraco.Cms.Infrastructure.Services.Notifications; + +namespace Umbraco.Cms.Infrastructure.PublishedCache.Compose +{ + public sealed class NotificationsComposer : ComponentComposer, ICoreComposer + { + public override void Compose(IUmbracoBuilder builder) + { + base.Compose(builder); + + builder.AddNotificationHandler(); + } + } +} diff --git a/src/Umbraco.PublishedCache.NuCache/PublishedSnapshotServiceEventHandler.cs b/src/Umbraco.PublishedCache.NuCache/PublishedSnapshotServiceEventHandler.cs index 25ceb9fb6a..df02320d87 100644 --- a/src/Umbraco.PublishedCache.NuCache/PublishedSnapshotServiceEventHandler.cs +++ b/src/Umbraco.PublishedCache.NuCache/PublishedSnapshotServiceEventHandler.cs @@ -9,6 +9,7 @@ using Umbraco.Cms.Core.Services.Changes; using Umbraco.Cms.Core.Services.Implement; using Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; using Umbraco.Cms.Infrastructure.PublishedCache.Persistence; +using Umbraco.Cms.Infrastructure.Services.Notifications; using Umbraco.Extensions; namespace Umbraco.Cms.Infrastructure.PublishedCache @@ -16,7 +17,7 @@ namespace Umbraco.Cms.Infrastructure.PublishedCache /// /// Subscribes to Umbraco events to ensure nucache remains consistent with the source data /// - public class PublishedSnapshotServiceEventHandler : IDisposable + public class PublishedSnapshotServiceEventHandler : IDisposable, INotificationHandler { private readonly IRuntimeState _runtime; private bool _disposedValue; @@ -79,9 +80,6 @@ namespace Umbraco.Cms.Infrastructure.PublishedCache ContentTypeService.ScopedRefreshedEntity += OnContentTypeRefreshedEntity; MediaTypeService.ScopedRefreshedEntity += OnMediaTypeRefreshedEntity; MemberTypeService.ScopedRefreshedEntity += OnMemberTypeRefreshedEntity; - - // TODO: This should be a cache refresher call! - LocalizationService.SavedLanguage += OnLanguageSaved; } private void TearDownRepositoryEvents() @@ -95,7 +93,6 @@ namespace Umbraco.Cms.Infrastructure.PublishedCache ContentTypeService.ScopedRefreshedEntity -= OnContentTypeRefreshedEntity; MediaTypeService.ScopedRefreshedEntity -= OnMediaTypeRefreshedEntity; MemberTypeService.ScopedRefreshedEntity -= OnMemberTypeRefreshedEntity; - LocalizationService.SavedLanguage -= OnLanguageSaved; // TODO: Shouldn't this be a cache refresher event? } // note: if the service is not ready, ie _isReady is false, then we still handle repository events, @@ -156,13 +153,14 @@ namespace Umbraco.Cms.Infrastructure.PublishedCache } } + // TODO: This should be a cache refresher call! /// /// If a is ever saved with a different culture, we need to rebuild all of the content nucache database table /// - private void OnLanguageSaved(ILocalizationService sender, SaveEventArgs e) + public void Handle(LanguageSavedNotification notification) { // culture changed on an existing language - var cultureChanged = e.SavedEntities.Any(x => !x.WasPropertyDirty(nameof(ILanguage.Id)) && x.WasPropertyDirty(nameof(ILanguage.IsoCode))); + var cultureChanged = notification.SavedEntities.Any(x => !x.WasPropertyDirty(nameof(ILanguage.Id)) && x.WasPropertyDirty(nameof(ILanguage.IsoCode))); if (cultureChanged) { // Rebuild all content for all content types diff --git a/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Scoping/ScopedRepositoryTests.cs b/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Scoping/ScopedRepositoryTests.cs index 96ba30905b..fcfd81a39d 100644 --- a/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Scoping/ScopedRepositoryTests.cs +++ b/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Scoping/ScopedRepositoryTests.cs @@ -6,6 +6,7 @@ using System.Linq; using Microsoft.Extensions.Logging; using NUnit.Framework; using Umbraco.Cms.Core.Cache; +using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.Membership; @@ -14,9 +15,12 @@ using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Services.Implement; using Umbraco.Cms.Core.Sync; using Umbraco.Cms.Core.Web; +using Umbraco.Cms.Infrastructure.PublishedCache; +using Umbraco.Cms.Infrastructure.Services.Notifications; using Umbraco.Cms.Infrastructure.Sync; using Umbraco.Cms.Tests.Common.Testing; using Umbraco.Cms.Tests.Integration.Testing; +using Umbraco.Extensions; namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Scoping { @@ -44,6 +48,17 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Scoping return result; } + protected override void CustomTestSetup(IUmbracoBuilder builder) + { + builder.Services.AddUnique(); + builder + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler(); + builder.AddNotificationHandler(); + } + [TearDown] public void Teardown() { @@ -154,9 +169,6 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Scoping Assert.AreEqual(lang.Id, globalCached.Id); Assert.AreEqual("fr-FR", globalCached.IsoCode); - _distributedCacheBinder = new DistributedCacheBinder(new DistributedCache(ServerMessenger, CacheRefresherCollection), GetRequiredService(), GetRequiredService>()); - _distributedCacheBinder.BindEvents(true); - Assert.IsNull(scopeProvider.AmbientScope); using (IScope scope = scopeProvider.CreateScope(repositoryCacheMode: RepositoryCacheMode.Scoped)) { @@ -250,8 +262,8 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Scoping Assert.AreEqual(item.Id, globalCached.Id); Assert.AreEqual("item-key", globalCached.ItemKey); - _distributedCacheBinder = new DistributedCacheBinder(new DistributedCache(ServerMessenger, CacheRefresherCollection), GetRequiredService(), GetRequiredService>()); - _distributedCacheBinder.BindEvents(true); + // _distributedCacheBinder = new DistributedCacheBinder(new DistributedCache(ServerMessenger, CacheRefresherCollection), GetRequiredService(), GetRequiredService>()); + // _distributedCacheBinder.BindEvents(true); Assert.IsNull(scopeProvider.AmbientScope); using (IScope scope = scopeProvider.CreateScope(repositoryCacheMode: RepositoryCacheMode.Scoped)) From 6a81b31d847fa5627496083ddce8bd80f1c388ab Mon Sep 17 00:00:00 2001 From: Mole Date: Thu, 25 Mar 2021 15:54:43 +0100 Subject: [PATCH 186/188] Tentative fix for ScopedRepositoryTests Caches are getting cleared because we can't unbind from events :( --- .../Cache/DistributedCacheBinderTests.cs | 6 ------ .../Scoping/ScopedRepositoryTests.cs | 7 ++++--- 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/src/Umbraco.Tests.Integration/Cache/DistributedCacheBinderTests.cs b/src/Umbraco.Tests.Integration/Cache/DistributedCacheBinderTests.cs index 9b8a1e9c98..6a263cb6ae 100644 --- a/src/Umbraco.Tests.Integration/Cache/DistributedCacheBinderTests.cs +++ b/src/Umbraco.Tests.Integration/Cache/DistributedCacheBinderTests.cs @@ -47,9 +47,6 @@ namespace Umbraco.Cms.Tests.Integration.Cache new EventDefinition>(null, UserService, new SaveEventArgs(Enumerable.Empty())), new EventDefinition>(null, UserService, new DeleteEventArgs(Enumerable.Empty())), - new EventDefinition>(null, LocalizationService, new SaveEventArgs(Enumerable.Empty())), - new EventDefinition>(null, LocalizationService, new DeleteEventArgs(Enumerable.Empty())), - new EventDefinition>(null, DataTypeService, new SaveEventArgs(Enumerable.Empty())), new EventDefinition>(null, DataTypeService, new DeleteEventArgs(Enumerable.Empty())), @@ -59,9 +56,6 @@ namespace Umbraco.Cms.Tests.Integration.Cache new EventDefinition>(null, DomainService, new SaveEventArgs(Enumerable.Empty())), new EventDefinition>(null, DomainService, new DeleteEventArgs(Enumerable.Empty())), - new EventDefinition>(null, LocalizationService, new SaveEventArgs(Enumerable.Empty())), - new EventDefinition>(null, LocalizationService, new DeleteEventArgs(Enumerable.Empty())), - new EventDefinition>(null, ContentTypeService, new SaveEventArgs(Enumerable.Empty())), new EventDefinition>(null, ContentTypeService, new DeleteEventArgs(Enumerable.Empty())), new EventDefinition>(null, MediaTypeService, new SaveEventArgs(Enumerable.Empty())), diff --git a/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Scoping/ScopedRepositoryTests.cs b/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Scoping/ScopedRepositoryTests.cs index fcfd81a39d..61334940e1 100644 --- a/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Scoping/ScopedRepositoryTests.cs +++ b/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Scoping/ScopedRepositoryTests.cs @@ -256,15 +256,16 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Scoping }; service.Save(item); + // Refresh the cache manually because we can't unbind + service.GetDictionaryItemById(item.Id); + service.GetLanguageById(lang.Id); + // global cache contains the entity var globalCached = (IDictionaryItem)globalCache.Get(GetCacheIdKey(item.Id), () => null); Assert.IsNotNull(globalCached); Assert.AreEqual(item.Id, globalCached.Id); Assert.AreEqual("item-key", globalCached.ItemKey); - // _distributedCacheBinder = new DistributedCacheBinder(new DistributedCache(ServerMessenger, CacheRefresherCollection), GetRequiredService(), GetRequiredService>()); - // _distributedCacheBinder.BindEvents(true); - Assert.IsNull(scopeProvider.AmbientScope); using (IScope scope = scopeProvider.CreateScope(repositoryCacheMode: RepositoryCacheMode.Scoped)) { From fbcaa6adea8a2f006a5b638ae55507485732c459 Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Thu, 25 Mar 2021 16:17:46 +0100 Subject: [PATCH 187/188] https://github.com/umbraco/Umbraco-CMS/issues/10060 - Fixed issue where appsettings section for modelsbuilder was not used. --- .../DependencyInjection/UmbracoBuilder.Configuration.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Configuration.cs b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Configuration.cs index a9e112efc4..bd834a5427 100644 --- a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Configuration.cs +++ b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Configuration.cs @@ -30,7 +30,8 @@ namespace Umbraco.Cms.Core.DependencyInjection builder.Services.AddSingleton, UnattendedSettingsValidator>(); // Register configuration sections. - builder.Services.Configure(builder.Config.GetSection(Constants.Configuration.ConfigModelsBuilder), o => o.BindNonPublicProperties = true); + + builder.Services.Configure(builder.Config.GetSection(Constants.Configuration.ConfigModelsBuilder), o => o.BindNonPublicProperties = true); builder.Services.Configure(builder.Config.GetSection("ConnectionStrings"), o => o.BindNonPublicProperties = true); AddOptions(builder, Constants.Configuration.ConfigActiveDirectory); From 32419835e2fb42824c44ee054eb14eb19d977e5d Mon Sep 17 00:00:00 2001 From: Mole Date: Fri, 26 Mar 2021 14:28:19 +0100 Subject: [PATCH 188/188] Move DeletingNotification and make LocalizationService internal --- .../Events}/DeletingNotification.cs | 3 +-- .../Services/Implement/LocalizationService.cs | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) rename src/{Umbraco.Infrastructure/Services/Notifications => Umbraco.Core/Events}/DeletingNotification.cs (84%) diff --git a/src/Umbraco.Infrastructure/Services/Notifications/DeletingNotification.cs b/src/Umbraco.Core/Events/DeletingNotification.cs similarity index 84% rename from src/Umbraco.Infrastructure/Services/Notifications/DeletingNotification.cs rename to src/Umbraco.Core/Events/DeletingNotification.cs index 2dd8e09c6b..46fc9b9a03 100644 --- a/src/Umbraco.Infrastructure/Services/Notifications/DeletingNotification.cs +++ b/src/Umbraco.Core/Events/DeletingNotification.cs @@ -2,9 +2,8 @@ // See LICENSE for more details. using System.Collections.Generic; -using Umbraco.Cms.Core.Events; -namespace Umbraco.Cms.Infrastructure.Services.Notifications +namespace Umbraco.Cms.Core.Events { public abstract class DeletingNotification : CancelableEnumerableObjectNotification { diff --git a/src/Umbraco.Infrastructure/Services/Implement/LocalizationService.cs b/src/Umbraco.Infrastructure/Services/Implement/LocalizationService.cs index b8c36f1ed9..a15d782db5 100644 --- a/src/Umbraco.Infrastructure/Services/Implement/LocalizationService.cs +++ b/src/Umbraco.Infrastructure/Services/Implement/LocalizationService.cs @@ -14,7 +14,7 @@ namespace Umbraco.Cms.Core.Services.Implement /// /// Represents the Localization Service, which is an easy access to operations involving and /// - public class LocalizationService : RepositoryService, ILocalizationService + internal class LocalizationService : RepositoryService, ILocalizationService { private readonly IDictionaryRepository _dictionaryRepository; private readonly ILanguageRepository _languageRepository;