diff --git a/src/Umbraco.Core/CacheRefreshersResolver.cs b/src/Umbraco.Core/CacheRefreshersResolver.cs index e01dfba01a..e8df98fc1b 100644 --- a/src/Umbraco.Core/CacheRefreshersResolver.cs +++ b/src/Umbraco.Core/CacheRefreshersResolver.cs @@ -27,7 +27,7 @@ namespace Umbraco.Core /// /// Gets the implementations. /// - public IEnumerable CacheResolvers + public IEnumerable CacheRefreshers { get { diff --git a/src/Umbraco.Core/Models/ContentExtensions.cs b/src/Umbraco.Core/Models/ContentExtensions.cs index 3715b1468f..73b5ed34db 100644 --- a/src/Umbraco.Core/Models/ContentExtensions.cs +++ b/src/Umbraco.Core/Models/ContentExtensions.cs @@ -26,6 +26,20 @@ namespace Umbraco.Core.Models { #region IContent + /// + /// Returns true if this entity was just published as part of a recent save operation (i.e. it wasn't previously published) + /// + /// + /// + /// + /// This is helpful for determining if the published event will execute during the saved event for a content item. + /// + internal static bool JustPublished(this IContent entity) + { + var dirty = (IRememberBeingDirty)entity; + return dirty.WasPropertyDirty("Published") && entity.Published; + } + /// /// Determines if a new version should be created /// diff --git a/src/Umbraco.Core/Models/EntityExtensions.cs b/src/Umbraco.Core/Models/EntityExtensions.cs index f461c4007c..6daf99a58d 100644 --- a/src/Umbraco.Core/Models/EntityExtensions.cs +++ b/src/Umbraco.Core/Models/EntityExtensions.cs @@ -23,5 +23,5 @@ namespace Umbraco.Core.Models var dirty = (IRememberBeingDirty)entity; return dirty.WasPropertyDirty("Id"); } - } + } } diff --git a/src/Umbraco.Core/Models/UmbracoEntity.cs b/src/Umbraco.Core/Models/UmbracoEntity.cs index 4be916ce7d..24261b4989 100644 --- a/src/Umbraco.Core/Models/UmbracoEntity.cs +++ b/src/Umbraco.Core/Models/UmbracoEntity.cs @@ -54,7 +54,8 @@ namespace Umbraco.Core.Models Trashed = trashed; } - public UmbracoEntity(int trashed) + // for MySql + public UmbracoEntity(UInt64 trashed) { AdditionalData = new Dictionary(); Trashed = trashed == 1; diff --git a/src/Umbraco.Core/ObjectExtensions.cs b/src/Umbraco.Core/ObjectExtensions.cs index 026a9fcef5..90d173b49a 100644 --- a/src/Umbraco.Core/ObjectExtensions.cs +++ b/src/Umbraco.Core/ObjectExtensions.cs @@ -246,12 +246,14 @@ namespace Umbraco.Core else if (destinationType == typeof(Double)) { Double value; - return Double.TryParse(input, out value) ? Attempt.Succeed(value) : Attempt.Fail(); + var input2 = NormalizeNumberDecimalSeparator(input); + return Double.TryParse(input2, out value) ? Attempt.Succeed(value) : Attempt.Fail(); } else if (destinationType == typeof(Single)) { Single value; - return Single.TryParse(input, out value) ? Attempt.Succeed(value) : Attempt.Fail(); + var input2 = NormalizeNumberDecimalSeparator(input); + return Single.TryParse(input2, out value) ? Attempt.Succeed(value) : Attempt.Fail(); } else if (destinationType == typeof(Char)) { @@ -320,7 +322,8 @@ namespace Umbraco.Core else if (destinationType == typeof(Decimal)) { Decimal value; - return Decimal.TryParse(input, out value) ? Attempt.Succeed(value) : Attempt.Fail(); + var input2 = NormalizeNumberDecimalSeparator(input); + return Decimal.TryParse(input2, out value) ? Attempt.Succeed(value) : Attempt.Fail(); } else if (destinationType == typeof(Version)) { @@ -332,6 +335,14 @@ namespace Umbraco.Core return null; // we can't decide... } + private readonly static char[] NumberDecimalSeparatorsToNormalize = new[] {'.', ','}; + + private static string NormalizeNumberDecimalSeparator(string s) + { + var normalized = System.Threading.Thread.CurrentThread.CurrentUICulture.NumberFormat.NumberDecimalSeparator[0]; + return s.ReplaceMany(NumberDecimalSeparatorsToNormalize, normalized); + } + internal static void CheckThrowObjectDisposed(this IDisposable disposable, bool isDisposed, string objectname) { //TODO: Localise this exception diff --git a/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs b/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs index b626177ce3..7e9d7c20a4 100644 --- a/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs @@ -142,6 +142,7 @@ namespace Umbraco.Core.Persistence.Repositories { var list = new List { + "DELETE FROM cmsTask WHERE nodeId = @Id", "DELETE FROM umbracoUser2NodeNotify WHERE nodeId = @Id", "DELETE FROM umbracoUser2NodePermission WHERE nodeId = @Id", "DELETE FROM umbracoRelation WHERE parentId = @Id", diff --git a/src/Umbraco.Core/Persistence/Repositories/MediaRepository.cs b/src/Umbraco.Core/Persistence/Repositories/MediaRepository.cs index 13cd277cc8..4233f5d515 100644 --- a/src/Umbraco.Core/Persistence/Repositories/MediaRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/MediaRepository.cs @@ -131,6 +131,7 @@ namespace Umbraco.Core.Persistence.Repositories { var list = new List { + "DELETE FROM cmsTask WHERE nodeId = @Id", "DELETE FROM umbracoUser2NodeNotify WHERE nodeId = @Id", "DELETE FROM umbracoUser2NodePermission WHERE nodeId = @Id", "DELETE FROM umbracoRelation WHERE parentId = @Id", diff --git a/src/Umbraco.Core/Persistence/Repositories/MemberRepository.cs b/src/Umbraco.Core/Persistence/Repositories/MemberRepository.cs index b2c4f24995..4108ac6167 100644 --- a/src/Umbraco.Core/Persistence/Repositories/MemberRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/MemberRepository.cs @@ -172,6 +172,7 @@ namespace Umbraco.Core.Persistence.Repositories { var list = new List { + "DELETE FROM cmsTask WHERE nodeId = @Id", "DELETE FROM umbracoUser2NodeNotify WHERE nodeId = @Id", "DELETE FROM umbracoUser2NodePermission WHERE nodeId = @Id", "DELETE FROM umbracoRelation WHERE parentId = @Id", diff --git a/src/Umbraco.Core/Persistence/Repositories/UserRepository.cs b/src/Umbraco.Core/Persistence/Repositories/UserRepository.cs index e797057b60..0c3e64162f 100644 --- a/src/Umbraco.Core/Persistence/Repositories/UserRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/UserRepository.cs @@ -131,6 +131,8 @@ namespace Umbraco.Core.Persistence.Repositories { var list = new List { + "DELETE FROM cmsTask WHERE userId = @Id", + "DELETE FROM cmsTask WHERE parentUserId = @Id", "DELETE FROM umbracoUser2NodePermission WHERE userId = @Id", "DELETE FROM umbracoUser2NodeNotify WHERE userId = @Id", "DELETE FROM umbracoUserLogins WHERE userId = @Id", diff --git a/src/Umbraco.Core/Services/ContentService.cs b/src/Umbraco.Core/Services/ContentService.cs index 4979d1bcb0..d0cbc086d6 100644 --- a/src/Umbraco.Core/Services/ContentService.cs +++ b/src/Umbraco.Core/Services/ContentService.cs @@ -176,12 +176,20 @@ namespace Umbraco.Core.Services var contentType = FindContentTypeByAlias(contentTypeAlias); var content = new Content(name, parentId, contentType); + //NOTE: I really hate the notion of these Creating/Created events - they are so inconsistent, I've only just found + // out that in these 'WithIdentity' methods, the Saving/Saved events were not fired, wtf. Anyways, they're added now. if (Creating.IsRaisedEventCancelled(new NewEventArgs(content, contentTypeAlias, parentId), this)) { content.WasCancelled = true; return content; } + if (Saving.IsRaisedEventCancelled(new SaveEventArgs(content), this)) + { + content.WasCancelled = true; + return content; + } + var uow = _uowProvider.GetUnitOfWork(); using (var repository = _repositoryFactory.CreateContentRepository(uow)) { @@ -191,6 +199,8 @@ namespace Umbraco.Core.Services uow.Commit(); } + Saved.RaiseEvent(new SaveEventArgs(content, false), this); + Created.RaiseEvent(new NewEventArgs(content, false, contentTypeAlias, parentId), this); Audit.Add(AuditTypes.New, string.Format("Content '{0}' was created with Id {1}", name, content.Id), content.CreatorId, content.Id); @@ -216,12 +226,20 @@ namespace Umbraco.Core.Services var contentType = FindContentTypeByAlias(contentTypeAlias); var content = new Content(name, parent, contentType); + //NOTE: I really hate the notion of these Creating/Created events - they are so inconsistent, I've only just found + // out that in these 'WithIdentity' methods, the Saving/Saved events were not fired, wtf. Anyways, they're added now. if (Creating.IsRaisedEventCancelled(new NewEventArgs(content, contentTypeAlias, parent), this)) { content.WasCancelled = true; return content; } + if (Saving.IsRaisedEventCancelled(new SaveEventArgs(content), this)) + { + content.WasCancelled = true; + return content; + } + var uow = _uowProvider.GetUnitOfWork(); using (var repository = _repositoryFactory.CreateContentRepository(uow)) { @@ -231,6 +249,8 @@ namespace Umbraco.Core.Services uow.Commit(); } + Saved.RaiseEvent(new SaveEventArgs(content, false), this); + Created.RaiseEvent(new NewEventArgs(content, false, contentTypeAlias, parent), this); Audit.Add(AuditTypes.New, string.Format("Content '{0}' was created with Id {1}", name, content.Id), content.CreatorId, content.Id); diff --git a/src/Umbraco.Core/Services/IMembershipMemberService.cs b/src/Umbraco.Core/Services/IMembershipMemberService.cs index a4522a95ff..c2183bb254 100644 --- a/src/Umbraco.Core/Services/IMembershipMemberService.cs +++ b/src/Umbraco.Core/Services/IMembershipMemberService.cs @@ -15,7 +15,7 @@ namespace Umbraco.Core.Services public interface IMembershipMemberService : IMembershipMemberService, IMembershipRoleService { IMember CreateMember(string username, string email, string password, string memberType); - IMember CreateMemberWithIdentity(string username, string email, string password, IMemberType memberType, bool raiseEvents = true); + IMember CreateMemberWithIdentity(string username, string email, string password, IMemberType memberType); } /// @@ -47,9 +47,8 @@ namespace Umbraco.Core.Services /// /// /// - /// /// - T CreateWithIdentity(string username, string email, string password, string memberTypeAlias, bool raiseEvents = true); + T CreateWithIdentity(string username, string email, string password, string memberTypeAlias); /// /// Gets the member by the provider key diff --git a/src/Umbraco.Core/Services/IMembershipUserService.cs b/src/Umbraco.Core/Services/IMembershipUserService.cs index a04d9e23a6..873726dc2f 100644 --- a/src/Umbraco.Core/Services/IMembershipUserService.cs +++ b/src/Umbraco.Core/Services/IMembershipUserService.cs @@ -11,7 +11,7 @@ namespace Umbraco.Core.Services public interface IMembershipUserService : IMembershipMemberService { - IUser CreateMemberWithIdentity(string username, string email, string password, IUserType userType, bool raiseEvents = true); + IUser CreateUserWithIdentity(string username, string email, string password, IUserType userType); } } \ No newline at end of file diff --git a/src/Umbraco.Core/Services/MediaService.cs b/src/Umbraco.Core/Services/MediaService.cs index 23f3a96679..8c0de20a89 100644 --- a/src/Umbraco.Core/Services/MediaService.cs +++ b/src/Umbraco.Core/Services/MediaService.cs @@ -122,12 +122,23 @@ namespace Umbraco.Core.Services { var mediaType = FindMediaTypeByAlias(mediaTypeAlias); var media = new Models.Media(name, parentId, mediaType); + + //NOTE: I really hate the notion of these Creating/Created events - they are so inconsistent, I've only just found + // out that in these 'WithIdentity' methods, the Saving/Saved events were not fired, wtf. Anyways, they're added now. if (Creating.IsRaisedEventCancelled(new NewEventArgs(media, mediaTypeAlias, parentId), this)) { media.WasCancelled = true; return media; } + if (Saving.IsRaisedEventCancelled(new SaveEventArgs(media), this)) + { + media.WasCancelled = true; + return media; + } + + //TODO: Once we fix up the transaction logic, these write locks should be replaced with + // an outter transaction instead. using (new WriteLock(Locker)) { var uow = _uowProvider.GetUnitOfWork(); @@ -142,6 +153,8 @@ namespace Umbraco.Core.Services } } + Saved.RaiseEvent(new SaveEventArgs(media, false), this); + Created.RaiseEvent(new NewEventArgs(media, false, mediaTypeAlias, parentId), this); Audit.Add(AuditTypes.New, string.Format("Media '{0}' was created with Id {1}", name, media.Id), media.CreatorId, media.Id); @@ -166,12 +179,21 @@ namespace Umbraco.Core.Services { var mediaType = FindMediaTypeByAlias(mediaTypeAlias); var media = new Models.Media(name, parent, mediaType); + + //NOTE: I really hate the notion of these Creating/Created events - they are so inconsistent, I've only just found + // out that in these 'WithIdentity' methods, the Saving/Saved events were not fired, wtf. Anyways, they're added now. if (Creating.IsRaisedEventCancelled(new NewEventArgs(media, mediaTypeAlias, parent), this)) { media.WasCancelled = true; return media; } + if (Saving.IsRaisedEventCancelled(new SaveEventArgs(media), this)) + { + media.WasCancelled = true; + return media; + } + using (new WriteLock(Locker)) { var uow = _uowProvider.GetUnitOfWork(); @@ -186,6 +208,8 @@ namespace Umbraco.Core.Services } } + Saved.RaiseEvent(new SaveEventArgs(media, false), this); + Created.RaiseEvent(new NewEventArgs(media, false, mediaTypeAlias, parent), this); Audit.Add(AuditTypes.New, string.Format("Media '{0}' was created with Id {1}", name, media.Id), media.CreatorId, media.Id); diff --git a/src/Umbraco.Core/Services/MemberService.cs b/src/Umbraco.Core/Services/MemberService.cs index 0acb855f52..423f2ced83 100644 --- a/src/Umbraco.Core/Services/MemberService.cs +++ b/src/Umbraco.Core/Services/MemberService.cs @@ -589,19 +589,16 @@ namespace Umbraco.Core.Services return member; } - public IMember CreateMemberWithIdentity(string username, string email, string password, IMemberType memberType, bool raiseEvents = true) + public IMember CreateMemberWithIdentity(string username, string email, string password, IMemberType memberType) { if (memberType == null) throw new ArgumentNullException("memberType"); var member = new Member(username, email.ToLower().Trim(), username, password, -1, memberType); - if (raiseEvents) + if (Saving.IsRaisedEventCancelled(new SaveEventArgs(member), this)) { - if (Saving.IsRaisedEventCancelled(new SaveEventArgs(member), this)) - { - member.WasCancelled = true; - return member; - } + member.WasCancelled = true; + return member; } var uow = _uowProvider.GetUnitOfWork(); @@ -615,12 +612,12 @@ namespace Umbraco.Core.Services CreateAndSaveMemberXml(xml, member.Id, uow.Database); } - if (raiseEvents) - Saved.RaiseEvent(new SaveEventArgs(member, false), this); + Saved.RaiseEvent(new SaveEventArgs(member, false), this); + Created.RaiseEvent(new NewEventArgs(member, false, memberType.Alias, -1), this); return member; } - + /// /// Creates and persists a new Member /// @@ -628,9 +625,8 @@ namespace Umbraco.Core.Services /// /// /// - /// /// - public IMember CreateWithIdentity(string username, string email, string password, string memberTypeAlias, bool raiseEvents = true) + IMember IMembershipMemberService.CreateWithIdentity(string username, string email, string password, string memberTypeAlias) { var uow = _uowProvider.GetUnitOfWork(); IMemberType memberType; @@ -646,7 +642,7 @@ namespace Umbraco.Core.Services throw new ArgumentException(string.Format("No MemberType matching the passed in Alias: '{0}' was found", memberTypeAlias)); } - return CreateMemberWithIdentity(username, email, password, memberType, raiseEvents); + return CreateMemberWithIdentity(username, email, password, memberType); } /// diff --git a/src/Umbraco.Core/Services/UserService.cs b/src/Umbraco.Core/Services/UserService.cs index c5167b5bce..395e0aebd2 100644 --- a/src/Umbraco.Core/Services/UserService.cs +++ b/src/Umbraco.Core/Services/UserService.cs @@ -77,7 +77,7 @@ namespace Umbraco.Core.Services } } - public IUser CreateMemberWithIdentity(string username, string email, string password, IUserType userType, bool raiseEvents = true) + public IUser CreateUserWithIdentity(string username, string email, string password, IUserType userType) { if (userType == null) throw new ArgumentNullException("userType"); @@ -105,23 +105,19 @@ namespace Umbraco.Core.Services IsApproved = true }; - if (raiseEvents) - { - if (SavingUser.IsRaisedEventCancelled(new SaveEventArgs(user), this)) - return user; - } + if (SavingUser.IsRaisedEventCancelled(new SaveEventArgs(user), this)) + return user; repository.AddOrUpdate(user); uow.Commit(); - if (raiseEvents) - SavedUser.RaiseEvent(new SaveEventArgs(user, false), this); + SavedUser.RaiseEvent(new SaveEventArgs(user, false), this); return user; } } - public IUser CreateWithIdentity(string username, string email, string password, string memberTypeAlias, bool raiseEvents = true) + IUser IMembershipMemberService.CreateWithIdentity(string username, string email, string password, string memberTypeAlias) { var userType = GetUserTypeByAlias(memberTypeAlias); if (userType == null) @@ -129,7 +125,7 @@ namespace Umbraco.Core.Services throw new EntityNotFoundException("The user type " + memberTypeAlias + " could not be resolved"); } - return CreateMemberWithIdentity(username, email, password, userType); + return CreateUserWithIdentity(username, email, password, userType); } public IUser GetById(int id) diff --git a/src/Umbraco.Core/Strings/DefaultShortStringHelper.cs b/src/Umbraco.Core/Strings/DefaultShortStringHelper.cs index ddf2420e8d..526706aa3a 100644 --- a/src/Umbraco.Core/Strings/DefaultShortStringHelper.cs +++ b/src/Umbraco.Core/Strings/DefaultShortStringHelper.cs @@ -97,6 +97,11 @@ namespace Umbraco.Core.Strings return InvalidFileNameChars.Contains(c) == false; } + public static string CutMaxLength(string text, int length) + { + return text.Length <= length ? text : text.Substring(0, length); + } + #endregion #region Configuration @@ -150,6 +155,7 @@ namespace Umbraco.Core.Strings return WithConfig(CleanStringType.UrlSegment, new Config { PreFilter = ApplyUrlReplaceCharacters, + PostFilter = x => CutMaxLength(x, 240), IsTerm = (c, leading) => char.IsLetterOrDigit(c) || c == '_', // letter, digit or underscore StringType = (UmbracoConfig.For.UmbracoSettings().RequestHandler.ConvertUrlsToAscii ? CleanStringType.Ascii : CleanStringType.Utf8) | CleanStringType.LowerCase, BreakTermsOnUpper = false, @@ -184,6 +190,7 @@ namespace Umbraco.Core.Strings { StringType = CleanStringType.Utf8 | CleanStringType.Unchanged; PreFilter = null; + PostFilter = null; IsTerm = (c, leading) => leading ? char.IsLetter(c) : char.IsLetterOrDigit(c); BreakTermsOnUpper = false; CutAcronymOnNonUpper = false; @@ -196,6 +203,7 @@ namespace Umbraco.Core.Strings return new Config { PreFilter = PreFilter, + PostFilter = PostFilter, IsTerm = IsTerm, StringType = StringType, BreakTermsOnUpper = BreakTermsOnUpper, @@ -206,6 +214,7 @@ namespace Umbraco.Core.Strings } public Func PreFilter { get; set; } + public Func PostFilter { get; set; } public Func IsTerm { get; set; } public CleanStringType StringType { get; set; } @@ -536,6 +545,10 @@ function validateSafeAlias(id, value, immediate, callback) {{ // clean text = CleanCodeString(text, stringType, separator.Value, culture, config); + // apply post-filter + if (config.PostFilter != null) + text = config.PostFilter(text); + return text; } diff --git a/src/Umbraco.Tests/Membership/UmbracoServiceMembershipProviderTests.cs b/src/Umbraco.Tests/Membership/UmbracoServiceMembershipProviderTests.cs index 0053dc758e..64305d6b1c 100644 --- a/src/Umbraco.Tests/Membership/UmbracoServiceMembershipProviderTests.cs +++ b/src/Umbraco.Tests/Membership/UmbracoServiceMembershipProviderTests.cs @@ -94,7 +94,7 @@ namespace Umbraco.Tests.Membership mServiceMock.Setup(service => service.GetByEmail("test@test.com")).Returns(() => null); mServiceMock.Setup(service => service.GetDefaultMemberType()).Returns("Member"); mServiceMock.Setup( - service => service.CreateWithIdentity(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + service => service.CreateWithIdentity(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .Callback((string u, string e, string p, string m, bool b) => { createdMember = new Member("test", e, u, p, memberType); @@ -125,7 +125,7 @@ namespace Umbraco.Tests.Membership mServiceMock.Setup(service => service.GetByEmail("test@test.com")).Returns(() => null); mServiceMock.Setup(service => service.GetDefaultMemberType()).Returns("Member"); mServiceMock.Setup( - service => service.CreateWithIdentity(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + service => service.CreateWithIdentity(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .Callback((string u, string e, string p, string m, bool b) => { createdMember = new Member("test", e, u, p, memberType); @@ -158,7 +158,7 @@ namespace Umbraco.Tests.Membership mServiceMock.Setup(service => service.GetByEmail("test@test.com")).Returns(() => null); mServiceMock.Setup(service => service.GetDefaultMemberType()).Returns("Member"); mServiceMock.Setup( - service => service.CreateWithIdentity(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + service => service.CreateWithIdentity(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .Callback((string u, string e, string p, string m, bool b) => { createdMember = new Member("test", e, u, p, memberType); diff --git a/src/Umbraco.Tests/Models/UmbracoEntityTests.cs b/src/Umbraco.Tests/Models/UmbracoEntityTests.cs index 2be59c7b07..a24b5f1c35 100644 --- a/src/Umbraco.Tests/Models/UmbracoEntityTests.cs +++ b/src/Umbraco.Tests/Models/UmbracoEntityTests.cs @@ -1,4 +1,5 @@ -using NUnit.Framework; +using System; +using NUnit.Framework; using Umbraco.Core.Models; namespace Umbraco.Tests.Models @@ -10,10 +11,10 @@ namespace Umbraco.Tests.Models public void UmbracoEntity_Can_Be_Initialized_From_Dynamic() { var boolIsTrue = true; - var intIsTrue = 1; + ulong ulongIsTrue = 1; // because MySql might return ulong var trashedWithBool = new UmbracoEntity((dynamic)boolIsTrue); - var trashedWithInt = new UmbracoEntity((dynamic)intIsTrue); + var trashedWithInt = new UmbracoEntity((dynamic)ulongIsTrue); Assert.IsTrue(trashedWithBool.Trashed); Assert.IsTrue(trashedWithInt.Trashed); diff --git a/src/Umbraco.Tests/PublishedContent/PublishedContentTestElements.cs b/src/Umbraco.Tests/PublishedContent/PublishedContentTestElements.cs index f7e41afb92..d8b3d52668 100644 --- a/src/Umbraco.Tests/PublishedContent/PublishedContentTestElements.cs +++ b/src/Umbraco.Tests/PublishedContent/PublishedContentTestElements.cs @@ -99,6 +99,12 @@ namespace Umbraco.Tests.PublishedContent { return _content.Count > 0; } + + public IPublishedContent CreateFragment(string contentTypeAlias, IDictionary dataValues, + bool isPreviewing, bool managed) + { + throw new NotImplementedException(); + } } class SolidPublishedContent : IPublishedContent diff --git a/src/Umbraco.Tests/Services/UserServiceTests.cs b/src/Umbraco.Tests/Services/UserServiceTests.cs index a56736774c..932b12cce0 100644 --- a/src/Umbraco.Tests/Services/UserServiceTests.cs +++ b/src/Umbraco.Tests/Services/UserServiceTests.cs @@ -39,7 +39,7 @@ namespace Umbraco.Tests.Services // Arrange var userService = ServiceContext.UserService; var userType = userService.GetUserTypeByAlias("admin"); - var user = ServiceContext.UserService.CreateMemberWithIdentity("test1", "test1@test.com", "123456", userType); + var user = ServiceContext.UserService.CreateUserWithIdentity("test1", "test1@test.com", "123456", userType); var contentType = MockedContentTypes.CreateSimpleContentType(); ServiceContext.ContentTypeService.Save(contentType); var content = new[] @@ -66,7 +66,7 @@ namespace Umbraco.Tests.Services // Arrange var userService = ServiceContext.UserService; var userType = userService.GetUserTypeByAlias("admin"); - var user = ServiceContext.UserService.CreateMemberWithIdentity("test1", "test1@test.com", "123456", userType); + var user = ServiceContext.UserService.CreateUserWithIdentity("test1", "test1@test.com", "123456", userType); var contentType = MockedContentTypes.CreateSimpleContentType(); ServiceContext.ContentTypeService.Save(contentType); var content = new[] @@ -100,7 +100,7 @@ namespace Umbraco.Tests.Services { var userType = MockedUserType.CreateUserType(); ServiceContext.UserService.SaveUserType(userType); - var user = ServiceContext.UserService.CreateMemberWithIdentity("JohnDoe", "john@umbraco.io", "12345", userType); + var user = ServiceContext.UserService.CreateUserWithIdentity("JohnDoe", "john@umbraco.io", "12345", userType); ServiceContext.UserService.Delete(user, true); var deleted = ServiceContext.UserService.GetUserById(user.Id); @@ -114,7 +114,7 @@ namespace Umbraco.Tests.Services { var userType = MockedUserType.CreateUserType(); ServiceContext.UserService.SaveUserType(userType); - var user = ServiceContext.UserService.CreateMemberWithIdentity("JohnDoe", "john@umbraco.io", "12345", userType); + var user = ServiceContext.UserService.CreateUserWithIdentity("JohnDoe", "john@umbraco.io", "12345", userType); ServiceContext.UserService.Delete(user); var deleted = ServiceContext.UserService.GetUserById(user.Id); @@ -128,7 +128,7 @@ namespace Umbraco.Tests.Services { var userType = MockedUserType.CreateUserType(); ServiceContext.UserService.SaveUserType(userType); - var user = ServiceContext.UserService.CreateMemberWithIdentity("JohnDoe", "john@umbraco.io", "12345", userType); + var user = ServiceContext.UserService.CreateUserWithIdentity("JohnDoe", "john@umbraco.io", "12345", userType); Assert.IsTrue(ServiceContext.UserService.Exists("JohnDoe")); Assert.IsFalse(ServiceContext.UserService.Exists("notFound")); @@ -139,7 +139,7 @@ namespace Umbraco.Tests.Services { var userType = MockedUserType.CreateUserType(); ServiceContext.UserService.SaveUserType(userType); - var user = ServiceContext.UserService.CreateMemberWithIdentity("JohnDoe", "john@umbraco.io", "12345", userType); + var user = ServiceContext.UserService.CreateUserWithIdentity("JohnDoe", "john@umbraco.io", "12345", userType); Assert.IsNotNull(ServiceContext.UserService.GetByEmail(user.Email)); Assert.IsNull(ServiceContext.UserService.GetByEmail("do@not.find")); @@ -150,7 +150,7 @@ namespace Umbraco.Tests.Services { var userType = MockedUserType.CreateUserType(); ServiceContext.UserService.SaveUserType(userType); - var user = ServiceContext.UserService.CreateMemberWithIdentity("JohnDoe", "john@umbraco.io", "12345", userType); + var user = ServiceContext.UserService.CreateUserWithIdentity("JohnDoe", "john@umbraco.io", "12345", userType); Assert.IsNotNull(ServiceContext.UserService.GetByUsername(user.Username)); Assert.IsNull(ServiceContext.UserService.GetByUsername("notFound")); @@ -161,7 +161,7 @@ namespace Umbraco.Tests.Services { var userType = MockedUserType.CreateUserType(); ServiceContext.UserService.SaveUserType(userType); - var user = ServiceContext.UserService.CreateMemberWithIdentity("mydomain\\JohnDoe", "john@umbraco.io", "12345", userType); + var user = ServiceContext.UserService.CreateUserWithIdentity("mydomain\\JohnDoe", "john@umbraco.io", "12345", userType); Assert.IsNotNull(ServiceContext.UserService.GetByUsername(user.Username)); Assert.IsNull(ServiceContext.UserService.GetByUsername("notFound")); @@ -172,7 +172,7 @@ namespace Umbraco.Tests.Services { var userType = MockedUserType.CreateUserType(); ServiceContext.UserService.SaveUserType(userType); - var user = ServiceContext.UserService.CreateMemberWithIdentity("JohnDoe", "john@umbraco.io", "12345", userType); + var user = ServiceContext.UserService.CreateUserWithIdentity("JohnDoe", "john@umbraco.io", "12345", userType); Assert.IsNotNull(ServiceContext.UserService.GetUserById(user.Id)); Assert.IsNull(ServiceContext.UserService.GetUserById(9876)); @@ -354,7 +354,7 @@ namespace Umbraco.Tests.Services var userType = userService.GetUserTypeByAlias("admin"); // Act - var membershipUser = userService.CreateMemberWithIdentity("JohnDoe", "john@umbraco.io", "12345", userType); + var membershipUser = userService.CreateUserWithIdentity("JohnDoe", "john@umbraco.io", "12345", userType); // Assert Assert.That(membershipUser.HasIdentity, Is.True); @@ -377,7 +377,7 @@ namespace Umbraco.Tests.Services var hash = new HMACSHA1(); hash.Key = Encoding.Unicode.GetBytes(password); var encodedPassword = Convert.ToBase64String(hash.ComputeHash(Encoding.Unicode.GetBytes(password))); - var membershipUser = userService.CreateMemberWithIdentity("JohnDoe", "john@umbraco.io", encodedPassword, userType); + var membershipUser = userService.CreateUserWithIdentity("JohnDoe", "john@umbraco.io", encodedPassword, userType); // Assert Assert.That(membershipUser.HasIdentity, Is.True); @@ -393,8 +393,8 @@ namespace Umbraco.Tests.Services { var userType = ServiceContext.UserService.GetUserTypeByAlias("admin"); - var user1 = ServiceContext.UserService.CreateMemberWithIdentity("test1", "test1@test.com", "test1", userType); - var user2 = ServiceContext.UserService.CreateMemberWithIdentity("test2", "test2@test.com", "test2", userType); + var user1 = ServiceContext.UserService.CreateUserWithIdentity("test1", "test1@test.com", "test1", userType); + var user2 = ServiceContext.UserService.CreateUserWithIdentity("test2", "test2@test.com", "test2", userType); //adds some allowed sections user1.AddAllowedSection("test"); @@ -418,7 +418,7 @@ namespace Umbraco.Tests.Services { // Arrange var userType = ServiceContext.UserService.GetUserTypeByAlias("admin"); - var user = ServiceContext.UserService.CreateMemberWithIdentity("test1", "test1@test.com", "test1", userType); + var user = ServiceContext.UserService.CreateUserWithIdentity("test1", "test1@test.com", "test1", userType); // Act @@ -435,7 +435,7 @@ namespace Umbraco.Tests.Services { // Arrange var userType = ServiceContext.UserService.GetUserTypeByAlias("admin"); - var user = (IUser)ServiceContext.UserService.CreateMemberWithIdentity("test1", "test1@test.com", "test1", userType); + var user = (IUser)ServiceContext.UserService.CreateUserWithIdentity("test1", "test1@test.com", "test1", userType); // Act @@ -452,7 +452,7 @@ namespace Umbraco.Tests.Services { // Arrange var userType = ServiceContext.UserService.GetUserTypeByAlias("admin"); - var originalUser = (User)ServiceContext.UserService.CreateMemberWithIdentity("test1", "test1@test.com", "test1", userType); + var originalUser = (User)ServiceContext.UserService.CreateUserWithIdentity("test1", "test1@test.com", "test1", userType); // Act diff --git a/src/Umbraco.Web.UI/config/umbracoSettings.config b/src/Umbraco.Web.UI/config/umbracoSettings.config index 579fa7ee54..8f1dec3e4d 100644 --- a/src/Umbraco.Web.UI/config/umbracoSettings.config +++ b/src/Umbraco.Web.UI/config/umbracoSettings.config @@ -211,7 +211,7 @@ - + 0 diff --git a/src/Umbraco.Web.UI/umbraco/controls/ContentTypeControlNew.ascx b/src/Umbraco.Web.UI/umbraco/controls/ContentTypeControlNew.ascx index b6fbbd6bde..8839c3caaf 100644 --- a/src/Umbraco.Web.UI/umbraco/controls/ContentTypeControlNew.ascx +++ b/src/Umbraco.Web.UI/umbraco/controls/ContentTypeControlNew.ascx @@ -126,9 +126,26 @@ return false; }); + + $("table.tabs-table tr.propertyContent input.sort-order").keydown(function(e) { + // Allow: backspace, delete, tab, escape, enter and . + if ($.inArray(e.keyCode, [46, 8, 9, 27, 13, 190]) !== -1 || + // Allow: Ctrl+A + (e.keyCode == 65 && e.ctrlKey === true) || + // Allow: home, end, left, right + (e.keyCode >= 35 && e.keyCode <= 39)) { + // let it happen, don't do anything + return; + } + // Ensure that it is a number and stop the keypress + if ((e.shiftKey || (e.keyCode < 48 || e.keyCode > 57)) && (e.keyCode < 96 || e.keyCode > 105)) { + e.preventDefault(); + } + }); + // Make each tr of the tabs table sortable (prevent dragging of header row, and set up a callback for when row dragged) - jQuery("table.tabs-table tbody").sortable({ + $("table.tabs-table tbody").sortable({ containment: 'parent', cancel: '.propertyHeader, input', tolerance: 'pointer', @@ -139,8 +156,8 @@ // Fired after row dragged; go through each tr and save position to the hidden sort order field function saveOrder() { - jQuery("table.tabs-table tbody tr.propertyContent").each(function (index) { - jQuery("input.sort-order", this).val(index + 1); + $("table.tabs-table tbody tr.propertyContent").each(function (index) { + $("input.sort-order", this).val(index + 1); }); } diff --git a/src/Umbraco.Web.UI/umbraco_client/ui/default.css b/src/Umbraco.Web.UI/umbraco_client/ui/default.css index 57f6fc98c8..f9d2354c35 100644 --- a/src/Umbraco.Web.UI/umbraco_client/ui/default.css +++ b/src/Umbraco.Web.UI/umbraco_client/ui/default.css @@ -751,7 +751,12 @@ table.tabs-table tr.propertyContent td } table.tabs-table input[type=text] { width: 200px; } table.tabs-table input[type=submit] { text-align: right; } -table.tabs-table tr.propertyContent input.sort-order {display: none;} + +table.tabs-table tr.propertyContent input.sort-order { + width: 20px; + background-color: lightgray; + padding: 0px 5px 0px 5px; +} li.no-properties-on-tab {background: none; background-color: #fff; cursor: default; } diff --git a/src/Umbraco.Web/Cache/CacheRefresherEventHandler.cs b/src/Umbraco.Web/Cache/CacheRefresherEventHandler.cs index 9b1a5c041e..5ad59a14da 100644 --- a/src/Umbraco.Web/Cache/CacheRefresherEventHandler.cs +++ b/src/Umbraco.Web/Cache/CacheRefresherEventHandler.cs @@ -134,19 +134,30 @@ namespace Umbraco.Web.Cache MediaService.Moving += MediaServiceMoving; MediaService.Trashing += MediaServiceTrashing; - ContentService.Created += ContentServiceCreated; + //Bind to content events - this is for unpublished content syncing across servers (primarily for examine) + + ContentService.Saved += ContentServiceSaved; + ContentService.Deleted += ContentServiceDeleted; ContentService.Copied += ContentServiceCopied; + //NOTE: we do not listen for the trashed event because there is no cache to update for content in that case since + // the unpublishing event handles that, and for examine with unpublished content indexes, we want to keep that data + // in the index, it's not until it's complete deleted that we want to remove it. } + + #region Content service event handlers /// - /// When an entity is copied new permissions may be assigned to it based on it's parent, if that is the - /// case then we need to clear all user permissions cache. + /// Handles cache refreshgi for when content is copied /// /// /// - static void ContentServiceCopied(IContentService sender, Core.Events.CopyEventArgs e) + /// + /// When an entity is copied new permissions may be assigned to it based on it's parent, if that is the + /// case then we need to clear all user permissions cache. + /// + static void ContentServiceCopied(IContentService sender, CopyEventArgs e) { //check if permissions have changed var permissionsChanged = ((Content)e.Copy).WasPropertyDirty("PermissionsChanged"); @@ -154,23 +165,63 @@ namespace Umbraco.Web.Cache { DistributedCache.Instance.RefreshAllUserPermissionsCache(); } + + //run the un-published cache refresher + DistributedCache.Instance.RefreshUnpublishedPageCache(e.Copy); } /// - /// When an entity is created new permissions may be assigned to it based on it's parent, if that is the - /// case then we need to clear all user permissions cache. + /// Handles cache refreshing for when content is deleted (not unpublished) /// /// /// - static void ContentServiceCreated(IContentService sender, Core.Events.NewEventArgs e) + static void ContentServiceDeleted(IContentService sender, DeleteEventArgs e) { - //check if permissions have changed - var permissionsChanged = ((Content)e.Entity).WasPropertyDirty("PermissionsChanged"); - if (permissionsChanged) + DistributedCache.Instance.RemoveUnpublishedPageCache(e.DeletedEntities.ToArray()); + } + + /// + /// Handles cache refreshing for when content is saved (not published) + /// + /// + /// + /// + /// When an entity is saved we need to notify other servers about the change in order for the Examine indexes to + /// stay up-to-date for unpublished content. + /// + /// When an entity is created new permissions may be assigned to it based on it's parent, if that is the + /// case then we need to clear all user permissions cache. + /// + static void ContentServiceSaved(IContentService sender, SaveEventArgs e) + { + var clearUserPermissions = false; + e.SavedEntities.ForEach(x => + { + //check if it is new + if (x.IsNewEntity()) + { + //check if permissions have changed + var permissionsChanged = ((Content)x).WasPropertyDirty("PermissionsChanged"); + if (permissionsChanged) + { + clearUserPermissions = true; + } + } + }); + + if (clearUserPermissions) { DistributedCache.Instance.RefreshAllUserPermissionsCache(); } - } + + //filter out the entities that have only been saved (not newly published) since + // newly published ones will be synced with the published page cache refresher + var unpublished = e.SavedEntities.Where(x => x.JustPublished() == false); + //run the un-published cache refresher + DistributedCache.Instance.RefreshUnpublishedPageCache(unpublished.ToArray()); + } + + #endregion #region ApplicationTree event handlers @@ -454,12 +505,12 @@ namespace Umbraco.Web.Cache InvalidateCacheForPermissionsChange(sender); } - void UserServiceSavedUser(IUserService sender, Core.Events.SaveEventArgs e) + static void UserServiceSavedUser(IUserService sender, SaveEventArgs e) { e.SavedEntities.ForEach(x => DistributedCache.Instance.RefreshUserCache(x.Id)); } - void UserServiceDeletedUser(IUserService sender, Core.Events.DeleteEventArgs e) + static void UserServiceDeletedUser(IUserService sender, DeleteEventArgs e) { e.DeletedEntities.ForEach(x => DistributedCache.Instance.RemoveUserCache(x.Id)); } @@ -565,22 +616,22 @@ namespace Umbraco.Web.Cache #endregion #region Media event handlers - static void MediaServiceTrashing(IMediaService sender, Core.Events.MoveEventArgs e) + static void MediaServiceTrashing(IMediaService sender, MoveEventArgs e) { - DistributedCache.Instance.RemoveMediaCache(e.Entity); + DistributedCache.Instance.RemoveMediaCache(false, e.Entity); } - static void MediaServiceMoving(IMediaService sender, Core.Events.MoveEventArgs e) + static void MediaServiceMoving(IMediaService sender, MoveEventArgs e) { DistributedCache.Instance.RefreshMediaCache(e.Entity); } - static void MediaServiceDeleting(IMediaService sender, Core.Events.DeleteEventArgs e) + static void MediaServiceDeleting(IMediaService sender, DeleteEventArgs e) { - DistributedCache.Instance.RemoveMediaCache(e.DeletedEntities.ToArray()); + DistributedCache.Instance.RemoveMediaCache(true, e.DeletedEntities.ToArray()); } - static void MediaServiceSaved(IMediaService sender, Core.Events.SaveEventArgs e) + static void MediaServiceSaved(IMediaService sender, SaveEventArgs e) { DistributedCache.Instance.RefreshMediaCache(e.SavedEntities.ToArray()); } @@ -588,27 +639,21 @@ namespace Umbraco.Web.Cache #region Member event handlers - static void MemberServiceDeleted(IMemberService sender, Core.Events.DeleteEventArgs e) + static void MemberServiceDeleted(IMemberService sender, DeleteEventArgs e) { - foreach (var m in e.DeletedEntities.ToArray()) - { - DistributedCache.Instance.RemoveMemberCache(m.Id); - } + DistributedCache.Instance.RemoveMemberCache(e.DeletedEntities.ToArray()); } - static void MemberServiceSaved(IMemberService sender, Core.Events.SaveEventArgs e) + static void MemberServiceSaved(IMemberService sender, SaveEventArgs e) { - foreach (var m in e.SavedEntities.ToArray()) - { - DistributedCache.Instance.RefreshMemberCache(m.Id); - } + DistributedCache.Instance.RefreshMemberCache(e.SavedEntities.ToArray()); } #endregion #region Member group event handlers - static void MemberGroupService_Deleted(IMemberGroupService sender, Core.Events.DeleteEventArgs e) + static void MemberGroupService_Deleted(IMemberGroupService sender, DeleteEventArgs e) { foreach (var m in e.DeletedEntities.ToArray()) { @@ -616,7 +661,7 @@ namespace Umbraco.Web.Cache } } - static void MemberGroupService_Saved(IMemberGroupService sender, Core.Events.SaveEventArgs e) + static void MemberGroupService_Saved(IMemberGroupService sender, SaveEventArgs e) { foreach (var m in e.SavedEntities.ToArray()) { diff --git a/src/Umbraco.Web/Cache/DistributedCache.cs b/src/Umbraco.Web/Cache/DistributedCache.cs index b66dd2174a..8fa1dec3b3 100644 --- a/src/Umbraco.Web/Cache/DistributedCache.cs +++ b/src/Umbraco.Web/Cache/DistributedCache.cs @@ -39,6 +39,7 @@ namespace Umbraco.Web.Cache public const string ApplicationCacheRefresherId = "B15F34A1-BC1D-4F8B-8369-3222728AB4C8"; public const string TemplateRefresherId = "DD12B6A0-14B9-46e8-8800-C154F74047C8"; public const string PageCacheRefresherId = "27AB3022-3DFA-47b6-9119-5945BC88FD66"; + public const string UnpublishedPageCacheRefresherId = "55698352-DFC5-4DBE-96BD-A4A0F6F77145"; public const string MemberCacheRefresherId = "E285DF34-ACDC-4226-AE32-C0CB5CF388DA"; public const string MemberGroupCacheRefresherId = "187F236B-BD21-4C85-8A7C-29FBA3D6C00C"; public const string MediaCacheRefresherId = "B29286DD-2D40-4DDB-B325-681226589FEC"; diff --git a/src/Umbraco.Web/Cache/DistributedCacheExtensions.cs b/src/Umbraco.Web/Cache/DistributedCacheExtensions.cs index 73df419550..601a189107 100644 --- a/src/Umbraco.Web/Cache/DistributedCacheExtensions.cs +++ b/src/Umbraco.Web/Cache/DistributedCacheExtensions.cs @@ -124,8 +124,7 @@ namespace Umbraco.Web.Cache } #endregion - - + #region Data type cache /// /// Refreshes the cache amongst servers for a data type @@ -233,15 +232,58 @@ namespace Umbraco.Web.Cache public static void RemovePageCache(this DistributedCache dc, int documentId) { dc.Remove(new Guid(DistributedCache.PageCacheRefresherId), documentId); - } + } + + /// + /// invokes the unpublished page cache refresher + /// + /// + /// + public static void RefreshUnpublishedPageCache(this DistributedCache dc, params IContent[] content) + { + dc.Refresh(new Guid(DistributedCache.UnpublishedPageCacheRefresherId), x => x.Id, content); + } + + /// + /// invokes the unpublished page cache refresher + /// + /// + /// + public static void RemoveUnpublishedPageCache(this DistributedCache dc, params IContent[] content) + { + dc.Remove(new Guid(DistributedCache.UnpublishedPageCacheRefresherId), x => x.Id, content); + } + #endregion #region Member cache + + /// + /// Refreshes the cache among servers for a member + /// + /// + /// + public static void RefreshMemberCache(this DistributedCache dc, params IMember[] members) + { + dc.Refresh(new Guid(DistributedCache.MemberCacheRefresherId), x => x.Id, members); + } + + /// + /// Removes the cache among servers for a member + /// + /// + /// + public static void RemoveMemberCache(this DistributedCache dc, params IMember[] members) + { + dc.Remove(new Guid(DistributedCache.MemberCacheRefresherId), x => x.Id, members); + } + /// /// Refreshes the cache among servers for a member /// /// /// + [Obsolete("Use the RefreshMemberCache with strongly typed IMember objects instead")] public static void RefreshMemberCache(this DistributedCache dc, int memberId) { dc.Refresh(new Guid(DistributedCache.MemberCacheRefresherId), memberId); @@ -252,6 +294,7 @@ namespace Umbraco.Web.Cache /// /// /// + [Obsolete("Use the RemoveMemberCache with strongly typed IMember objects instead")] public static void RemoveMemberCache(this DistributedCache dc, int memberId) { dc.Remove(new Guid(DistributedCache.MemberCacheRefresherId), memberId); @@ -292,7 +335,7 @@ namespace Umbraco.Web.Cache public static void RefreshMediaCache(this DistributedCache dc, params IMedia[] media) { dc.RefreshByJson(new Guid(DistributedCache.MediaCacheRefresherId), - MediaCacheRefresher.SerializeToJsonPayload(media)); + MediaCacheRefresher.SerializeToJsonPayload(MediaCacheRefresher.OperationType.Saved, media)); } /// @@ -305,6 +348,7 @@ namespace Umbraco.Web.Cache /// to clear all of the cache but the media item will be removed before the other servers can /// look it up. Only here for legacy purposes. /// + [Obsolete("Ensure to clear with other RemoveMediaCache overload")] public static void RemoveMediaCache(this DistributedCache dc, int mediaId) { dc.Remove(new Guid(DistributedCache.MediaCacheRefresherId), mediaId); @@ -314,11 +358,14 @@ namespace Umbraco.Web.Cache /// Removes the cache amongst servers for media items /// /// + /// /// - public static void RemoveMediaCache(this DistributedCache dc, params IMedia[] media) + public static void RemoveMediaCache(this DistributedCache dc, bool isPermanentlyDeleted, params IMedia[] media) { - dc.RefreshByJson(new Guid(DistributedCache.MediaCacheRefresherId), - MediaCacheRefresher.SerializeToJsonPayload(media)); + dc.RefreshByJson(new Guid(DistributedCache.MediaCacheRefresherId), + MediaCacheRefresher.SerializeToJsonPayload( + isPermanentlyDeleted ? MediaCacheRefresher.OperationType.Deleted : MediaCacheRefresher.OperationType.Trashed, + media)); } #endregion diff --git a/src/Umbraco.Web/Cache/MediaCacheRefresher.cs b/src/Umbraco.Web/Cache/MediaCacheRefresher.cs index 53abacc7a5..72a541b720 100644 --- a/src/Umbraco.Web/Cache/MediaCacheRefresher.cs +++ b/src/Umbraco.Web/Cache/MediaCacheRefresher.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.Web.Script.Serialization; using Umbraco.Core; using Umbraco.Core.Cache; @@ -19,13 +20,13 @@ namespace Umbraco.Web.Cache public class MediaCacheRefresher : JsonCacheRefresherBase { #region Static helpers - + /// /// Converts the json to a JsonPayload object /// /// /// - private static JsonPayload[] DeserializeFromJsonPayload(string json) + internal static JsonPayload[] DeserializeFromJsonPayload(string json) { var serializer = new JavaScriptSerializer(); var jsonObject = serializer.Deserialize(json); @@ -35,12 +36,13 @@ namespace Umbraco.Web.Cache /// /// Creates the custom Json payload used to refresh cache amongst the servers /// + /// /// /// - internal static string SerializeToJsonPayload(params IMedia[] media) + internal static string SerializeToJsonPayload(OperationType operation, params IMedia[] media) { var serializer = new JavaScriptSerializer(); - var items = media.Select(FromMedia).ToArray(); + var items = media.Select(x => FromMedia(x, operation)).ToArray(); var json = serializer.Serialize(items); return json; } @@ -49,15 +51,17 @@ namespace Umbraco.Web.Cache /// Converts a macro to a jsonPayload object /// /// + /// /// - private static JsonPayload FromMedia(IMedia media) + internal static JsonPayload FromMedia(IMedia media, OperationType operation) { if (media == null) return null; var payload = new JsonPayload { Id = media.Id, - Path = media.Path + Path = media.Path, + Operation = operation }; return payload; } @@ -66,10 +70,18 @@ namespace Umbraco.Web.Cache #region Sub classes - private class JsonPayload + internal enum OperationType + { + Saved, + Trashed, + Deleted + } + + internal class JsonPayload { public string Path { get; set; } public int Id { get; set; } + public OperationType Operation { get; set; } } #endregion @@ -97,13 +109,15 @@ namespace Umbraco.Web.Cache public override void Refresh(int id) { - ClearCache(FromMedia(ApplicationContext.Current.Services.MediaService.GetById(id))); + ClearCache(FromMedia(ApplicationContext.Current.Services.MediaService.GetById(id), OperationType.Saved)); base.Refresh(id); } public override void Remove(int id) - { - ClearCache(FromMedia(ApplicationContext.Current.Services.MediaService.GetById(id))); + { + ClearCache(FromMedia(ApplicationContext.Current.Services.MediaService.GetById(id), + //NOTE: we'll just default to trashed for this one. + OperationType.Trashed)); base.Remove(id); } @@ -121,7 +135,7 @@ namespace Umbraco.Web.Cache string.Format("{0}_{1}_True", CacheKeys.MediaCacheKey, idPart)); // Also clear calls that only query this specific item! - if (idPart == payload.Id.ToString()) + if (idPart == payload.Id.ToString(CultureInfo.InvariantCulture)) ApplicationContext.Current.ApplicationCache.ClearCacheByKeySearch( string.Format("{0}_{1}", CacheKeys.MediaCacheKey, payload.Id)); diff --git a/src/Umbraco.Web/Cache/MemberCacheRefresher.cs b/src/Umbraco.Web/Cache/MemberCacheRefresher.cs index 2b740e826d..5267602402 100644 --- a/src/Umbraco.Web/Cache/MemberCacheRefresher.cs +++ b/src/Umbraco.Web/Cache/MemberCacheRefresher.cs @@ -14,7 +14,7 @@ namespace Umbraco.Web.Cache /// /// This is not intended to be used directly in your code and it should be sealed but due to legacy code we cannot seal it. /// - public class MemberCacheRefresher : CacheRefresherBase + public class MemberCacheRefresher : TypedCacheRefresherBase { protected override MemberCacheRefresher Instance @@ -44,6 +44,18 @@ namespace Umbraco.Web.Cache base.Remove(id); } + public override void Refresh(IMember instance) + { + ClearCache(instance.Id); + base.Remove(instance); + } + + public override void Remove(IMember instance) + { + ClearCache(instance.Id); + base.Remove(instance); + } + private void ClearCache(int id) { ApplicationContext.Current.ApplicationCache.ClearPartialViewCache(); diff --git a/src/Umbraco.Web/Cache/UnpublishedPageCacheRefresher.cs b/src/Umbraco.Web/Cache/UnpublishedPageCacheRefresher.cs new file mode 100644 index 0000000000..4413a61e9b --- /dev/null +++ b/src/Umbraco.Web/Cache/UnpublishedPageCacheRefresher.cs @@ -0,0 +1,33 @@ +using System; +using Umbraco.Core.Cache; +using Umbraco.Core.Models; + +namespace Umbraco.Web.Cache +{ + /// + /// A cache refresher used for non-published content, this is primarily to notify Examine indexes to update + /// + public sealed class UnpublishedPageCacheRefresher : TypedCacheRefresherBase + { + protected override UnpublishedPageCacheRefresher Instance + { + get { return this; } + } + + public override Guid UniqueIdentifier + { + get { return new Guid(DistributedCache.UnpublishedPageCacheRefresherId); } + } + + public override string Name + { + get { return "Unpublished Page Refresher"; } + } + + //NOTE: There is no functionality for this cache refresher, it is here simply to emit events on each server for which examine + // binds to. We could put the Examine index functionality in here but we've kept it all in the ExamineEvents class so that all of + // the logic is in one place. In the future we may put the examine logic in a cache refresher instead (that would make sense) but we'd + // want to get this done before making more cache refreshers: + // http://issues.umbraco.org/issue/U4-2633 + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/Mvc/UmbracoViewPageOfTModel.cs b/src/Umbraco.Web/Mvc/UmbracoViewPageOfTModel.cs index b06a04b79d..0cf248fd48 100644 --- a/src/Umbraco.Web/Mvc/UmbracoViewPageOfTModel.cs +++ b/src/Umbraco.Web/Mvc/UmbracoViewPageOfTModel.cs @@ -141,6 +141,7 @@ namespace Umbraco.Web.Mvc // maps model protected override void SetViewData(ViewDataDictionary viewData) { + // if view data contains no model, nothing to do var source = viewData.Model; if (source == null) { @@ -148,6 +149,8 @@ namespace Umbraco.Web.Mvc return; } + // get the type of the view data model (what we have) + // get the type of this view model (what we want) var sourceType = source.GetType(); var targetType = typeof (TModel); @@ -160,13 +163,15 @@ namespace Umbraco.Web.Mvc // try to grab the content // if no content is found, return, nothing we can do - var sourceContent = source as IPublishedContent; + var sourceContent = source as IPublishedContent; // check if what we have is an IPublishedContent if (sourceContent == null && sourceType.Implements()) { + // else check if it's an IRenderModel => get the content sourceContent = ((IRenderModel)source).Content; } if (sourceContent == null) { + // else check if we can convert it to a content var attempt = source.TryConvertTo(); if (attempt.Success) sourceContent = attempt.Result; } diff --git a/src/Umbraco.Web/PublishedCache/IPublishedContentCache.cs b/src/Umbraco.Web/PublishedCache/IPublishedContentCache.cs index a80aaa41b3..f020dc6097 100644 --- a/src/Umbraco.Web/PublishedCache/IPublishedContentCache.cs +++ b/src/Umbraco.Web/PublishedCache/IPublishedContentCache.cs @@ -33,5 +33,53 @@ namespace Umbraco.Web.PublishedCache /// The route. /// The value of overrides the context. string GetRouteById(UmbracoContext umbracoContext, bool preview, int contentId); + + /// + /// Creates a content fragment. + /// + /// The content type alias. + /// The content property raw values. + /// A value indicating whether the fragment is previewing. + /// A value indicating whether the fragment is managed by the cache. + /// The newly created content fragment. + // + // notes + // + // in XmlPublishedCache, IPublishedContent instances are not meant to survive longer + // that a request or else we cannot guarantee that the converted property values will + // be properly managed - because XmlPublishedProperty just stores the result of the + // conversion locally. + // + // in DrippingPublishedCache, IPublishedContent instances are meant to survive for as + // long as the content itself has not been modified, and the property respects the + // converter's indication ie whether the converted value should be cached at + // .Content - cache until the content changes + // .ContentCache - cache until any content changes + // .Request - cache for the current request + // + // a fragment can be either "detached" or "managed". + // detached: created from code, managed by code, converted property values are + // cached within the fragment itself for as long as the fragment lives + // managed: created from a property converter as part of a content, managed by + // the cache, converted property values can be cached... + // + // XmlPublishedCache: same as content properties, store the result of the + // conversion locally, neither content nor fragments should survive longer + // than a request + // DrippingPublishedCache: depends + // .Content: cache within the fragment + // .ContentCache, .Request: cache within the cache + // + // in the latter case, use a fragment-owned guid as the cache key. because we + // don't really have any other choice. this opens potential memory leaks: if the + // fragment is re-created on each request and has a property that caches its + // converted value at .ContentCache level then we'll flood that cache with data + // that's never removed (as long as no content is edited). + // + // so a requirement should be that any converter that creates fragment, should + // be marked .Content -- and nothing else + // + IPublishedContent CreateFragment(string contentTypeAlias, IDictionary dataValues, + bool isPreviewing, bool managed); } } diff --git a/src/Umbraco.Web/PublishedCache/XmlPublishedCache/PublishedContentCache.cs b/src/Umbraco.Web/PublishedCache/XmlPublishedCache/PublishedContentCache.cs index 0cbfd15344..ce9b7e6f0d 100644 --- a/src/Umbraco.Web/PublishedCache/XmlPublishedCache/PublishedContentCache.cs +++ b/src/Umbraco.Web/PublishedCache/XmlPublishedCache/PublishedContentCache.cs @@ -462,5 +462,15 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache } #endregion + + #region Fragments + + public IPublishedContent CreateFragment(string contentTypeAlias, IDictionary dataValues, + bool isPreviewing, bool managed) + { + return new PublishedFragment(contentTypeAlias, dataValues, isPreviewing, managed); + } + + #endregion } } \ No newline at end of file diff --git a/src/Umbraco.Web/PublishedCache/XmlPublishedCache/PublishedFragment.cs b/src/Umbraco.Web/PublishedCache/XmlPublishedCache/PublishedFragment.cs new file mode 100644 index 0000000000..860f9f043f --- /dev/null +++ b/src/Umbraco.Web/PublishedCache/XmlPublishedCache/PublishedFragment.cs @@ -0,0 +1,169 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Umbraco.Core; +using Umbraco.Core.Models; +using Umbraco.Core.Models.PublishedContent; +using Umbraco.Web.Models; + +namespace Umbraco.Web.PublishedCache.XmlPublishedCache +{ + class PublishedFragment : PublishedContentBase + { + private readonly PublishedContentType _contentType; + private readonly IPublishedProperty[] _properties; + + public PublishedFragment(string contentTypeAlias, IDictionary dataValues, + bool isPreviewing, bool managed) + { + IsPreviewing = isPreviewing; + _contentType = PublishedContentType.Get(PublishedItemType.Content, contentTypeAlias); + + // we don't care about managed because in both cases, XmlPublishedCache stores + // converted property values in the IPublishedContent, which is not meant to + // survive the request + + var dataValues2 = new Dictionary(); + foreach (var kvp in dataValues) + dataValues2[kvp.Key.ToLowerInvariant()] = kvp.Value; + + _properties = _contentType.PropertyTypes + .Select(x => + { + object dataValue; + return dataValues2.TryGetValue(x.PropertyTypeAlias.ToLowerInvariant(), out dataValue) + ? new PublishedFragmentProperty(x, this, dataValue) + : new PublishedFragmentProperty(x, this); + }) + .Cast() + .ToArray(); + } + + #region IPublishedContent + + public override int Id + { + get { throw new NotImplementedException(); } + } + + public override int DocumentTypeId + { + get { return _contentType.Id; } + } + + public override string DocumentTypeAlias + { + get { return _contentType.Alias; } + } + + public override PublishedItemType ItemType + { + get { return PublishedItemType.Content; } + } + + public override string Name + { + get { throw new NotImplementedException(); } + } + + public override int Level + { + get { throw new NotImplementedException(); } + } + + public override string Path + { + get { throw new NotImplementedException(); } + } + + public override int SortOrder + { + // note - could a published fragment have a sort order? + get { throw new NotImplementedException(); } + } + + public override Guid Version + { + get { throw new NotImplementedException(); } + } + + public override int TemplateId + { + get { throw new NotImplementedException(); } + } + + public override string UrlName + { + get { return string.Empty; } + } + + public override DateTime CreateDate + { + get { throw new NotImplementedException(); } + } + + public override DateTime UpdateDate + { + get { throw new NotImplementedException(); } + } + + public override int CreatorId + { + get { throw new NotImplementedException(); } + } + + public override string CreatorName + { + get { throw new NotImplementedException(); } + } + + public override int WriterId + { + get { throw new NotImplementedException(); } + } + + public override string WriterName + { + get { throw new NotImplementedException(); } + } + + public override bool IsDraft + { + get { throw new NotImplementedException(); } + } + + public override IPublishedContent Parent + { + get { throw new NotImplementedException(); } + } + + public override IEnumerable Children + { + get { throw new NotImplementedException(); } + } + + public override ICollection Properties + { + get { return _properties; } + } + + public override IPublishedProperty GetProperty(string alias) + { + return _properties.FirstOrDefault(x => x.PropertyTypeAlias.InvariantEquals(alias)); + } + + public override PublishedContentType ContentType + { + get { return _contentType; } + } + + #endregion + + #region Internal + + // used by PublishedFragmentProperty + internal bool IsPreviewing { get; private set; } + + #endregion + } +} diff --git a/src/Umbraco.Web/PublishedCache/XmlPublishedCache/PublishedFragmentProperty.cs b/src/Umbraco.Web/PublishedCache/XmlPublishedCache/PublishedFragmentProperty.cs new file mode 100644 index 0000000000..7ae10aebdd --- /dev/null +++ b/src/Umbraco.Web/PublishedCache/XmlPublishedCache/PublishedFragmentProperty.cs @@ -0,0 +1,43 @@ +using System; +using Umbraco.Core.Models.PublishedContent; + +namespace Umbraco.Web.PublishedCache.XmlPublishedCache +{ + class PublishedFragmentProperty : PublishedPropertyBase + { + private readonly object _dataValue; + private readonly PublishedFragment _content; + + private readonly Lazy _sourceValue; + private readonly Lazy _objectValue; + private readonly Lazy _xpathValue; + + public PublishedFragmentProperty(PublishedPropertyType propertyType, PublishedFragment content) + : this(propertyType, content, null) + { } + + public PublishedFragmentProperty(PublishedPropertyType propertyType, PublishedFragment content, object dataValue) + : base(propertyType) + { + _dataValue = dataValue; + _content = content; + + _sourceValue = new Lazy(() => PropertyType.ConvertDataToSource(_dataValue, _content.IsPreviewing)); + _objectValue = new Lazy(() => PropertyType.ConvertSourceToObject(_sourceValue.Value, _content.IsPreviewing)); + _xpathValue = new Lazy(() => PropertyType.ConvertSourceToXPath(_sourceValue.Value, _content.IsPreviewing)); + } + + public override bool HasValue + { + get { return _dataValue != null && ((_dataValue is string) == false || string.IsNullOrWhiteSpace((string)_dataValue) == false); } + } + + public override object DataValue + { + get { return _dataValue; } + } + + public override object Value { get { return _objectValue.Value; } } + public override object XPathValue { get { return _xpathValue.Value; } } + } +} diff --git a/src/Umbraco.Web/Routing/DomainHelper.cs b/src/Umbraco.Web/Routing/DomainHelper.cs index 85a1c5f66f..9f64b51e04 100644 --- a/src/Umbraco.Web/Routing/DomainHelper.cs +++ b/src/Umbraco.Web/Routing/DomainHelper.cs @@ -152,12 +152,21 @@ namespace Umbraco.Web.Routing } else { - // look for the first domain that would be the base of the hint - var hintWithSlash = current.EndPathWithSlash(); + // look for the first domain that would be the base of the current url + // ie current is www.example.com/foo/bar, look for domain www.example.com + var currentWithSlash = current.EndPathWithSlash(); domainAndUri = domainsAndUris - .FirstOrDefault(d => d.Uri.EndPathWithSlash().IsBaseOf(hintWithSlash)); + .FirstOrDefault(d => d.Uri.EndPathWithSlash().IsBaseOf(currentWithSlash)); + if (domainAndUri != null) return domainAndUri; + + // look for the first domain that the current url would be the base of + // ie current is www.example.com, look for domain www.example.com/foo/bar + domainAndUri = domainsAndUris + .FirstOrDefault(d => currentWithSlash.IsBaseOf(d.Uri.EndPathWithSlash())); + if (domainAndUri != null) return domainAndUri; + // if none matches, then try to run the filter to pick a domain - if (domainAndUri == null && filter != null) + if (filter != null) { domainAndUri = filter(domainsAndUris); // if still nothing, pick the first one? diff --git a/src/Umbraco.Web/Routing/PublishedContentRequest.cs b/src/Umbraco.Web/Routing/PublishedContentRequest.cs index 08eb2946ef..1836a72812 100644 --- a/src/Umbraco.Web/Routing/PublishedContentRequest.cs +++ b/src/Umbraco.Web/Routing/PublishedContentRequest.cs @@ -313,6 +313,16 @@ namespace Umbraco.Web.Routing TemplateModel = template; } + /// + /// Resets the template. + /// + /// The RenderingEngine becomes unknown. + public void ResetTemplate() + { + EnsureWriteable(); + TemplateModel = null; + } + /// /// Gets a value indicating whether the content request has a template. /// diff --git a/src/Umbraco.Web/Search/ExamineEvents.cs b/src/Umbraco.Web/Search/ExamineEvents.cs index 4e13d4262f..3dd3082b78 100644 --- a/src/Umbraco.Web/Search/ExamineEvents.cs +++ b/src/Umbraco.Web/Search/ExamineEvents.cs @@ -8,9 +8,13 @@ using Examine; using Examine.LuceneEngine; using Lucene.Net.Documents; using Umbraco.Core; +using Umbraco.Core.Cache; using Umbraco.Core.Logging; using Umbraco.Core.Models; +using Umbraco.Core.Models.EntityBase; using Umbraco.Core.Services; +using Umbraco.Core.Sync; +using Umbraco.Web.Cache; using UmbracoExamine; using umbraco; using umbraco.BusinessLogic; @@ -51,26 +55,16 @@ namespace Umbraco.Web.Search if (registeredProviders == 0) return; - MediaService.Saved += MediaServiceSaved; - MediaService.Deleted += MediaServiceDeleted; - MediaService.Moved += MediaServiceMoved; - MediaService.Trashed += MediaServiceTrashed; - - ContentService.Saved += ContentServiceSaved; - ContentService.Deleted += ContentServiceDeleted; - ContentService.Moved += ContentServiceMoved; - ContentService.Trashed += ContentServiceTrashed; - - //These should only fire for providers that DONT have SupportUnpublishedContent set to true - content.AfterUpdateDocumentCache += ContentAfterUpdateDocumentCache; - content.AfterClearDocumentCache += ContentAfterClearDocumentCache; - + //Bind to distributed cache events - this ensures that this logic occurs on ALL servers that are taking part + // in a load balanced environment. + CacheRefresherBase.CacheUpdated += UnpublishedPageCacheRefresherCacheUpdated; + CacheRefresherBase.CacheUpdated += PublishedPageCacheRefresherCacheUpdated; + CacheRefresherBase.CacheUpdated += MediaCacheRefresherCacheUpdated; + CacheRefresherBase.CacheUpdated += MemberCacheRefresherCacheUpdated; //TODO: Remove the legacy event handlers once we proxy the legacy members stuff through the new services - Member.AfterSave += MemberAfterSave; - Member.AfterDelete += MemberAfterDelete; MemberService.Saved += MemberServiceSaved; MemberService.Deleted += MemberServiceDeleted; - + var contentIndexer = ExamineManager.Instance.IndexProviderCollection["InternalIndexer"] as UmbracoContentIndexer; if (contentIndexer != null) { @@ -83,136 +77,242 @@ namespace Umbraco.Web.Search } } - static void ContentServiceTrashed(IContentService sender, Core.Events.MoveEventArgs e) - { - IndexConent(e.Entity); - } - - - static void MediaServiceTrashed(IMediaService sender, Core.Events.MoveEventArgs e) - { - IndexMedia(e.Entity); - } - - - static void ContentServiceMoved(IContentService sender, Umbraco.Core.Events.MoveEventArgs e) - { - IndexConent(e.Entity); - } - - - static void ContentServiceDeleted(IContentService sender, Umbraco.Core.Events.DeleteEventArgs e) - { - e.DeletedEntities.ForEach( - content => - ExamineManager.Instance.DeleteFromIndex( - content.Id.ToString(), - ExamineManager.Instance.IndexProviderCollection.OfType().Where(x => x.EnableDefaultEventHandler))); - } - - - static void ContentServiceSaved(IContentService sender, Umbraco.Core.Events.SaveEventArgs e) - { - e.SavedEntities.ForEach(IndexConent); - } - - - static void MediaServiceMoved(IMediaService sender, Umbraco.Core.Events.MoveEventArgs e) - { - IndexMedia(e.Entity); - } - - - static void MediaServiceDeleted(IMediaService sender, Umbraco.Core.Events.DeleteEventArgs e) - { - e.DeletedEntities.ForEach( - media => - ExamineManager.Instance.DeleteFromIndex( - media.Id.ToString(), - ExamineManager.Instance.IndexProviderCollection.OfType().Where(x => x.EnableDefaultEventHandler))); - } - - - static void MediaServiceSaved(IMediaService sender, Umbraco.Core.Events.SaveEventArgs e) - { - e.SavedEntities.ForEach(IndexMedia); - } - - static void MemberServiceSaved(IMemberService sender, Core.Events.SaveEventArgs e) - { - foreach (var m in e.SavedEntities) + static void MemberCacheRefresherCacheUpdated(MemberCacheRefresher sender, CacheRefresherEventArgs e) + { + switch (e.MessageType) { - var xml = m.ToXml(); - //ensure that only the providers are flagged to listen execute - var providers = ExamineManager.Instance.IndexProviderCollection.OfType() - .Where(x => x.EnableDefaultEventHandler); - ExamineManager.Instance.ReIndexNode(xml, IndexTypes.Member, providers); + case MessageType.RefreshById: + var c1 = ApplicationContext.Current.Services.MemberService.GetById((int)e.MessageObject); + if (c1 != null) + { + ReIndexForMember(c1); + } + break; + case MessageType.RemoveById: + + // This is triggered when the item is permanently deleted + + DeleteIndexForEntity((int)e.MessageObject, false); + break; + case MessageType.RefreshByInstance: + var c3 = e.MessageObject as IMember; + if (c3 != null) + { + ReIndexForMember(c3); + } + break; + case MessageType.RemoveByInstance: + + // This is triggered when the item is permanently deleted + + var c4 = e.MessageObject as IMember; + if (c4 != null) + { + DeleteIndexForEntity(c4.Id, false); + } + break; + case MessageType.RefreshAll: + case MessageType.RefreshByJson: + default: + //We don't support these, these message types will not fire for unpublished content + break; + } + } + + /// + /// Handles index management for all media events - basically handling saving/copying/trashing/deleting + /// + /// + /// + static void MediaCacheRefresherCacheUpdated(MediaCacheRefresher sender, CacheRefresherEventArgs e) + { + switch (e.MessageType) + { + case MessageType.RefreshById: + var c1 = ApplicationContext.Current.Services.MediaService.GetById((int)e.MessageObject); + if (c1 != null) + { + ReIndexForMedia(c1, c1.Trashed == false); + } + break; + case MessageType.RemoveById: + var c2 = ApplicationContext.Current.Services.MediaService.GetById((int)e.MessageObject); + if (c2 != null) + { + //This is triggered when the item has trashed. + // So we need to delete the index from all indexes not supporting unpublished content. + + DeleteIndexForEntity(c2.Id, true); + + //We then need to re-index this item for all indexes supporting unpublished content + + ReIndexForMedia(c2, false); + } + break; + case MessageType.RefreshByJson: + + var jsonPayloads = MediaCacheRefresher.DeserializeFromJsonPayload((string)e.MessageObject); + if (jsonPayloads.Any()) + { + foreach (var payload in jsonPayloads) + { + switch (payload.Operation) + { + case MediaCacheRefresher.OperationType.Saved: + var media = ApplicationContext.Current.Services.MediaService.GetById(payload.Id); + if (media != null) + { + ReIndexForMedia(media, media.Trashed == false); + } + break; + case MediaCacheRefresher.OperationType.Trashed: + //keep if trashed for indexes supporting unpublished + DeleteIndexForEntity(payload.Id, true); + break; + case MediaCacheRefresher.OperationType.Deleted: + //permanently remove from all indexes + DeleteIndexForEntity(payload.Id, false); + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + } + + break; + case MessageType.RefreshByInstance: + case MessageType.RemoveByInstance: + case MessageType.RefreshAll: + default: + //We don't support these, these message types will not fire for media + break; } } - static void MemberServiceDeleted(IMemberService sender, Core.Events.DeleteEventArgs e) + /// + /// Handles index management for all published content events - basically handling published/unpublished + /// + /// + /// + /// + /// This will execute on all servers taking part in load balancing + /// + static void PublishedPageCacheRefresherCacheUpdated(PageCacheRefresher sender, CacheRefresherEventArgs e) { - foreach (var m in e.DeletedEntities) + switch (e.MessageType) { - var nodeId = m.Id.ToString(CultureInfo.InvariantCulture); - //ensure that only the providers are flagged to listen execute - ExamineManager.Instance.DeleteFromIndex(nodeId, - ExamineManager.Instance.IndexProviderCollection.OfType() - .Where(x => x.EnableDefaultEventHandler)); + case MessageType.RefreshById: + var c1 = ApplicationContext.Current.Services.ContentService.GetById((int)e.MessageObject); + if (c1 != null) + { + ReIndexForContent(c1, true); + } + break; + case MessageType.RemoveById: + + //This is triggered when the item has been unpublished or trashed (which also performs an unpublish). + + var c2 = ApplicationContext.Current.Services.ContentService.GetById((int)e.MessageObject); + if (c2 != null) + { + // So we need to delete the index from all indexes not supporting unpublished content. + + DeleteIndexForEntity(c2.Id, true); + + // We then need to re-index this item for all indexes supporting unpublished content + + ReIndexForContent(c2, false); + } + break; + case MessageType.RefreshByInstance: + var c3 = e.MessageObject as IContent; + if (c3 != null) + { + ReIndexForContent(c3, true); + } + break; + case MessageType.RemoveByInstance: + + //This is triggered when the item has been unpublished or trashed (which also performs an unpublish). + + var c4 = e.MessageObject as IContent; + if (c4 != null) + { + // So we need to delete the index from all indexes not supporting unpublished content. + + DeleteIndexForEntity(c4.Id, true); + + // We then need to re-index this item for all indexes supporting unpublished content + + ReIndexForContent(c4, false); + } + break; + case MessageType.RefreshAll: + case MessageType.RefreshByJson: + default: + //We don't support these for examine indexing + break; } } - private static void MemberAfterSave(Member sender, SaveEventArgs e) - { - //ensure that only the providers are flagged to listen execute - var xml = ExamineXmlExtensions.ToXElement(sender.ToXml(new System.Xml.XmlDocument(), false)); - var providers = ExamineManager.Instance.IndexProviderCollection.OfType() - .Where(x => x.EnableDefaultEventHandler); - ExamineManager.Instance.ReIndexNode(xml, IndexTypes.Member, providers); - } + /// + /// Handles index management for all unpublished content events - basically handling saving/copying/deleting + /// + /// + /// + /// + /// This will execute on all servers taking part in load balancing + /// + static void UnpublishedPageCacheRefresherCacheUpdated(UnpublishedPageCacheRefresher sender, CacheRefresherEventArgs e) + { + switch (e.MessageType) + { + case MessageType.RefreshById: + var c1 = ApplicationContext.Current.Services.ContentService.GetById((int) e.MessageObject); + if (c1 != null) + { + ReIndexForContent(c1, false); + } + break; + case MessageType.RemoveById: + + // This is triggered when the item is permanently deleted + + DeleteIndexForEntity((int)e.MessageObject, false); + break; + case MessageType.RefreshByInstance: + var c3 = e.MessageObject as IContent; + if (c3 != null) + { + ReIndexForContent(c3, false); + } + break; + case MessageType.RemoveByInstance: + + // This is triggered when the item is permanently deleted + + var c4 = e.MessageObject as IContent; + if (c4 != null) + { + DeleteIndexForEntity(c4.Id, false); + } + break; + case MessageType.RefreshAll: + case MessageType.RefreshByJson: + default: + //We don't support these, these message types will not fire for unpublished content + break; + } + } - private static void MemberAfterDelete(Member sender, DeleteEventArgs e) + private static void ReIndexForMember(IMember member) { - var nodeId = sender.Id.ToString(CultureInfo.InvariantCulture); - - //ensure that only the providers are flagged to listen execute - ExamineManager.Instance.DeleteFromIndex(nodeId, - ExamineManager.Instance.IndexProviderCollection.OfType() - .Where(x => x.EnableDefaultEventHandler)); - } - - /// - /// Only Update indexes for providers that dont SupportUnpublishedContent - /// - /// - /// - - private static void ContentAfterUpdateDocumentCache(Document sender, DocumentCacheEventArgs e) - { - //ensure that only the providers that have DONT unpublishing support enabled - //that are also flagged to listen - ExamineManager.Instance.ReIndexNode(ToXDocument(sender, true).Root, IndexTypes.Content, - ExamineManager.Instance.IndexProviderCollection.OfType() - .Where(x => !x.SupportUnpublishedContent - && x.EnableDefaultEventHandler)); - } - - /// - /// Only update indexes for providers that don't SupportUnpublishedContnet - /// - /// - /// - - private static void ContentAfterClearDocumentCache(Document sender, DocumentCacheEventArgs e) - { - var nodeId = sender.Id.ToString(); - //ensure that only the providers that DONT have unpublishing support enabled - //that are also flagged to listen - ExamineManager.Instance.DeleteFromIndex(nodeId, - ExamineManager.Instance.IndexProviderCollection.OfType() - .Where(x => !x.SupportUnpublishedContent - && x.EnableDefaultEventHandler)); + ExamineManager.Instance.ReIndexNode( + member.ToXml(), IndexTypes.Member, + ExamineManager.Instance.IndexProviderCollection.OfType() + //ensure that only the providers are flagged to listen execute + .Where(x => x.EnableDefaultEventHandler)); } /// @@ -238,28 +338,62 @@ namespace Umbraco.Web.Search } } - - private static void IndexMedia(IMedia sender) + [SecuritySafeCritical] + private static void ReIndexForMedia(IMedia sender, bool isMediaPublished) { ExamineManager.Instance.ReIndexNode( - sender.ToXml(), "media", - ExamineManager.Instance.IndexProviderCollection.OfType().Where(x => x.EnableDefaultEventHandler)); + sender.ToXml(), IndexTypes.Media, + ExamineManager.Instance.IndexProviderCollection.OfType() + + //Index this item for all indexers if the media is not trashed, otherwise if the item is trashed + // then only index this for indexers supporting unpublished media + + .Where(x => isMediaPublished || (x.SupportUnpublishedContent)) + .Where(x => x.EnableDefaultEventHandler)); } - private static void IndexConent(IContent sender) - { - //only index this content if the indexer supports unpublished content. that is because the - // content.AfterUpdateDocumentCache will handle anything being published and will only index against indexers - // that only support published content. - // NOTE: The events for publishing have changed slightly from 6.0 to 6.1 and are streamlined in 6.1. Before - // this event would fire before publishing, then again after publishing. Now the save event fires once before - // publishing and that is all. - - ExamineManager.Instance.ReIndexNode( - sender.ToXml(), "content", - ExamineManager.Instance.IndexProviderCollection.OfType() - .Where(x => x.SupportUnpublishedContent && x.EnableDefaultEventHandler)); - } + /// + /// Remove items from any index that doesn't support unpublished content + /// + /// + /// + /// If true, indicates that we will only delete this item from indexes that don't support unpublished content. + /// If false it will delete this from all indexes regardless. + /// + [SecuritySafeCritical] + private static void DeleteIndexForEntity(int entityId, bool keepIfUnpublished) + { + ExamineManager.Instance.DeleteFromIndex( + entityId.ToString(CultureInfo.InvariantCulture), + ExamineManager.Instance.IndexProviderCollection.OfType() + + //if keepIfUnpublished == true then only delete this item from indexes not supporting unpublished content, + // otherwise if keepIfUnpublished == false then remove from all indexes + + .Where(x => keepIfUnpublished == false || x.SupportUnpublishedContent == false) + .Where(x => x.EnableDefaultEventHandler)); + } + + /// + /// Re-indexes a content item whether published or not but only indexes them for indexes supporting unpublished content + /// + /// + /// + /// Value indicating whether the item is published or not + /// + [SecuritySafeCritical] + private static void ReIndexForContent(IContent sender, bool isContentPublished) + { + ExamineManager.Instance.ReIndexNode( + sender.ToXml(), IndexTypes.Content, + ExamineManager.Instance.IndexProviderCollection.OfType() + + //Index this item for all indexers if the content is published, otherwise if the item is not published + // then only index this for indexers supporting unpublished content + + .Where(x => isContentPublished || (x.SupportUnpublishedContent)) + .Where(x => x.EnableDefaultEventHandler)); + } /// /// Converts a content node to XDocument diff --git a/src/Umbraco.Web/Standalone/PowershellAssemblyResolver.cs b/src/Umbraco.Web/Standalone/PowershellAssemblyResolver.cs new file mode 100644 index 0000000000..63dd2ff252 --- /dev/null +++ b/src/Umbraco.Web/Standalone/PowershellAssemblyResolver.cs @@ -0,0 +1,41 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Reflection; + +namespace Umbraco.Web.Standalone +{ + internal static class PowershellAssemblyResolver + { + private static readonly Dictionary Assemblies; + private static readonly object Locko = new object(); + + static PowershellAssemblyResolver() + { + var comparer = StringComparer.CurrentCultureIgnoreCase; + Assemblies = new Dictionary(comparer); + AppDomain.CurrentDomain.AssemblyResolve += ResolveHandler; + } + + public static void AddAssemblyLocation(string path) + { + if (string.IsNullOrWhiteSpace(path)) + throw new ArgumentException("Arg is null or empty.", "path"); + + lock (Locko) + { + var name = Path.GetFileNameWithoutExtension(path); + Assemblies.Add(name, path); + } + } + + private static Assembly ResolveHandler(object sender, ResolveEventArgs args) + { + var assemblyName = new AssemblyName(args.Name); + string assemblyFile; + return Assemblies.TryGetValue(assemblyName.Name, out assemblyFile) + ? Assembly.LoadFrom(assemblyFile) + : null; + } + } +} diff --git a/src/Umbraco.Web/Standalone/StandaloneApplication.cs b/src/Umbraco.Web/Standalone/StandaloneApplication.cs index 64e2975dc2..e490684dc7 100644 --- a/src/Umbraco.Web/Standalone/StandaloneApplication.cs +++ b/src/Umbraco.Web/Standalone/StandaloneApplication.cs @@ -59,7 +59,15 @@ namespace Umbraco.Web.Standalone if (noerr) return; throw new InvalidOperationException("Application has already started."); } - Application_Start(this, EventArgs.Empty); + try + { + Application_Start(this, EventArgs.Empty); + } + catch + { + TerminateInternal(); + throw; + } _started = true; } } @@ -74,14 +82,24 @@ namespace Umbraco.Web.Standalone throw new InvalidOperationException("Application has already been terminated."); } + TerminateInternal(); + } + } + + private void TerminateInternal() + { + if (ApplicationContext.Current != null) + { ApplicationContext.Current.DisposeIfDisposable(); // should reset resolution, clear caches & resolvers... ApplicationContext.Current = null; + } + if (UmbracoContext.Current != null) + { UmbracoContext.Current.DisposeIfDisposable(); // dunno UmbracoContext.Current = null; - - _started = false; - _application = null; } + _started = false; + _application = null; } #endregion diff --git a/src/Umbraco.Web/Standalone/WriteableConfigSystem.cs b/src/Umbraco.Web/Standalone/WriteableConfigSystem.cs new file mode 100644 index 0000000000..2438256cce --- /dev/null +++ b/src/Umbraco.Web/Standalone/WriteableConfigSystem.cs @@ -0,0 +1,262 @@ +using System; +using System.Collections.Specialized; +using System.Configuration; +using System.Configuration.Internal; +using System.Reflection; +using System.Threading; + +namespace Umbraco.Web.Standalone +{ + // see http://stackoverflow.com/questions/15653621/how-to-update-add-modify-delete-keys-in-appsettings-section-of-web-config-at-r + // see http://www.codeproject.com/Articles/69364/Override-Configuration-Manager + + internal sealed class WriteableConfigSystem : IInternalConfigSystem + { + private static readonly ReaderWriterLockSlim RwLock = new ReaderWriterLockSlim(); + private static WriteableConfigSystem _installed; + private static IInternalConfigSystem _clientConfigSystem; + private object _appsettings; + private object _connectionStrings; + private static object _sInitStateOrig; + private static object _sConfigSystemOrig; + + public static bool Installed + { + get + { + try + { + RwLock.EnterReadLock(); + return _installed != null; + } + finally + { + RwLock.ExitReadLock(); + } + } + } + + /// + /// Re-initializes the ConfigurationManager, allowing us to merge in the settings from Core.Config + /// + public static void Install() + { + try + { + RwLock.EnterWriteLock(); + + if (_installed != null) + throw new InvalidOperationException("ConfigSystem is already installed."); + + FieldInfo[] fiStateValues = null; + var tInitState = typeof(ConfigurationManager).GetNestedType("InitState", BindingFlags.NonPublic); + + if (tInitState != null) + fiStateValues = tInitState.GetFields(); + // 0: NotStarted + // 1: Started + // 2: Usable + // 3: Completed + + var fiInit = typeof(ConfigurationManager).GetField("s_initState", BindingFlags.NonPublic | BindingFlags.Static); + var fiSystem = typeof(ConfigurationManager).GetField("s_configSystem", BindingFlags.NonPublic | BindingFlags.Static); + + if (fiInit != null && fiSystem != null && fiStateValues != null) + { + _sInitStateOrig = fiInit.GetValue(null); + _sConfigSystemOrig = fiSystem.GetValue(null); + fiInit.SetValue(null, fiStateValues[1].GetValue(null)); // set to Started + fiSystem.SetValue(null, null); // clear current config system + } + + _installed = new WriteableConfigSystem(); + + var configFactoryType = Type.GetType("System.Configuration.Internal.InternalConfigSettingsFactory, System.Configuration, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a", true); + var configSettingsFactory = (IInternalConfigSettingsFactory)Activator.CreateInstance(configFactoryType, true); + // just does ConfigurationManager.SetConfigurationSystem(_installed, false); + // 'false' turns initState to 2 ie usable (vs 3 ie completed) + configSettingsFactory.SetConfigurationSystem(_installed, false); + + // note: prob. don't need the factory... see how we uninstall... + + var clientConfigSystemType = Type.GetType("System.Configuration.ClientConfigurationSystem, System.Configuration, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a", true); + _clientConfigSystem = (IInternalConfigSystem)Activator.CreateInstance(clientConfigSystemType, true); + } + finally + { + RwLock.ExitWriteLock(); + } + } + + public static void Uninstall() + { + try + { + RwLock.EnterWriteLock(); + + if (_installed == null) + throw new InvalidOperationException("ConfigSystem is not installed."); + + FieldInfo[] fiStateValues = null; + var tInitState = typeof(ConfigurationManager).GetNestedType("InitState", BindingFlags.NonPublic); + + if (tInitState != null) + fiStateValues = tInitState.GetFields(); + + var fiInit = typeof(ConfigurationManager).GetField("s_initState", BindingFlags.NonPublic | BindingFlags.Static); + var fiSystem = typeof(ConfigurationManager).GetField("s_configSystem", BindingFlags.NonPublic | BindingFlags.Static); + + if (fiInit != null && fiSystem != null && fiStateValues != null) + { + // reset - the hard way + fiInit.SetValue(null, _sInitStateOrig); + fiSystem.SetValue(null, _sConfigSystemOrig); + } + + _installed = null; + _clientConfigSystem = null; + } + finally + { + RwLock.ExitWriteLock(); + } + } + + public static void Reset() + { + try + { + RwLock.EnterWriteLock(); + + if (_installed == null) + throw new InvalidOperationException("ConfigSystem is not installed."); + + _installed._appsettings = null; + _installed._connectionStrings = null; + } + finally + { + RwLock.ExitWriteLock(); + } + } + + #region IInternalConfigSystem Members + + public object GetSection(string configKey) + { + // get the section from the default location (web.config or app.config) + var section = _clientConfigSystem.GetSection(configKey); + + try + { + RwLock.EnterReadLock(); + + switch (configKey) + { + case "appSettings": + // Return cached version if exists + if (_appsettings != null) + return _appsettings; + + // create a new collection because the underlying collection is read-only + var cfg = new NameValueCollection(); + + // If an AppSettings section exists in Web.config, read and add values from it + var nvSection = section as NameValueCollection; + if (nvSection != null) + { + var localSettings = nvSection; + foreach (string key in localSettings) + cfg.Add(key, localSettings[key]); + } + + //// -------------------------------------------------------------------- + //// Here I read and decrypt keys and add them to secureConfig dictionary + //// To test assume the following line is a key stored in secure sotrage. + ////secureConfig = SecureConfig.LoadConfig(); + //secureConfig.Add("ACriticalKey", "VeryCriticalValue"); + //// -------------------------------------------------------------------- + //foreach (KeyValuePair item in secureConfig) + //{ + // if (cfg.AllKeys.Contains(item.Key)) + // { + // cfg[item.Key] = item.Value; + // } + // else + // { + // cfg.Add(item.Key, item.Value); + // } + //} + //// -------------------------------------------------------------------- + + + // Cach the settings for future use + + _appsettings = cfg; + // return the merged version of the items from secure storage and appsettings + section = _appsettings; + break; + + case "connectionStrings": + // Return cached version if exists + if (_connectionStrings != null) + return _connectionStrings; + + // create a new collection because the underlying collection is read-only + var connectionStringsSection = new ConnectionStringsSection(); + + // copy the existing connection strings into the new collection + foreach ( + ConnectionStringSettings connectionStringSetting in + ((ConnectionStringsSection)section).ConnectionStrings) + connectionStringsSection.ConnectionStrings.Add(connectionStringSetting); + + // -------------------------------------------------------------------- + // Again Load connection strings from secure storage and merge like below + // connectionStringsSection.ConnectionStrings.Add(connectionStringSetting); + // -------------------------------------------------------------------- + + // Cach the settings for future use + _connectionStrings = connectionStringsSection; + // return the merged version of the items from secure storage and appsettings + section = _connectionStrings; + break; + } + } + finally + { + RwLock.ExitReadLock(); + } + + return section; + } + + public void RefreshConfig(string sectionName) + { + try + { + RwLock.EnterWriteLock(); + + if (sectionName == "appSettings") + { + _appsettings = null; + } + + if (sectionName == "connectionStrings") + { + _connectionStrings = null; + } + } + finally + { + RwLock.ExitWriteLock(); + } + + _clientConfigSystem.RefreshConfig(sectionName); + } + + public bool SupportsUserConfig { get { return _clientConfigSystem.SupportsUserConfig; } } + + #endregion + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/Strategies/LegacyActionHandlerEventHandler.cs b/src/Umbraco.Web/Strategies/LegacyActionHandlerEventHandler.cs new file mode 100644 index 0000000000..4ab8b25849 --- /dev/null +++ b/src/Umbraco.Web/Strategies/LegacyActionHandlerEventHandler.cs @@ -0,0 +1,43 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using umbraco.BusinessLogic.Actions; +using umbraco.cms.businesslogic.web; +using Umbraco.Core; +using Umbraco.Core.Events; +using Umbraco.Core.Models; +using Umbraco.Core.Publishing; +using Umbraco.Core.Services; + +namespace Umbraco.Web.Strategies +{ + + /// + /// This is used to trigger the legacy ActionHandlers based on events + /// + public sealed class LegacyActionHandlerEventHandler : ApplicationEventHandler + { + //NOTE: this is to fix this currently: http://issues.umbraco.org/issue/U4-1550 + + protected override void ApplicationStarted(UmbracoApplicationBase umbracoApplication, ApplicationContext applicationContext) + { + ContentService.Published += ContentService_Published; + ContentService.UnPublished += ContentService_UnPublished; + } + + static void ContentService_UnPublished(IPublishingStrategy sender, PublishEventArgs e) + { + e.PublishedEntities.ForEach(x => + global::umbraco.BusinessLogic.Actions.Action.RunActionHandlers( + new Document(x), ActionUnPublish.Instance)); + } + + static void ContentService_Published(IPublishingStrategy sender, PublishEventArgs e) + { + e.PublishedEntities.ForEach(x => + global::umbraco.BusinessLogic.Actions.Action.RunActionHandlers( + new Document(x), ActionPublish.Instance)); + } + } +} diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index 359b6cbbbf..8aec82581e 100644 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -289,6 +289,7 @@ + @@ -341,6 +342,8 @@ + + @@ -584,6 +587,7 @@ + @@ -653,6 +657,8 @@ + + True True diff --git a/src/Umbraco.Web/umbraco.presentation/content.cs b/src/Umbraco.Web/umbraco.presentation/content.cs index c40ab99436..1a10045d75 100644 --- a/src/Umbraco.Web/umbraco.presentation/content.cs +++ b/src/Umbraco.Web/umbraco.presentation/content.cs @@ -522,7 +522,7 @@ namespace umbraco var cachedFieldKeyStart = string.Format("{0}{1}_", CacheKeys.ContentItemCacheKey, d.Id); ApplicationContext.Current.ApplicationCache.ClearCacheByKeySearch(cachedFieldKeyStart); - + FireAfterUpdateDocumentCache(d, e); } } @@ -531,6 +531,7 @@ namespace umbraco /// Updates the document cache for multiple documents /// /// The documents. + [Obsolete("This is not used and will be removed from the codebase in future versions")] public virtual void UpdateDocumentCache(List Documents) { // We need to lock content cache here, because we cannot allow other threads @@ -549,7 +550,6 @@ namespace umbraco XmlContentInternal = xmlContentCopy; ClearContextCache(); } - } /// diff --git a/src/Umbraco.Web/umbraco.presentation/umbraco/webservices/CacheRefresher.asmx.cs b/src/Umbraco.Web/umbraco.presentation/umbraco/webservices/CacheRefresher.asmx.cs index 7f79097615..4c2f721bd2 100644 --- a/src/Umbraco.Web/umbraco.presentation/umbraco/webservices/CacheRefresher.asmx.cs +++ b/src/Umbraco.Web/umbraco.presentation/umbraco/webservices/CacheRefresher.asmx.cs @@ -115,7 +115,7 @@ namespace umbraco.presentation.webservices { var xd = new XmlDocument(); xd.LoadXml(""); - foreach (var cr in CacheRefreshersResolver.Current.CacheResolvers) + foreach (var cr in CacheRefreshersResolver.Current.CacheRefreshers) { var n = xmlHelper.addTextNode(xd, "cacheRefresher", cr.Name); n.Attributes.Append(xmlHelper.addAttribute(xd, "uniqueIdentifier", cr.UniqueIdentifier.ToString())); diff --git a/src/umbraco.cms/businesslogic/CMSNode.cs b/src/umbraco.cms/businesslogic/CMSNode.cs index 406a95dd63..9b0712df0e 100644 --- a/src/umbraco.cms/businesslogic/CMSNode.cs +++ b/src/umbraco.cms/businesslogic/CMSNode.cs @@ -378,6 +378,19 @@ namespace umbraco.cms.businesslogic setupNode(); } + /// + /// This is purely for a hackity hack hack hack in order to make the new Document(id, version) constructor work because + /// the Version property needs to be set on the object before setupNode is called, otherwise it never works! this allows + /// inheritors to set default data before setupNode() is called. + /// + /// + /// + internal CMSNode(int id, object[] ctorArgs) + { + _id = id; + PreSetupNode(ctorArgs); + } + /// /// Initializes a new instance of the class. /// @@ -1017,6 +1030,18 @@ order by level,sortOrder"; _entity.Name = txt; } + /// + /// This is purely for a hackity hack hack hack in order to make the new Document(id, version) constructor work because + /// the Version property needs to be set on the object before setupNode is called, otherwise it never works! + /// + /// + internal virtual void PreSetupNode(params object[] ctorArgs) + { + //if people want to override then awesome but then we call setupNode so they need to ensure + // to call base.PreSetupNode + setupNode(); + } + /// /// Sets up the internal data of the CMSNode, used by the various constructors /// diff --git a/src/umbraco.cms/businesslogic/Content.cs b/src/umbraco.cms/businesslogic/Content.cs index ddd54b7d9c..f81ecce499 100644 --- a/src/umbraco.cms/businesslogic/Content.cs +++ b/src/umbraco.cms/businesslogic/Content.cs @@ -40,13 +40,18 @@ namespace umbraco.cms.businesslogic private bool _versionDateInitialized; private string _contentTypeIcon; private ContentType _contentType; - private Properties m_LoadedProperties = null; + private Properties _loadedProperties = null; protected internal IContentBase ContentBase; #endregion #region Constructors - + + protected internal Content(int id, Guid version) + : base(id, new object[] { version }) + { + } + public Content(int id) : base(id) { } protected Content(int id, bool noSetup) : base(id, noSetup) { } @@ -224,7 +229,7 @@ namespace umbraco.cms.businesslogic get { EnsureProperties(); - return m_LoadedProperties; + return _loadedProperties; } } @@ -237,7 +242,7 @@ namespace umbraco.cms.businesslogic get { EnsureProperties(); - return m_LoadedProperties.ToArray(); + return _loadedProperties.ToArray(); } } @@ -289,7 +294,7 @@ namespace umbraco.cms.businesslogic { EnsureProperties(); - return m_LoadedProperties.SingleOrDefault(x => x.PropertyType.Alias == alias); + return _loadedProperties.SingleOrDefault(x => x.PropertyType.Alias == alias); } /// @@ -301,7 +306,7 @@ namespace umbraco.cms.businesslogic { EnsureProperties(); - return m_LoadedProperties.SingleOrDefault(x => x.PropertyType.Id == pt.Id); + return _loadedProperties.SingleOrDefault(x => x.PropertyType.Id == pt.Id); } /// @@ -463,6 +468,21 @@ namespace umbraco.cms.businesslogic #region Protected Methods + /// + /// This is purely for a hackity hack hack hack in order to make the new Document(id, version) constructor work because + /// the Version property needs to be set on the object before setupNode is called, otherwise it never works! + /// + /// + internal override void PreSetupNode(params object[] ctorArgs) + { + //we know that there is one ctor arg and it is a GUID since we are only calling the base + // ctor with this overload for one purpose. + var version = (Guid) ctorArgs[0]; + _version = version; + + base.PreSetupNode(ctorArgs); + } + /// /// Sets up the ContentType property for this content item and sets the addition content properties manually. /// If the ContentType property is not already set, then this will get the ContentType from Cache. @@ -617,7 +637,7 @@ namespace umbraco.cms.businesslogic /// private void ClearLoadedProperties() { - m_LoadedProperties = null; + _loadedProperties = null; } /// @@ -625,7 +645,7 @@ namespace umbraco.cms.businesslogic /// private void EnsureProperties() { - if (m_LoadedProperties == null) + if (_loadedProperties == null) { InitializeProperties(); } @@ -644,13 +664,13 @@ namespace umbraco.cms.businesslogic /// private void InitializeProperties() { - m_LoadedProperties = new Properties(); + _loadedProperties = new Properties(); if (ContentBase != null) { //NOTE: we will not load any properties where HasIdentity = false - this is because if properties are // added to the property collection that aren't persisted we'll get ysods - m_LoadedProperties.AddRange(ContentBase.Properties.Where(x => x.HasIdentity).Select(x => new Property(x))); + _loadedProperties.AddRange(ContentBase.Properties.Where(x => x.HasIdentity).Select(x => new Property(x))); return; } @@ -659,7 +679,7 @@ namespace umbraco.cms.businesslogic //Create anonymous typed list with 2 props, Id and PropertyTypeId of type Int. //This will still be an empty list since the props list is empty. - var propData = m_LoadedProperties.Select(x => new { Id = 0, PropertyTypeId = 0 }).ToList(); + var propData = _loadedProperties.Select(x => new { Id = 0, PropertyTypeId = 0 }).ToList(); string sql = @"select id, propertyTypeId from cmsPropertyData where versionId=@versionId"; @@ -698,7 +718,7 @@ namespace umbraco.cms.businesslogic continue; //this remains from old code... not sure why we would do this? } - m_LoadedProperties.Add(p); + _loadedProperties.Add(p); } } diff --git a/src/umbraco.cms/businesslogic/Dictionary.cs b/src/umbraco.cms/businesslogic/Dictionary.cs index e524f90429..cd32162fa7 100644 --- a/src/umbraco.cms/businesslogic/Dictionary.cs +++ b/src/umbraco.cms/businesslogic/Dictionary.cs @@ -532,5 +532,24 @@ namespace umbraco.cms.businesslogic } #endregion } + + // zb023 - utility method + public static string ReplaceKey(string text) + { + if (text.StartsWith("#") == false) + return text; + + var lang = Language.GetByCultureCode(Thread.CurrentThread.CurrentCulture.Name); + + if (lang == null) + return "[" + text + "]"; + + if (DictionaryItem.hasKey(text.Substring(1, text.Length - 1)) == false) + return "[" + text + "]"; + + var di = new DictionaryItem(text.Substring(1, text.Length - 1)); + return di.Value(lang.id); + } + } } \ No newline at end of file diff --git a/src/umbraco.cms/businesslogic/web/Document.cs b/src/umbraco.cms/businesslogic/web/Document.cs index 892cb1f3a0..98e7db6647 100644 --- a/src/umbraco.cms/businesslogic/web/Document.cs +++ b/src/umbraco.cms/businesslogic/web/Document.cs @@ -56,9 +56,8 @@ namespace umbraco.cms.businesslogic.web /// The id of the document /// The version of the document public Document(int id, Guid Version) - : base(id) + : base(id, Version) { - this.Version = Version; } /// diff --git a/src/umbraco.cms/businesslogic/web/Domain.cs b/src/umbraco.cms/businesslogic/web/Domain.cs index 4547078673..8c3e630285 100644 --- a/src/umbraco.cms/businesslogic/web/Domain.cs +++ b/src/umbraco.cms/businesslogic/web/Domain.cs @@ -143,7 +143,7 @@ namespace umbraco.cms.businesslogic.web return GetDomains(false); } - internal static IEnumerable GetDomains(bool includeWildcards) + public static IEnumerable GetDomains(bool includeWildcards) { var domains = ApplicationContext.Current.ApplicationCache.GetCacheItem( CacheKeys.DomainCacheKey, @@ -191,6 +191,11 @@ namespace umbraco.cms.businesslogic.web return GetDomains().Where(d => d._root == nodeId).ToArray(); } + public static Domain[] GetDomainsById(int nodeId, bool includeWildcards) + { + return GetDomains(includeWildcards).Where(d => d._root == nodeId).ToArray(); + } + public static bool Exists(string DomainName) { return GetDomain(DomainName) != null; diff --git a/src/umbraco.editorControls/dropdownlist/dropdown.cs b/src/umbraco.editorControls/dropdownlist/dropdown.cs index b14ced4d05..6b302ef766 100644 --- a/src/umbraco.editorControls/dropdownlist/dropdown.cs +++ b/src/umbraco.editorControls/dropdownlist/dropdown.cs @@ -60,7 +60,7 @@ namespace umbraco.editorControls { foreach (object key in _prevalues.Keys) { - this.Items.Add(new ListItem(dropdown.DictionaryReplace(_prevalues[key].ToString()), key.ToString())); + this.Items.Add(new ListItem(Dictionary.ReplaceKey(_prevalues[key].ToString()), key.ToString())); } } @@ -68,7 +68,7 @@ namespace umbraco.editorControls { foreach (KeyValuePair item in Prevalues) { - this.Items.Add(new ListItem(dropdown.DictionaryReplace(item.Value), item.Key.ToString())); + this.Items.Add(new ListItem(Dictionary.ReplaceKey(item.Value), item.Key.ToString())); } } @@ -77,26 +77,5 @@ namespace umbraco.editorControls if (_data != null && _data.Value != null) this.SelectedValue = _data.Value.ToString(); } - - static string DictionaryReplace(string text) - { - if (!text.StartsWith("#")) - return text; - else - { - Language lang = Language.GetByCultureCode(System.Threading.Thread.CurrentThread.CurrentCulture.Name); - if (lang != null) - { - if (Dictionary.DictionaryItem.hasKey(text.Substring(1, text.Length - 1))) - { - Dictionary.DictionaryItem di = new Dictionary.DictionaryItem(text.Substring(1, text.Length - 1)); - return di.Value(lang.id); - } - } - - return "[" + text + "]"; - } - } - } } \ No newline at end of file