diff --git a/src/Umbraco.Core/EnumerableExtensions.cs b/src/Umbraco.Core/EnumerableExtensions.cs index d71ccb04b9..3719bb0750 100644 --- a/src/Umbraco.Core/EnumerableExtensions.cs +++ b/src/Umbraco.Core/EnumerableExtensions.cs @@ -10,6 +10,18 @@ namespace Umbraco.Core /// public static class EnumerableExtensions { + internal static bool HasDuplicates(this IEnumerable items, bool includeNull) + { + var hs = new HashSet(); + foreach (var item in items) + { + if ((item != null || includeNull) && !hs.Add(item)) + return true; + } + return false; + } + + /// /// Wraps this object instance into an IEnumerable{T} consisting of a single item. /// @@ -100,7 +112,7 @@ namespace Umbraco.Core } } - + /// /// Returns true if all items in the other collection exist in this collection /// diff --git a/src/Umbraco.Core/Migrations/Install/DatabaseDataCreator.cs b/src/Umbraco.Core/Migrations/Install/DatabaseDataCreator.cs index f0e6dd2e5b..1de983636b 100644 --- a/src/Umbraco.Core/Migrations/Install/DatabaseDataCreator.cs +++ b/src/Umbraco.Core/Migrations/Install/DatabaseDataCreator.cs @@ -226,6 +226,8 @@ namespace Umbraco.Core.Migrations.Install _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 32, UniqueId = 32.ToGuid(), DataTypeId = Constants.DataTypes.LabelDateTime, ContentTypeId = 1044, PropertyTypeGroupId = 11, Alias = Constants.Conventions.Member.LastLockoutDate, Name = Constants.Conventions.Member.LastLockoutDateLabel, SortOrder = 4, Mandatory = false, ValidationRegExp = null, Description = null, Variations = (byte) ContentVariation.Nothing }); _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 33, UniqueId = 33.ToGuid(), DataTypeId = Constants.DataTypes.LabelDateTime, ContentTypeId = 1044, PropertyTypeGroupId = 11, Alias = Constants.Conventions.Member.LastLoginDate, Name = Constants.Conventions.Member.LastLoginDateLabel, SortOrder = 5, Mandatory = false, ValidationRegExp = null, Description = null, Variations = (byte) ContentVariation.Nothing }); _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 34, UniqueId = 34.ToGuid(), DataTypeId = Constants.DataTypes.LabelDateTime, ContentTypeId = 1044, PropertyTypeGroupId = 11, Alias = Constants.Conventions.Member.LastPasswordChangeDate, Name = Constants.Conventions.Member.LastPasswordChangeDateLabel, SortOrder = 6, Mandatory = false, ValidationRegExp = null, Description = null, Variations = (byte) ContentVariation.Nothing }); + _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 35, UniqueId = 35.ToGuid(), DataTypeId = Constants.DataTypes.LabelDateTime, ContentTypeId = 1044, PropertyTypeGroupId = null, Alias = Constants.Conventions.Member.PasswordQuestion, Name = Constants.Conventions.Member.PasswordQuestionLabel, SortOrder = 7, Mandatory = false, ValidationRegExp = null, Description = null, Variations = (byte)ContentVariation.Nothing }); + _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 36, UniqueId = 36.ToGuid(), DataTypeId = Constants.DataTypes.LabelDateTime, ContentTypeId = 1044, PropertyTypeGroupId = null, Alias = Constants.Conventions.Member.PasswordAnswer, Name = Constants.Conventions.Member.PasswordAnswerLabel, SortOrder = 8, Mandatory = false, ValidationRegExp = null, Description = null, Variations = (byte)ContentVariation.Nothing }); } diff --git a/src/Umbraco.Core/Models/ContentTypeBase.cs b/src/Umbraco.Core/Models/ContentTypeBase.cs index d4e35cccdf..04bcb7424a 100644 --- a/src/Umbraco.Core/Models/ContentTypeBase.cs +++ b/src/Umbraco.Core/Models/ContentTypeBase.cs @@ -95,6 +95,21 @@ namespace Umbraco.Core.Models protected void PropertyTypesChanged(object sender, NotifyCollectionChangedEventArgs e) { + //enable this to detect duplicate property aliases. We do want this, however making this change in a + //patch release might be a little dangerous + + ////detect if there are any duplicate aliases - this cannot be allowed + //if (e.Action == NotifyCollectionChangedAction.Add + // || e.Action == NotifyCollectionChangedAction.Replace) + //{ + // var allAliases = _noGroupPropertyTypes.Concat(PropertyGroups.SelectMany(x => x.PropertyTypes)).Select(x => x.Alias); + // if (allAliases.HasDuplicates(false)) + // { + // var newAliases = string.Join(", ", e.NewItems.Cast().Select(x => x.Alias)); + // throw new InvalidOperationException($"Other property types already exist with the aliases: {newAliases}"); + // } + //} + OnPropertyChanged(nameof(PropertyTypes)); } @@ -388,15 +403,16 @@ namespace Umbraco.Core.Models var group = PropertyGroups[propertyGroupName]; if (group == null) return; - // re-assign the group's properties to no group + // first remove the group + PropertyGroups.RemoveItem(propertyGroupName); + + // Then re-assign the group's properties to no group foreach (var property in group.PropertyTypes) { property.PropertyGroupId = null; _noGroupPropertyTypes.Add(property); } - // actually remove the group - PropertyGroups.RemoveItem(propertyGroupName); OnPropertyChanged(nameof(PropertyGroups)); } diff --git a/src/Umbraco.Core/Models/PropertyCollection.cs b/src/Umbraco.Core/Models/PropertyCollection.cs index 977600a2f7..c587a45424 100644 --- a/src/Umbraco.Core/Models/PropertyCollection.cs +++ b/src/Umbraco.Core/Models/PropertyCollection.cs @@ -15,7 +15,7 @@ namespace Umbraco.Core.Models public class PropertyCollection : KeyedCollection, INotifyCollectionChanged, IDeepCloneable { private readonly object _addLocker = new object(); - internal Action OnAdd; + internal Func AdditionValidator { get; set; } /// @@ -49,10 +49,12 @@ namespace Umbraco.Core.Models /// internal void Reset(IEnumerable properties) { + //collection events will be raised in each of these calls Clear(); + + //collection events will be raised in each of these calls foreach (var property in properties) Add(property); - OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); } /// @@ -60,8 +62,9 @@ namespace Umbraco.Core.Models /// protected override void SetItem(int index, Property property) { + var oldItem = index >= 0 ? this[index] : property; base.SetItem(index, property); - OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, property, index)); + OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Replace, property, oldItem)); } /// @@ -120,10 +123,8 @@ namespace Umbraco.Core.Models } } + //collection events will be raised in InsertItem with Add base.Add(property); - - OnAdd?.Invoke(); - OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, property)); } } diff --git a/src/Umbraco.Core/Models/PropertyGroupCollection.cs b/src/Umbraco.Core/Models/PropertyGroupCollection.cs index 26e0fef178..5422dfb792 100644 --- a/src/Umbraco.Core/Models/PropertyGroupCollection.cs +++ b/src/Umbraco.Core/Models/PropertyGroupCollection.cs @@ -19,9 +19,6 @@ namespace Umbraco.Core.Models { private readonly ReaderWriterLockSlim _addLocker = new ReaderWriterLockSlim(); - // TODO: this doesn't seem to be used anywhere - internal Action OnAdd; - internal PropertyGroupCollection() { } @@ -37,16 +34,19 @@ namespace Umbraco.Core.Models /// internal void Reset(IEnumerable groups) { + //collection events will be raised in each of these calls Clear(); + + //collection events will be raised in each of these calls foreach (var group in groups) Add(group); - OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); } protected override void SetItem(int index, PropertyGroup item) { + var oldItem = index >= 0 ? this[index] : item; base.SetItem(index, item); - OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, item, index)); + OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Replace, item, oldItem)); } protected override void RemoveItem(int index) @@ -84,6 +84,7 @@ namespace Umbraco.Core.Models if (keyExists) throw new Exception($"Naming conflict: Changing the name of PropertyGroup '{item.Name}' would result in duplicates"); + //collection events will be raised in SetItem SetItem(IndexOfKey(item.Id), item); return; } @@ -96,16 +97,14 @@ namespace Umbraco.Core.Models var exists = Contains(key); if (exists) { + //collection events will be raised in SetItem SetItem(IndexOfKey(key), item); return; } } } - + //collection events will be raised in InsertItem base.Add(item); - OnAdd?.Invoke(); - - OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, item)); } finally { diff --git a/src/Umbraco.Core/Models/PropertyTypeCollection.cs b/src/Umbraco.Core/Models/PropertyTypeCollection.cs index 6181ee078b..6e41f0d12b 100644 --- a/src/Umbraco.Core/Models/PropertyTypeCollection.cs +++ b/src/Umbraco.Core/Models/PropertyTypeCollection.cs @@ -19,9 +19,6 @@ namespace Umbraco.Core.Models [IgnoreDataMember] private readonly ReaderWriterLockSlim _addLocker = new ReaderWriterLockSlim(); - // TODO: This doesn't seem to be used - [IgnoreDataMember] - internal Action OnAdd; internal PropertyTypeCollection(bool supportsPublishing) { @@ -43,36 +40,44 @@ namespace Umbraco.Core.Models /// internal void Reset(IEnumerable properties) { + //collection events will be raised in each of these calls Clear(); + + //collection events will be raised in each of these calls foreach (var property in properties) - Add(property); - OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); + Add(property); } protected override void SetItem(int index, PropertyType item) { item.SupportsPublishing = SupportsPublishing; - base.SetItem(index, item); - OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, item, index)); + var oldItem = index >= 0 ? this[index] : item; + base.SetItem(index, item); + OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Replace, item, oldItem)); + item.PropertyChanged += Item_PropertyChanged; } protected override void RemoveItem(int index) { var removed = this[index]; base.RemoveItem(index); + removed.PropertyChanged -= Item_PropertyChanged; OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, removed)); } protected override void InsertItem(int index, PropertyType item) { item.SupportsPublishing = SupportsPublishing; - base.InsertItem(index, item); + base.InsertItem(index, item); OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, item)); + item.PropertyChanged += Item_PropertyChanged; } protected override void ClearItems() { base.ClearItems(); + foreach (var item in this) + item.PropertyChanged -= Item_PropertyChanged; OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); } @@ -91,6 +96,7 @@ namespace Umbraco.Core.Models var exists = Contains(key); if (exists) { + //collection events will be raised in SetItem SetItem(IndexOfKey(key), item); return; } @@ -103,10 +109,8 @@ namespace Umbraco.Core.Models item.SortOrder = this.Max(x => x.SortOrder) + 1; } + //collection events will be raised in InsertItem base.Add(item); - OnAdd?.Invoke(); - - OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, item)); } finally { @@ -115,6 +119,17 @@ namespace Umbraco.Core.Models } } + /// + /// Occurs when a property changes on a PropertyType that exists in this collection + /// + /// + /// + private void Item_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e) + { + var propType = (PropertyType)sender; + OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Replace, propType, propType)); + } + /// /// Determines whether this collection contains a whose alias matches the specified PropertyType. /// diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/ContentTypeCommonRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/ContentTypeCommonRepository.cs index 194a00b7f2..645ab9f924 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/ContentTypeCommonRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/ContentTypeCommonRepository.cs @@ -240,6 +240,25 @@ namespace Umbraco.Core.Persistence.Repositories.Implement propertyIx++; } contentType.NoGroupPropertyTypes = noGroupPropertyTypes; + + // ensure builtin properties + if (contentType is MemberType memberType) + { + // ensure that the group exists (ok if it already exists) + memberType.AddPropertyGroup(Constants.Conventions.Member.StandardPropertiesGroupName); + + // ensure that property types exist (ok if they already exist) + foreach (var (alias, propertyType) in builtinProperties) + { + var added = memberType.AddPropertyType(propertyType, Constants.Conventions.Member.StandardPropertiesGroupName); + + if (added) + { + var access = new MemberTypePropertyProfileAccess(false, false, false); + memberType.MemberTypePropertyTypes[alias] = access; + } + } + } } } @@ -264,7 +283,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement if (contentType is MemberType memberType) { var access = new MemberTypePropertyProfileAccess(dto.ViewOnProfile, dto.CanEdit, dto.IsSensitive); - memberType.MemberTypePropertyTypes.Add(dto.Alias, access); + memberType.MemberTypePropertyTypes[dto.Alias] = access; } return new PropertyType(dto.DataTypeDto.EditorAlias, storageType, readonlyStorageType, dto.Alias) diff --git a/src/Umbraco.Core/PropertyEditors/ConfigurationEditor.cs b/src/Umbraco.Core/PropertyEditors/ConfigurationEditor.cs index bb58a8ef72..8151753a43 100644 --- a/src/Umbraco.Core/PropertyEditors/ConfigurationEditor.cs +++ b/src/Umbraco.Core/PropertyEditors/ConfigurationEditor.cs @@ -13,12 +13,15 @@ namespace Umbraco.Core.PropertyEditors /// public class ConfigurationEditor : IConfigurationEditor { + private IDictionary _defaultConfiguration; + /// /// Initializes a new instance of the class. /// public ConfigurationEditor() { Fields = new List(); + _defaultConfiguration = new Dictionary(); } /// @@ -61,7 +64,10 @@ namespace Umbraco.Core.PropertyEditors /// [JsonProperty("defaultConfig")] - public virtual IDictionary DefaultConfiguration => new Dictionary(); + public virtual IDictionary DefaultConfiguration { + get => _defaultConfiguration; + internal set => _defaultConfiguration = value; + } /// public virtual object DefaultConfigurationObject => DefaultConfiguration; diff --git a/src/Umbraco.Core/PropertyEditors/DataEditor.cs b/src/Umbraco.Core/PropertyEditors/DataEditor.cs index ae6ace996e..43f4b68b99 100644 --- a/src/Umbraco.Core/PropertyEditors/DataEditor.cs +++ b/src/Umbraco.Core/PropertyEditors/DataEditor.cs @@ -173,7 +173,13 @@ namespace Umbraco.Core.PropertyEditors /// protected virtual IConfigurationEditor CreateConfigurationEditor() { - return new ConfigurationEditor(); + var editor = new ConfigurationEditor(); + // pass the default configuration if this is not a property value editor + if((Type & EditorType.PropertyValue) == 0) + { + editor.DefaultConfiguration = _defaultConfiguration; + } + return editor; } /// diff --git a/src/Umbraco.Core/PropertyEditors/SliderConfiguration.cs b/src/Umbraco.Core/PropertyEditors/SliderConfiguration.cs index 55ca199121..dc71c2a48f 100644 --- a/src/Umbraco.Core/PropertyEditors/SliderConfiguration.cs +++ b/src/Umbraco.Core/PropertyEditors/SliderConfiguration.cs @@ -9,18 +9,18 @@ public bool EnableRange { get; set; } [ConfigurationField("initVal1", "Initial value", "number")] - public int InitialValue { get; set; } + public decimal InitialValue { get; set; } [ConfigurationField("initVal2", "Initial value 2", "number", Description = "Used when range is enabled")] - public int InitialValue2 { get; set; } + public decimal InitialValue2 { get; set; } [ConfigurationField("minVal", "Minimum value", "number")] - public int MinimumValue { get; set; } + public decimal MinimumValue { get; set; } [ConfigurationField("maxVal", "Maximum value", "number")] - public int MaximumValue { get; set; } + public decimal MaximumValue { get; set; } [ConfigurationField("step", "Step increments", "number")] - public int StepIncrements { get; set; } + public decimal StepIncrements { get; set; } } } diff --git a/src/Umbraco.Tests/Cache/DistributedCacheBinderTests.cs b/src/Umbraco.Tests/Cache/DistributedCacheBinderTests.cs index c092306473..e446e049b6 100644 --- a/src/Umbraco.Tests/Cache/DistributedCacheBinderTests.cs +++ b/src/Umbraco.Tests/Cache/DistributedCacheBinderTests.cs @@ -159,6 +159,7 @@ namespace Umbraco.Tests.Cache TestObjects.GetUmbracoSettings(), TestObjects.GetGlobalSettings(), new UrlProviderCollection(Enumerable.Empty()), + new MediaUrlProviderCollection(Enumerable.Empty()), Mock.Of()); // just assert it does not throw diff --git a/src/Umbraco.Tests/Cache/PublishedCache/PublishedContentCacheTests.cs b/src/Umbraco.Tests/Cache/PublishedCache/PublishedContentCacheTests.cs index 11fb55f9b3..bb46b369c5 100644 --- a/src/Umbraco.Tests/Cache/PublishedCache/PublishedContentCacheTests.cs +++ b/src/Umbraco.Tests/Cache/PublishedCache/PublishedContentCacheTests.cs @@ -81,6 +81,7 @@ namespace Umbraco.Tests.Cache.PublishedCache new WebSecurity(_httpContextFactory.HttpContext, Current.Services.UserService, globalSettings), umbracoSettings, Enumerable.Empty(), + Enumerable.Empty(), globalSettings, new TestVariationContextAccessor()); diff --git a/src/Umbraco.Tests/Composing/TypeLoaderTests.cs b/src/Umbraco.Tests/Composing/TypeLoaderTests.cs index 7b7574ce47..7459ae848b 100644 --- a/src/Umbraco.Tests/Composing/TypeLoaderTests.cs +++ b/src/Umbraco.Tests/Composing/TypeLoaderTests.cs @@ -27,10 +27,7 @@ namespace Umbraco.Tests.Composing public void Initialize() { // this ensures it's reset - _typeLoader = new TypeLoader(NoAppCache.Instance, IOHelper.MapPath("~/App_Data/TEMP"), new ProfilingLogger(Mock.Of(), Mock.Of())); - - foreach (var file in Directory.GetFiles(IOHelper.MapPath(SystemDirectories.TempData.EnsureEndsWith('/') + "TypesCache"))) - File.Delete(file); + _typeLoader = new TypeLoader(NoAppCache.Instance, IOHelper.MapPath("~/App_Data/TEMP"), new ProfilingLogger(Mock.Of(), Mock.Of()), false); // for testing, we'll specify which assemblies are scanned for the PluginTypeResolver // TODO: Should probably update this so it only searches this assembly and add custom types to be found diff --git a/src/Umbraco.Tests/Models/ContentTypeTests.cs b/src/Umbraco.Tests/Models/ContentTypeTests.cs index d9e65ba6c6..9c3b976bf3 100644 --- a/src/Umbraco.Tests/Models/ContentTypeTests.cs +++ b/src/Umbraco.Tests/Models/ContentTypeTests.cs @@ -15,13 +15,47 @@ namespace Umbraco.Tests.Models [TestFixture] public class ContentTypeTests : UmbracoTestBase { + [Test] + [Ignore("Ignoring this test until we actually enforce this, see comments in ContentTypeBase.PropertyTypesChanged")] + public void Cannot_Add_Duplicate_Property_Aliases() + { + var contentType = MockedContentTypes.CreateBasicContentType(); + contentType.PropertyGroups.Add(new PropertyGroup(new PropertyTypeCollection(false, new[] + { + new PropertyType("testPropertyEditor", ValueStorageType.Nvarchar){ Alias = "myPropertyType" } + }))); + + Assert.Throws(() => + contentType.PropertyTypeCollection.Add( + new PropertyType("testPropertyEditor", ValueStorageType.Nvarchar) { Alias = "myPropertyType" })); + + } + + [Test] + [Ignore("Ignoring this test until we actually enforce this, see comments in ContentTypeBase.PropertyTypesChanged")] + public void Cannot_Update_Duplicate_Property_Aliases() + { + var contentType = MockedContentTypes.CreateBasicContentType(); + + contentType.PropertyGroups.Add(new PropertyGroup(new PropertyTypeCollection(false, new[] + { + new PropertyType("testPropertyEditor", ValueStorageType.Nvarchar){ Alias = "myPropertyType" } + }))); + + contentType.PropertyTypeCollection.Add(new PropertyType("testPropertyEditor", ValueStorageType.Nvarchar) { Alias = "myPropertyType2" }); + + var toUpdate = contentType.PropertyTypeCollection["myPropertyType2"]; + + Assert.Throws(() => toUpdate.Alias = "myPropertyType"); + + } [Test] public void Can_Deep_Clone_Content_Type_Sort() { var contentType = new ContentTypeSort(new Lazy(() => 3), 4, "test"); - var clone = (ContentTypeSort) contentType.DeepClone(); + var clone = (ContentTypeSort)contentType.DeepClone(); Assert.AreNotSame(clone, contentType); Assert.AreEqual(clone, contentType); Assert.AreEqual(clone.Id.Value, contentType.Id.Value); @@ -54,7 +88,7 @@ namespace Umbraco.Tests.Models contentType.Id = 10; contentType.CreateDate = DateTime.Now; contentType.CreatorId = 22; - contentType.SetDefaultTemplate(new Template((string) "Test Template", (string) "testTemplate") + contentType.SetDefaultTemplate(new Template((string)"Test Template", (string)"testTemplate") { Id = 88 }); @@ -117,12 +151,12 @@ namespace Umbraco.Tests.Models { group.Id = ++i; } - contentType.AllowedTemplates = new[] { new Template((string) "Name", (string) "name") { Id = 200 }, new Template((string) "Name2", (string) "name2") { Id = 201 } }; + contentType.AllowedTemplates = new[] { new Template((string)"Name", (string)"name") { Id = 200 }, new Template((string)"Name2", (string)"name2") { Id = 201 } }; contentType.AllowedContentTypes = new[] { new ContentTypeSort(new Lazy(() => 888), 8, "sub"), new ContentTypeSort(new Lazy(() => 889), 9, "sub2") }; contentType.Id = 10; contentType.CreateDate = DateTime.Now; contentType.CreatorId = 22; - contentType.SetDefaultTemplate(new Template((string) "Test Template", (string) "testTemplate") + contentType.SetDefaultTemplate(new Template((string)"Test Template", (string)"testTemplate") { Id = 88 }); @@ -167,12 +201,12 @@ namespace Umbraco.Tests.Models { group.Id = ++i; } - contentType.AllowedTemplates = new[] { new Template((string) "Name", (string) "name") { Id = 200 }, new Template((string) "Name2", (string) "name2") { Id = 201 } }; - contentType.AllowedContentTypes = new[] {new ContentTypeSort(new Lazy(() => 888), 8, "sub"), new ContentTypeSort(new Lazy(() => 889), 9, "sub2")}; + contentType.AllowedTemplates = new[] { new Template((string)"Name", (string)"name") { Id = 200 }, new Template((string)"Name2", (string)"name2") { Id = 201 } }; + contentType.AllowedContentTypes = new[] { new ContentTypeSort(new Lazy(() => 888), 8, "sub"), new ContentTypeSort(new Lazy(() => 889), 9, "sub2") }; contentType.Id = 10; contentType.CreateDate = DateTime.Now; contentType.CreatorId = 22; - contentType.SetDefaultTemplate(new Template((string) "Test Template", (string) "testTemplate") + contentType.SetDefaultTemplate(new Template((string)"Test Template", (string)"testTemplate") { Id = 88 }); @@ -264,12 +298,12 @@ namespace Umbraco.Tests.Models { propertyType.Id = ++i; } - contentType.AllowedTemplates = new[] { new Template((string) "Name", (string) "name") { Id = 200 }, new Template((string) "Name2", (string) "name2") { Id = 201 } }; + contentType.AllowedTemplates = new[] { new Template((string)"Name", (string)"name") { Id = 200 }, new Template((string)"Name2", (string)"name2") { Id = 201 } }; contentType.AllowedContentTypes = new[] { new ContentTypeSort(new Lazy(() => 888), 8, "sub"), new ContentTypeSort(new Lazy(() => 889), 9, "sub2") }; contentType.Id = 10; contentType.CreateDate = DateTime.Now; contentType.CreatorId = 22; - contentType.SetDefaultTemplate(new Template((string) "Test Template", (string) "testTemplate") + contentType.SetDefaultTemplate(new Template((string)"Test Template", (string)"testTemplate") { Id = 88 }); diff --git a/src/Umbraco.Tests/Persistence/Repositories/MemberTypeRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/MemberTypeRepositoryTest.cs index 2b3ab50a22..79e8e43804 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/MemberTypeRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/MemberTypeRepositoryTest.cs @@ -171,7 +171,6 @@ namespace Umbraco.Tests.Persistence.Repositories } } - //NOTE: This tests for left join logic (rev 7b14e8eacc65f82d4f184ef46c23340c09569052) [Test] public void Can_Get_All_Members_When_No_Properties_Assigned() @@ -200,7 +199,6 @@ namespace Umbraco.Tests.Persistence.Repositories } } - [Test] public void Can_Get_Member_Type_By_Id() { @@ -233,22 +231,114 @@ namespace Umbraco.Tests.Persistence.Repositories } } + // See: https://github.com/umbraco/Umbraco-CMS/issues/4963#issuecomment-483516698 + [Test] + public void Bug_Changing_Built_In_Member_Type_Property_Type_Aliases_Results_In_Exception() + { + var stubs = Constants.Conventions.Member.GetStandardPropertyTypeStubs(); + + var provider = TestObjects.GetScopeProvider(Logger); + using (provider.CreateScope()) + { + var repository = CreateRepository(provider); + + IMemberType memberType = MockedContentTypes.CreateSimpleMemberType("mtype"); + + // created without the stub properties + Assert.AreEqual(1, memberType.PropertyGroups.Count); + Assert.AreEqual(3, memberType.PropertyTypes.Count()); + + // saving *new* member type adds the stub properties + repository.Save(memberType); + + // saving has added (and saved) the stub properties + Assert.AreEqual(2, memberType.PropertyGroups.Count); + Assert.AreEqual(3 + stubs.Count, memberType.PropertyTypes.Count()); + + foreach (var stub in stubs) + { + var prop = memberType.PropertyTypes.First(x => x.Alias == stub.Key); + prop.Alias = prop.Alias + "__0000"; + } + + // saving *existing* member type does *not* ensure stub properties + repository.Save(memberType); + + // therefore, nothing has changed + Assert.AreEqual(2, memberType.PropertyGroups.Count); + Assert.AreEqual(3 + stubs.Count, memberType.PropertyTypes.Count()); + + // fetching ensures that the stub properties are there + memberType = repository.Get("mtype"); + Assert.IsNotNull(memberType); + + Assert.AreEqual(2, memberType.PropertyGroups.Count); + Assert.AreEqual(3 + stubs.Count * 2, memberType.PropertyTypes.Count()); + } + } + [Test] public void Built_In_Member_Type_Properties_Are_Automatically_Added_When_Creating() { + var stubs = Constants.Conventions.Member.GetStandardPropertyTypeStubs(); + var provider = TestObjects.GetScopeProvider(Logger); - using (var scope = provider.CreateScope()) + using (provider.CreateScope()) { var repository = CreateRepository(provider); IMemberType memberType = MockedContentTypes.CreateSimpleMemberType(); + + // created without the stub properties + Assert.AreEqual(1, memberType.PropertyGroups.Count); + Assert.AreEqual(3, memberType.PropertyTypes.Count()); + + // saving *new* member type adds the stub properties repository.Save(memberType); + // saving has added (and saved) the stub properties + Assert.AreEqual(2, memberType.PropertyGroups.Count); + Assert.AreEqual(3 + stubs.Count, memberType.PropertyTypes.Count()); + // getting with stub properties memberType = repository.Get(memberType.Id); - Assert.That(memberType.PropertyTypes.Count(), Is.EqualTo(3 + Constants.Conventions.Member.GetStandardPropertyTypeStubs().Count)); - Assert.That(memberType.PropertyGroups.Count(), Is.EqualTo(2)); + Assert.AreEqual(2, memberType.PropertyGroups.Count); + Assert.AreEqual(3 + stubs.Count, memberType.PropertyTypes.Count()); + } + } + + [Test] + public void Built_In_Member_Type_Properties_Missing_Are_Automatically_Added_When_Creating() + { + var stubs = Constants.Conventions.Member.GetStandardPropertyTypeStubs(); + + var provider = TestObjects.GetScopeProvider(Logger); + using (provider.CreateScope()) + { + var repository = CreateRepository(provider); + + IMemberType memberType = MockedContentTypes.CreateSimpleMemberType(); + + // created without the stub properties + Assert.AreEqual(1, memberType.PropertyGroups.Count); + Assert.AreEqual(3, memberType.PropertyTypes.Count()); + + // add one stub property, others are still missing + memberType.AddPropertyType(stubs.First().Value, Constants.Conventions.Member.StandardPropertiesGroupName); + + // saving *new* member type adds the (missing) stub properties + repository.Save(memberType); + + // saving has added (and saved) the (missing) stub properties + Assert.AreEqual(2, memberType.PropertyGroups.Count); + Assert.AreEqual(3 + stubs.Count, memberType.PropertyTypes.Count()); + + // getting with stub properties + memberType = repository.Get(memberType.Id); + + Assert.AreEqual(2, memberType.PropertyGroups.Count); + Assert.AreEqual(3 + stubs.Count, memberType.PropertyTypes.Count()); } } diff --git a/src/Umbraco.Tests/PublishedContent/PublishedContentSnapshotTestBase.cs b/src/Umbraco.Tests/PublishedContent/PublishedContentSnapshotTestBase.cs index 7a9a882baa..7e6ae75356 100644 --- a/src/Umbraco.Tests/PublishedContent/PublishedContentSnapshotTestBase.cs +++ b/src/Umbraco.Tests/PublishedContent/PublishedContentSnapshotTestBase.cs @@ -71,6 +71,7 @@ namespace Umbraco.Tests.PublishedContent new WebSecurity(httpContext, Current.Services.UserService, globalSettings), TestObjects.GetUmbracoSettings(), Enumerable.Empty(), + Enumerable.Empty(), globalSettings, new TestVariationContextAccessor()); diff --git a/src/Umbraco.Tests/Routing/MediaUrlProviderTests.cs b/src/Umbraco.Tests/Routing/MediaUrlProviderTests.cs new file mode 100644 index 0000000000..5de99fdd38 --- /dev/null +++ b/src/Umbraco.Tests/Routing/MediaUrlProviderTests.cs @@ -0,0 +1,152 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Moq; +using Newtonsoft.Json; +using NUnit.Framework; +using Umbraco.Core; +using Umbraco.Core.Models; +using Umbraco.Core.Models.PublishedContent; +using Umbraco.Core.PropertyEditors; +using Umbraco.Core.PropertyEditors.ValueConverters; +using Umbraco.Tests.PublishedContent; +using Umbraco.Tests.TestHelpers; +using Umbraco.Tests.TestHelpers.Stubs; +using Umbraco.Tests.Testing; +using Umbraco.Web.Routing; + +namespace Umbraco.Tests.Routing +{ + [TestFixture] + [UmbracoTest(Database = UmbracoTestOptions.Database.NewSchemaPerFixture)] + public class MediaUrlProviderTests : BaseWebTest + { + private DefaultMediaUrlProvider _mediaUrlProvider; + + public override void SetUp() + { + base.SetUp(); + + _mediaUrlProvider = new DefaultMediaUrlProvider(); + } + + public override void TearDown() + { + base.TearDown(); + + _mediaUrlProvider = null; + } + + [Test] + public void Get_Media_Url_Resolves_Url_From_Upload_Property_Editor() + { + const string expected = "/media/rfeiw584/test.jpg"; + + var umbracoContext = GetUmbracoContext("/", mediaUrlProviders: new[] { _mediaUrlProvider }); + var publishedContent = CreatePublishedContent(Constants.PropertyEditors.Aliases.UploadField, expected, null); + + var resolvedUrl = umbracoContext.UrlProvider.GetMediaUrl(publishedContent, "umbracoFile", UrlProviderMode.Auto, null, null); + + Assert.AreEqual(expected, resolvedUrl); + } + + [Test] + public void Get_Media_Url_Resolves_Url_From_Image_Cropper_Property_Editor() + { + const string expected = "/media/rfeiw584/test.jpg"; + + var configuration = new ImageCropperConfiguration(); + var imageCropperValue = JsonConvert.SerializeObject(new ImageCropperValue + { + Src = expected + }); + + var umbracoContext = GetUmbracoContext("/", mediaUrlProviders: new[] { _mediaUrlProvider }); + var publishedContent = CreatePublishedContent(Constants.PropertyEditors.Aliases.ImageCropper, imageCropperValue, configuration); + + var resolvedUrl = umbracoContext.UrlProvider.GetMediaUrl(publishedContent, "umbracoFile", UrlProviderMode.Auto, null, null); + + Assert.AreEqual(expected, resolvedUrl); + } + + [Test] + public void Get_Media_Url_Can_Resolve_Absolute_Url() + { + const string mediaUrl = "/media/rfeiw584/test.jpg"; + var expected = $"http://localhost{mediaUrl}"; + + var umbracoContext = GetUmbracoContext("http://localhost", mediaUrlProviders: new[] { _mediaUrlProvider }); + var publishedContent = CreatePublishedContent(Constants.PropertyEditors.Aliases.UploadField, mediaUrl, null); + + var resolvedUrl = umbracoContext.UrlProvider.GetMediaUrl(publishedContent, "umbracoFile", UrlProviderMode.Absolute, null, null); + + Assert.AreEqual(expected, resolvedUrl); + } + + [Test] + public void Get_Media_Url_Returns_Empty_String_When_PropertyType_Is_Not_Supported() + { + var umbracoContext = GetUmbracoContext("/", mediaUrlProviders: new[] { _mediaUrlProvider }); + var publishedContent = CreatePublishedContent(Constants.PropertyEditors.Aliases.Boolean, "0", null); + + var resolvedUrl = umbracoContext.UrlProvider.GetMediaUrl(publishedContent, "test", UrlProviderMode.Absolute, null, null); + + Assert.AreEqual(string.Empty, resolvedUrl); + } + + [Test] + public void Get_Media_Url_Can_Resolve_Variant_Property_Url() + { + var umbracoContext = GetUmbracoContext("http://localhost", mediaUrlProviders: new[] { _mediaUrlProvider }); + + var umbracoFilePropertyType = CreatePropertyType(Constants.PropertyEditors.Aliases.UploadField, null, ContentVariation.Culture); + + const string enMediaUrl = "/media/rfeiw584/en.jpg"; + const string daMediaUrl = "/media/uf8ewud2/da.jpg"; + + var property = new SolidPublishedPropertyWithLanguageVariants + { + Alias = "umbracoFile", + PropertyType = umbracoFilePropertyType, + }; + + property.SetValue("en", enMediaUrl, true); + property.SetValue("da", daMediaUrl); + + var contentType = new PublishedContentType(666, "alias", PublishedItemType.Content, Enumerable.Empty(), new [] { umbracoFilePropertyType }, ContentVariation.Culture); + var publishedContent = new SolidPublishedContent(contentType) {Properties = new[] {property}}; + + var resolvedUrl = umbracoContext.UrlProvider.GetMediaUrl(publishedContent, "umbracoFile", UrlProviderMode.Auto, "da", null); + Assert.AreEqual(daMediaUrl, resolvedUrl); + } + + private static TestPublishedContent CreatePublishedContent(string propertyEditorAlias, object propertyValue, object dataTypeConfiguration) + { + var umbracoFilePropertyType = CreatePropertyType(propertyEditorAlias, dataTypeConfiguration, ContentVariation.Nothing); + + var contentType = new PublishedContentType(666, "alias", PublishedItemType.Content, Enumerable.Empty(), + new[] {umbracoFilePropertyType}, ContentVariation.Nothing); + + return new TestPublishedContent(contentType, 1234, Guid.NewGuid(), + new Dictionary {{"umbracoFile", propertyValue } }, false); + } + + private static PublishedPropertyType CreatePropertyType(string propertyEditorAlias, object dataTypeConfiguration, ContentVariation variation) + { + var uploadDataType = new PublishedDataType(1234, propertyEditorAlias, new Lazy(() => dataTypeConfiguration)); + + var propertyValueConverters = new PropertyValueConverterCollection(new IPropertyValueConverter[] + { + new UploadPropertyConverter(), + new ImageCropperValueConverter(), + }); + + var publishedModelFactory = Mock.Of(); + var publishedContentTypeFactory = new Mock(); + publishedContentTypeFactory.Setup(x => x.GetDataType(It.IsAny())) + .Returns(uploadDataType); + + return new PublishedPropertyType("umbracoFile", 42, true, variation, propertyValueConverters, publishedModelFactory, publishedContentTypeFactory.Object); + } + } +} diff --git a/src/Umbraco.Tests/Scoping/ScopedNuCacheTests.cs b/src/Umbraco.Tests/Scoping/ScopedNuCacheTests.cs index 4f297b894b..3709490697 100644 --- a/src/Umbraco.Tests/Scoping/ScopedNuCacheTests.cs +++ b/src/Umbraco.Tests/Scoping/ScopedNuCacheTests.cs @@ -118,6 +118,7 @@ namespace Umbraco.Tests.Scoping new WebSecurity(httpContext, Current.Services.UserService, globalSettings), umbracoSettings ?? SettingsForTests.GetDefaultUmbracoSettings(), urlProviders ?? Enumerable.Empty(), + Enumerable.Empty(), globalSettings, new TestVariationContextAccessor()); diff --git a/src/Umbraco.Tests/Security/BackOfficeCookieManagerTests.cs b/src/Umbraco.Tests/Security/BackOfficeCookieManagerTests.cs index e32df610b9..2b04a02f46 100644 --- a/src/Umbraco.Tests/Security/BackOfficeCookieManagerTests.cs +++ b/src/Umbraco.Tests/Security/BackOfficeCookieManagerTests.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Configuration; +using System.Linq; using System.Web; using Microsoft.Owin; using Moq; @@ -33,7 +34,7 @@ namespace Umbraco.Tests.Security Mock.Of(), Mock.Of(), new WebSecurity(Mock.Of(), Current.Services.UserService, globalSettings), - TestObjects.GetUmbracoSettings(), new List(),globalSettings, + TestObjects.GetUmbracoSettings(), new List(), Enumerable.Empty(), globalSettings, new TestVariationContextAccessor()); var runtime = Mock.Of(x => x.Level == RuntimeLevel.Install); @@ -53,7 +54,7 @@ namespace Umbraco.Tests.Security Mock.Of(), Mock.Of(), new WebSecurity(Mock.Of(), Current.Services.UserService, globalSettings), - TestObjects.GetUmbracoSettings(), new List(), globalSettings, + TestObjects.GetUmbracoSettings(), new List(), Enumerable.Empty(), globalSettings, new TestVariationContextAccessor()); var runtime = Mock.Of(x => x.Level == RuntimeLevel.Run); diff --git a/src/Umbraco.Tests/TestHelpers/ControllerTesting/TestControllerActivatorBase.cs b/src/Umbraco.Tests/TestHelpers/ControllerTesting/TestControllerActivatorBase.cs index cf2b3b843f..e52d338ca6 100644 --- a/src/Umbraco.Tests/TestHelpers/ControllerTesting/TestControllerActivatorBase.cs +++ b/src/Umbraco.Tests/TestHelpers/ControllerTesting/TestControllerActivatorBase.cs @@ -139,6 +139,7 @@ namespace Umbraco.Tests.TestHelpers.ControllerTesting webSecurity.Object, Mock.Of(section => section.WebRouting == Mock.Of(routingSection => routingSection.UrlProviderMode == "Auto")), Enumerable.Empty(), + Enumerable.Empty(), globalSettings, new TestVariationContextAccessor()); diff --git a/src/Umbraco.Tests/TestHelpers/TestObjects-Mocks.cs b/src/Umbraco.Tests/TestHelpers/TestObjects-Mocks.cs index ee938cd027..dc8e35bb52 100644 --- a/src/Umbraco.Tests/TestHelpers/TestObjects-Mocks.cs +++ b/src/Umbraco.Tests/TestHelpers/TestObjects-Mocks.cs @@ -123,6 +123,7 @@ namespace Umbraco.Tests.TestHelpers var umbracoSettings = GetUmbracoSettings(); var globalSettings = GetGlobalSettings(); var urlProviders = new UrlProviderCollection(Enumerable.Empty()); + var mediaUrlProviders = new MediaUrlProviderCollection(Enumerable.Empty()); if (accessor == null) accessor = new TestUmbracoContextAccessor(); @@ -134,6 +135,7 @@ namespace Umbraco.Tests.TestHelpers umbracoSettings, globalSettings, urlProviders, + mediaUrlProviders, Mock.Of()); return umbracoContextFactory.EnsureUmbracoContext(httpContext).UmbracoContext; diff --git a/src/Umbraco.Tests/TestHelpers/TestWithDatabaseBase.cs b/src/Umbraco.Tests/TestHelpers/TestWithDatabaseBase.cs index 7f3c855593..146cb23c1f 100644 --- a/src/Umbraco.Tests/TestHelpers/TestWithDatabaseBase.cs +++ b/src/Umbraco.Tests/TestHelpers/TestWithDatabaseBase.cs @@ -353,7 +353,7 @@ namespace Umbraco.Tests.TestHelpers } } - protected UmbracoContext GetUmbracoContext(string url, int templateId = 1234, RouteData routeData = null, bool setSingleton = false, IUmbracoSettingsSection umbracoSettings = null, IEnumerable urlProviders = null, IGlobalSettings globalSettings = null, IPublishedSnapshotService snapshotService = null) + protected UmbracoContext GetUmbracoContext(string url, int templateId = 1234, RouteData routeData = null, bool setSingleton = false, IUmbracoSettingsSection umbracoSettings = null, IEnumerable urlProviders = null, IEnumerable mediaUrlProviders = null, IGlobalSettings globalSettings = null, IPublishedSnapshotService snapshotService = null) { // ensure we have a PublishedCachesService var service = snapshotService ?? PublishedSnapshotService as PublishedSnapshotService; @@ -380,6 +380,7 @@ namespace Umbraco.Tests.TestHelpers Factory.GetInstance()), umbracoSettings ?? Factory.GetInstance(), urlProviders ?? Enumerable.Empty(), + mediaUrlProviders ?? Enumerable.Empty(), globalSettings ?? Factory.GetInstance(), new TestVariationContextAccessor()); diff --git a/src/Umbraco.Tests/Testing/TestingTests/MockTests.cs b/src/Umbraco.Tests/Testing/TestingTests/MockTests.cs index f0e31226e2..28fecd6b2b 100644 --- a/src/Umbraco.Tests/Testing/TestingTests/MockTests.cs +++ b/src/Umbraco.Tests/Testing/TestingTests/MockTests.cs @@ -80,7 +80,7 @@ namespace Umbraco.Tests.Testing.TestingTests .Returns(UrlInfo.Url("/hello/world/1234")); var urlProvider = urlProviderMock.Object; - var theUrlProvider = new UrlProvider(umbracoContext, new [] { urlProvider }, umbracoContext.VariationContextAccessor); + var theUrlProvider = new UrlProvider(umbracoContext, new [] { urlProvider }, Enumerable.Empty(), umbracoContext.VariationContextAccessor); var contentType = new PublishedContentType(666, "alias", PublishedItemType.Content, Enumerable.Empty(), Enumerable.Empty(), ContentVariation.Nothing); var publishedContent = Mock.Of(); diff --git a/src/Umbraco.Tests/Umbraco.Tests.csproj b/src/Umbraco.Tests/Umbraco.Tests.csproj index 6188f577bb..ddd67df6e5 100644 --- a/src/Umbraco.Tests/Umbraco.Tests.csproj +++ b/src/Umbraco.Tests/Umbraco.Tests.csproj @@ -143,6 +143,7 @@ + diff --git a/src/Umbraco.Tests/Web/Mvc/RenderIndexActionSelectorAttributeTests.cs b/src/Umbraco.Tests/Web/Mvc/RenderIndexActionSelectorAttributeTests.cs index 0b76de9879..eeb1eb4b37 100644 --- a/src/Umbraco.Tests/Web/Mvc/RenderIndexActionSelectorAttributeTests.cs +++ b/src/Umbraco.Tests/Web/Mvc/RenderIndexActionSelectorAttributeTests.cs @@ -72,6 +72,7 @@ namespace Umbraco.Tests.Web.Mvc TestObjects.GetUmbracoSettings(), globalSettings, new UrlProviderCollection(Enumerable.Empty()), + new MediaUrlProviderCollection(Enumerable.Empty()), Mock.Of()); var umbracoContextReference = umbracoContextFactory.EnsureUmbracoContext(Mock.Of()); @@ -101,6 +102,7 @@ namespace Umbraco.Tests.Web.Mvc TestObjects.GetUmbracoSettings(), globalSettings, new UrlProviderCollection(Enumerable.Empty()), + new MediaUrlProviderCollection(Enumerable.Empty()), Mock.Of()); var umbracoContextReference = umbracoContextFactory.EnsureUmbracoContext(Mock.Of()); @@ -130,6 +132,7 @@ namespace Umbraco.Tests.Web.Mvc TestObjects.GetUmbracoSettings(), globalSettings, new UrlProviderCollection(Enumerable.Empty()), + new MediaUrlProviderCollection(Enumerable.Empty()), Mock.Of()); var umbracoContextReference = umbracoContextFactory.EnsureUmbracoContext(Mock.Of()); @@ -159,6 +162,7 @@ namespace Umbraco.Tests.Web.Mvc TestObjects.GetUmbracoSettings(), globalSettings, new UrlProviderCollection(Enumerable.Empty()), + new MediaUrlProviderCollection(Enumerable.Empty()), Mock.Of()); var umbracoContextReference = umbracoContextFactory.EnsureUmbracoContext(Mock.Of()); diff --git a/src/Umbraco.Tests/Web/Mvc/SurfaceControllerTests.cs b/src/Umbraco.Tests/Web/Mvc/SurfaceControllerTests.cs index 7de2dd1aad..1a789023a5 100644 --- a/src/Umbraco.Tests/Web/Mvc/SurfaceControllerTests.cs +++ b/src/Umbraco.Tests/Web/Mvc/SurfaceControllerTests.cs @@ -47,6 +47,7 @@ namespace Umbraco.Tests.Web.Mvc TestObjects.GetUmbracoSettings(), globalSettings, new UrlProviderCollection(Enumerable.Empty()), + new MediaUrlProviderCollection(Enumerable.Empty()), Mock.Of()); var umbracoContextReference = umbracoContextFactory.EnsureUmbracoContext(Mock.Of()); @@ -74,6 +75,7 @@ namespace Umbraco.Tests.Web.Mvc TestObjects.GetUmbracoSettings(), globalSettings, new UrlProviderCollection(Enumerable.Empty()), + new MediaUrlProviderCollection(Enumerable.Empty()), Mock.Of()); var umbracoContextReference = umbracoContextFactory.EnsureUmbracoContext(Mock.Of()); @@ -104,6 +106,7 @@ namespace Umbraco.Tests.Web.Mvc Mock.Of(section => section.WebRouting == Mock.Of(routingSection => routingSection.UrlProviderMode == "Auto")), globalSettings, new UrlProviderCollection(Enumerable.Empty()), + new MediaUrlProviderCollection(Enumerable.Empty()), Mock.Of()); var umbracoContextReference = umbracoContextFactory.EnsureUmbracoContext(Mock.Of()); @@ -141,6 +144,7 @@ namespace Umbraco.Tests.Web.Mvc Mock.Of(section => section.WebRouting == webRoutingSettings), globalSettings, new UrlProviderCollection(Enumerable.Empty()), + new MediaUrlProviderCollection(Enumerable.Empty()), Mock.Of()); var umbracoContextReference = umbracoContextFactory.EnsureUmbracoContext(Mock.Of()); diff --git a/src/Umbraco.Tests/Web/Mvc/UmbracoViewPageTests.cs b/src/Umbraco.Tests/Web/Mvc/UmbracoViewPageTests.cs index d1395c6f2e..5c291c9601 100644 --- a/src/Umbraco.Tests/Web/Mvc/UmbracoViewPageTests.cs +++ b/src/Umbraco.Tests/Web/Mvc/UmbracoViewPageTests.cs @@ -440,6 +440,7 @@ namespace Umbraco.Tests.Web.Mvc new WebSecurity(http, Current.Services.UserService, globalSettings), TestObjects.GetUmbracoSettings(), Enumerable.Empty(), + Enumerable.Empty(), globalSettings, new TestVariationContextAccessor()); diff --git a/src/Umbraco.Tests/Web/TemplateUtilitiesTests.cs b/src/Umbraco.Tests/Web/TemplateUtilitiesTests.cs index c868914347..60e3971464 100644 --- a/src/Umbraco.Tests/Web/TemplateUtilitiesTests.cs +++ b/src/Umbraco.Tests/Web/TemplateUtilitiesTests.cs @@ -105,6 +105,7 @@ namespace Umbraco.Tests.Web Mock.Of(section => section.WebRouting == Mock.Of(routingSection => routingSection.UrlProviderMode == "Auto")), globalSettings, new UrlProviderCollection(new[] { testUrlProvider.Object }), + new MediaUrlProviderCollection(Enumerable.Empty()), Mock.Of()); using (var reference = umbracoContextFactory.EnsureUmbracoContext(Mock.Of())) diff --git a/src/Umbraco.Tests/Web/WebExtensionMethodTests.cs b/src/Umbraco.Tests/Web/WebExtensionMethodTests.cs index 21b72a3832..9ba010642e 100644 --- a/src/Umbraco.Tests/Web/WebExtensionMethodTests.cs +++ b/src/Umbraco.Tests/Web/WebExtensionMethodTests.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.IO; +using System.Linq; using System.Web; using System.Web.Mvc; using System.Web.Routing; @@ -31,6 +32,7 @@ namespace Umbraco.Tests.Web new WebSecurity(Mock.Of(), Current.Services.UserService, TestObjects.GetGlobalSettings()), TestObjects.GetUmbracoSettings(), new List(), + Enumerable.Empty(), TestObjects.GetGlobalSettings(), new TestVariationContextAccessor()); var r1 = new RouteData(); @@ -49,6 +51,7 @@ namespace Umbraco.Tests.Web new WebSecurity(Mock.Of(), Current.Services.UserService, TestObjects.GetGlobalSettings()), TestObjects.GetUmbracoSettings(), new List(), + Enumerable.Empty(), TestObjects.GetGlobalSettings(), new TestVariationContextAccessor()); @@ -77,6 +80,7 @@ namespace Umbraco.Tests.Web new WebSecurity(Mock.Of(), Current.Services.UserService, TestObjects.GetGlobalSettings()), TestObjects.GetUmbracoSettings(), new List(), + Enumerable.Empty(), TestObjects.GetGlobalSettings(), new TestVariationContextAccessor()); diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbconfirm.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbconfirm.directive.js index ca9e7a6712..ce6b90c1ec 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbconfirm.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbconfirm.directive.js @@ -60,6 +60,7 @@ function confirmDirective() { link: function (scope, element, attr, ctrl) { scope.showCancel = false; scope.showConfirm = false; + scope.confirmButtonState = "init"; if (scope.onConfirm) { scope.showConfirm = true; @@ -68,6 +69,15 @@ function confirmDirective() { if (scope.onCancel) { scope.showCancel = true; } + + scope.confirm = function () { + if (!scope.onConfirm) { + return; + } + + scope.confirmButtonState = "busy"; + scope.onConfirm(); + } } }; } diff --git a/src/Umbraco.Web.UI.Client/src/less/components/editor/subheader/umb-editor-sub-header.less b/src/Umbraco.Web.UI.Client/src/less/components/editor/subheader/umb-editor-sub-header.less index 515efdfa00..78cccac57a 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/editor/subheader/umb-editor-sub-header.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/editor/subheader/umb-editor-sub-header.less @@ -28,6 +28,7 @@ padding-left: 10px; padding-right: 10px; background-color: @pinkLight; + border-color: @pinkLight; border-radius: 3px; } diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-checkmark.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-checkmark.less index 75ac13bdd0..021fc8cc9b 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-checkmark.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-checkmark.less @@ -2,19 +2,21 @@ border: 2px solid @white; width: 25px; height: 25px; - background: @gray-7; - border-radius: 50%; + border: 1px solid @gray-7; + border-radius: 3px; box-sizing: border-box; display: flex; justify-content: center; align-items: center; - color: @white; + color: @gray-7; cursor: pointer; font-size: 15px; } .umb-checkmark--checked { - background: @green; + background: @ui-active; + border-color: @ui-active; + color: @white; } .umb-checkmark--xs { diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-group-builder.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-group-builder.less index e2a35c5fb5..4c23aef5f0 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-group-builder.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-group-builder.less @@ -137,7 +137,7 @@ input.umb-group-builder__group-title-input:disabled:hover { .umb-group-builder__group-add-property { min-height: 46px; - margin-right: 30px; + margin-right: 45px; margin-left: 270px; border-radius: 3px; diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-nested-content.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-nested-content.less index e0abd3fd26..db0d96b79d 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-nested-content.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-nested-content.less @@ -127,6 +127,9 @@ border-radius: 200px; text-decoration: none !important; } +.umb-nested-content__icon.umb-nested-content__icon--disabled:hover { + cursor: default; +} .umb-nested-content__icon:hover, .umb-nested-content__icon--active diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-table.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-table.less index 6674e01475..f387b6540b 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-table.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-table.less @@ -136,9 +136,14 @@ input.umb-table__input { } .umb-table-body__link { + + color: @ui-option-type; + font-size: 14px; + font-weight: bold; text-decoration: none; - - &:hover { + + &:hover, &:focus { + color: @ui-option-type-hover; text-decoration: underline; } } @@ -259,6 +264,15 @@ input.umb-table__input { flex: 0 0 auto !important; } +.umb-table-cell--small { + flex: .5 .5 1%; + max-width: 12.5%; +} +.umb-table-cell--large { + flex: 1 1 25%; + max-width: 25%; +} + .umb-table-cell--faded { opacity: 0.4; } diff --git a/src/Umbraco.Web.UI.Client/src/less/components/users/umb-user-table.less b/src/Umbraco.Web.UI.Client/src/less/components/users/umb-user-table.less index bc85ae90a9..0c61a5d113 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/users/umb-user-table.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/users/umb-user-table.less @@ -3,6 +3,10 @@ .umb-user-table-col-avatar { flex: 0 0 32px; padding: 15px 0; + + .umb-checkmark { + margin-left:5px; + } } .umb-table-cell a { @@ -14,15 +18,8 @@ } .umb-table-body .umb-table-cell.umb-table__name { - margin: 0; - padding: 0; a { display: flex; - padding: 6px 2px; - height: 42px; - span { - margin: auto 14px; - } } } .umb-table-cell.umb-table__name a { diff --git a/src/Umbraco.Web.UI.Client/src/less/modals.less b/src/Umbraco.Web.UI.Client/src/less/modals.less index 51f87d09dd..52a573d8c4 100644 --- a/src/Umbraco.Web.UI.Client/src/less/modals.less +++ b/src/Umbraco.Web.UI.Client/src/less/modals.less @@ -52,7 +52,7 @@ bottom: 0px; left: 0px; right: 0px; - position: absolute; + position: absolute; } .--notInFront .umb-modalcolumn::after { @@ -67,7 +67,7 @@ /* re-align loader */ .umb-modal .umb-loader-wrapper, .umb-modalcolumn .umb-loader-wrapper, .umb-dialog .umb-loader-wrapper{ - position:relative; + position:relative; margin: 20px -20px; } @@ -106,7 +106,7 @@ .umb-dialog-footer{ position: absolute; overflow:auto; - text-align: right; + text-align: right; height: 32px; left: 0px; right: 0px; @@ -126,7 +126,7 @@ .umbracoDialog form{height: 100%;} /*ensures dialogs doesnt have side-by-side labels*/ -.umbracoDialog .controls-row, +.umbracoDialog .controls-row, .umb-modal .controls-row{margin-left: 0px !important;} /* modal and umb-modal are used for right.hand dialogs */ @@ -174,7 +174,7 @@ padding: 20px; background: @white; border: none; - height: auto; + height: auto; } .umb-modal .umb-panel-body{ padding: 0px 20px 0px 20px; @@ -186,7 +186,7 @@ } .umb-modal i { font-size: 20px; -} +} .umb-modal .breadcrumb { background: none; padding: 0 diff --git a/src/Umbraco.Web.UI.Client/src/less/rte.less b/src/Umbraco.Web.UI.Client/src/less/rte.less index a4d6387012..ccf52acc53 100644 --- a/src/Umbraco.Web.UI.Client/src/less/rte.less +++ b/src/Umbraco.Web.UI.Client/src/less/rte.less @@ -66,6 +66,13 @@ /* pre-value editor */ -.rte-editor-preval .control-group .controls > div > label .mce-ico { - line-height: 20px; +.rte-editor-preval .control-group .controls > div > label { + cursor: pointer !important; + + .mce-cmd .checkbox { + padding-right: 0; + } + .mce-ico { + line-height: 20px; + } } diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/datatypesettings/datatypesettings.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/datatypesettings/datatypesettings.controller.js index 46a4238c0c..7faaddde77 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/datatypesettings/datatypesettings.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/datatypesettings/datatypesettings.controller.js @@ -100,6 +100,10 @@ } function submit() { + if (!formHelper.submitForm({ scope: $scope })) { + return; + } + vm.saveButtonState = "busy"; var preValues = dataTypeHelper.createPreValueProps(vm.dataType.preValues); diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/datatypesettings/datatypesettings.html b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/datatypesettings/datatypesettings.html index 3faf74fdef..8acaa544c1 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/datatypesettings/datatypesettings.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/datatypesettings/datatypesettings.html @@ -1,7 +1,8 @@
- -
+ + + - +
+ + -
diff --git a/src/Umbraco.Web.UI.Client/src/views/components/umb-confirm.html b/src/Umbraco.Web.UI.Client/src/views/components/umb-confirm.html index 8426d03acf..b89b7f559b 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/umb-confirm.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/umb-confirm.html @@ -1,9 +1,21 @@

{{caption}}

-
- Cancel - Ok -
+
+ + + + +
diff --git a/src/Umbraco.Web.UI.Client/src/views/components/umb-table.html b/src/Umbraco.Web.UI.Client/src/views/components/umb-table.html index 75bce1c7a5..79c9a8bbe9 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/umb-table.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/umb-table.html @@ -11,14 +11,18 @@
- Name - + + Name + +
Status
@@ -38,7 +38,8 @@ type="button" action="vm.save(vm.notifyOptions)" state="vm.saveState" - button-style="action"> + button-style="success" + disabled="!vm.canSave"> diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.doctypepicker.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.doctypepicker.html index a4cf675c8e..8701005743 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.doctypepicker.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.doctypepicker.html @@ -34,7 +34,7 @@ - + Delete diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.html index 572021aebd..bca767f650 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.html @@ -37,7 +37,7 @@ diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/rte.prevalues.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/rte.prevalues.controller.js index 1b489d6283..41097f9e9a 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/rte.prevalues.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/rte.prevalues.controller.js @@ -31,26 +31,21 @@ angular.module("umbraco").controller("Umbraco.PrevalueEditors.RteController", var icon = getFontIcon(obj.alias); return angular.extend(obj, { fontIcon: icon.name, - isCustom: icon.isCustom + isCustom: icon.isCustom, + selected: $scope.model.value.toolbar.indexOf(obj.alias) >= 0 }); }); }); stylesheetResource.getAll().then(function(stylesheets){ $scope.stylesheets = stylesheets; + + _.each($scope.stylesheets, function (stylesheet) { + // support both current format (full stylesheet path) and legacy format (stylesheet name only) + stylesheet.selected = $scope.model.value.stylesheets.indexOf(stylesheet.path) >= 0 ||$scope.model.value.stylesheets.indexOf(stylesheet.name) >= 0; + }); }); - $scope.commandSelected = function(cmd) { - cmd.selected = $scope.model.value.toolbar.indexOf(cmd.alias) >= 0; - return cmd.selected; - }; - - $scope.cssSelected = function (css) { - // support both current format (full stylesheet path) and legacy format (stylesheet name only) - css.selected = $scope.model.value.stylesheets.indexOf(css.path) >= 0 ||$scope.model.value.stylesheets.indexOf(css.name) >= 0; - return css.selected; - } - $scope.selectCommand = function(command){ var index = $scope.model.value.toolbar.indexOf(command.alias); diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/rte.prevalues.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/rte.prevalues.html index 463b1a03e0..ca6178fea9 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/rte.prevalues.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/rte.prevalues.html @@ -3,11 +3,10 @@
- - - - - - - - - +
-
- - - -
Group
- - - - -
+
+
+
+
Name
+
Sections
+
Content start node
+
Media start node
+
+
+ +
+
+ +
+ + +
+
+ + +
+
+
+ {{ section.name }}, + All sections +
+
+
+ No start node selected + {{ group.contentStartNode.name }} +
+
+ No start node selected + {{ group.mediaStartNode.name }} +
+
+
+ diff --git a/src/Umbraco.Web.UI.Client/src/views/users/views/users/users.html b/src/Umbraco.Web.UI.Client/src/views/users/views/users/users.html index 965880d94f..24f504be63 100644 --- a/src/Umbraco.Web.UI.Client/src/views/users/views/users/users.html +++ b/src/Umbraco.Web.UI.Client/src/views/users/views/users/users.html @@ -218,12 +218,11 @@
-
+ -
Name
User group
Last login
@@ -235,15 +234,7 @@ ng-click="vm.selectUser(user, vm.selection, $event)" ng-class="{'-selected': user.selected, '-selectable': vm.isSelectable(user)}" class="umb-table-row umb-user-table-row"> -
-
- - -
-
-
+ -
{{ userGroup.name }},
+
{{ userGroup.name }},
{{ user.formattedLastLogin }}
Downloads Likes Compatibility - This package is compatible with the following versions of Umbraco, as reported by community members. Full compatability cannot be gauranteed for versions reported below 100% + This package is compatible with the following versions of Umbraco, as reported by community members. Full compatability cannot be guaranteed for versions reported below 100% External sources Author Documentation diff --git a/src/Umbraco.Web/BatchedDatabaseServerMessenger.cs b/src/Umbraco.Web/BatchedDatabaseServerMessenger.cs index 818e8ecf77..6596fee245 100644 --- a/src/Umbraco.Web/BatchedDatabaseServerMessenger.cs +++ b/src/Umbraco.Web/BatchedDatabaseServerMessenger.cs @@ -13,6 +13,7 @@ using Umbraco.Core.Persistence; using Umbraco.Core.Persistence.Dtos; using Umbraco.Core.Scoping; using Umbraco.Web.Composing; +using System.ComponentModel; namespace Umbraco.Web { @@ -26,6 +27,13 @@ namespace Umbraco.Web { private readonly IUmbracoDatabaseFactory _databaseFactory; + [Obsolete("This overload should not be used, enableDistCalls has no effect")] + [EditorBrowsable(EditorBrowsableState.Never)] + public BatchedDatabaseServerMessenger( + IRuntimeState runtime, IUmbracoDatabaseFactory databaseFactory, IScopeProvider scopeProvider, ISqlContext sqlContext, IProfilingLogger proflog, IGlobalSettings globalSettings, bool enableDistCalls, DatabaseServerMessengerOptions options) + : this(runtime, databaseFactory, scopeProvider, sqlContext, proflog, globalSettings, options) + { } + public BatchedDatabaseServerMessenger( IRuntimeState runtime, IUmbracoDatabaseFactory databaseFactory, IScopeProvider scopeProvider, ISqlContext sqlContext, IProfilingLogger proflog, IGlobalSettings globalSettings, DatabaseServerMessengerOptions options) : base(runtime, scopeProvider, sqlContext, proflog, globalSettings, true, options) diff --git a/src/Umbraco.Web/Compose/NotificationsComponent.cs b/src/Umbraco.Web/Compose/NotificationsComponent.cs index adc0008f48..c099fde8ef 100644 --- a/src/Umbraco.Web/Compose/NotificationsComponent.cs +++ b/src/Umbraco.Web/Compose/NotificationsComponent.cs @@ -41,14 +41,20 @@ namespace Umbraco.Web.Compose //Send notifications for the update and created actions ContentService.Saved += (sender, args) => ContentServiceSaved(_notifier, sender, args, _actions); - //Send notifications for the delete action - ContentService.Deleted += (sender, args) => _notifier.Notify(_actions.GetAction(), args.DeletedEntities.ToArray()); - //Send notifications for the unpublish action ContentService.Unpublished += (sender, args) => _notifier.Notify(_actions.GetAction(), args.PublishedEntities.ToArray()); + //Send notifications for the move/move to recycle bin and restore actions + ContentService.Moved += (sender, args) => ContentServiceMoved(_notifier, sender, args, _actions); + + //Send notifications for the delete action when content is moved to the recycle bin + ContentService.Trashed += (sender, args) => _notifier.Notify(_actions.GetAction(), args.MoveInfoCollection.Select(m => m.Entity).ToArray()); + + //Send notifications for the copy action + ContentService.Copied += (sender, args) => _notifier.Notify(_actions.GetAction(), args.Original); + //Send notifications for the rollback action - ContentService.RolledBack += (sender, args) => _notifier.Notify(_actions.GetAction(), args.Entity); + ContentService.RolledBack += (sender, args) => _notifier.Notify(_actions.GetAction(), args.Entity); } public void Terminate() @@ -92,6 +98,22 @@ namespace Umbraco.Web.Compose notifier.Notify(actions.GetAction(), newEntities.ToArray()); notifier.Notify(actions.GetAction(), updatedEntities.ToArray()); } + + private void ContentServiceMoved(Notifier notifier, IContentService sender, Core.Events.MoveEventArgs args, ActionCollection actions) + { + // notify about the move for all moved items + _notifier.Notify(_actions.GetAction(), args.MoveInfoCollection.Select(m => m.Entity).ToArray()); + + // for any items being moved from the recycle bin (restored), explicitly notify about that too + var restoredEntities = args.MoveInfoCollection + .Where(m => m.OriginalPath.Contains(Constants.System.RecycleBinContentString)) + .Select(m => m.Entity) + .ToArray(); + if(restoredEntities.Any()) + { + _notifier.Notify(_actions.GetAction(), restoredEntities); + } + } /// /// This class is used to send the notifications diff --git a/src/Umbraco.Web/Composing/Current.cs b/src/Umbraco.Web/Composing/Current.cs index 420c0b8a4c..5be5e45ecd 100644 --- a/src/Umbraco.Web/Composing/Current.cs +++ b/src/Umbraco.Web/Composing/Current.cs @@ -101,6 +101,9 @@ namespace Umbraco.Web.Composing public static UrlProviderCollection UrlProviders => Factory.GetInstance(); + public static MediaUrlProviderCollection MediaUrlProviders + => Factory.GetInstance(); + public static HealthCheckCollectionBuilder HealthCheckCollectionBuilder => Factory.GetInstance(); diff --git a/src/Umbraco.Web/CompositionExtensions.cs b/src/Umbraco.Web/CompositionExtensions.cs index 9052b20934..27a56afc1e 100644 --- a/src/Umbraco.Web/CompositionExtensions.cs +++ b/src/Umbraco.Web/CompositionExtensions.cs @@ -90,6 +90,13 @@ namespace Umbraco.Web public static UrlProviderCollectionBuilder UrlProviders(this Composition composition) => composition.WithCollectionBuilder(); + /// + /// Gets the media url providers collection builder. + /// + /// The composition. + public static MediaUrlProviderCollectionBuilder MediaUrlProviders(this Composition composition) + => composition.WithCollectionBuilder(); + /// /// Gets the backoffice sections/applications collection builder. /// diff --git a/src/Umbraco.Web/ImageCropperTemplateExtensions.cs b/src/Umbraco.Web/ImageCropperTemplateExtensions.cs index ed9903e07a..d7f457287a 100644 --- a/src/Umbraco.Web/ImageCropperTemplateExtensions.cs +++ b/src/Umbraco.Web/ImageCropperTemplateExtensions.cs @@ -130,15 +130,15 @@ namespace Umbraco.Web if (mediaItem.HasProperty(propertyAlias) == false || mediaItem.HasValue(propertyAlias) == false) return string.Empty; + var mediaItemUrl = mediaItem.MediaUrl(propertyAlias); + //get the default obj from the value converter var cropperValue = mediaItem.Value(propertyAlias); //is it strongly typed? var stronglyTyped = cropperValue as ImageCropperValue; - string mediaItemUrl; if (stronglyTyped != null) { - mediaItemUrl = stronglyTyped.Src; return GetCropUrl( mediaItemUrl, stronglyTyped, width, height, cropAlias, quality, imageCropMode, imageCropAnchor, preferFocalPoint, useCropDimensions, cacheBusterValue, furtherOptions, ratioMode, upScale); @@ -149,14 +149,12 @@ namespace Umbraco.Web if (jobj != null) { stronglyTyped = jobj.ToObject(); - mediaItemUrl = stronglyTyped.Src; return GetCropUrl( mediaItemUrl, stronglyTyped, width, height, cropAlias, quality, imageCropMode, imageCropAnchor, preferFocalPoint, useCropDimensions, cacheBusterValue, furtherOptions, ratioMode, upScale); } //it's a single string - mediaItemUrl = cropperValue.ToString(); return GetCropUrl( mediaItemUrl, width, height, mediaItemUrl, cropAlias, quality, imageCropMode, imageCropAnchor, preferFocalPoint, useCropDimensions, cacheBusterValue, furtherOptions, ratioMode, upScale); @@ -322,7 +320,7 @@ namespace Umbraco.Web if (crop == null && !string.IsNullOrWhiteSpace(cropAlias)) return null; - imageProcessorUrl.Append(cropDataSet.Src); + imageProcessorUrl.Append(imageUrl); cropDataSet.AppendCropBaseUrl(imageProcessorUrl, crop, string.IsNullOrWhiteSpace(cropAlias), preferFocalPoint); if (crop != null & useCropDimensions) diff --git a/src/Umbraco.Web/Models/PublishedContentBase.cs b/src/Umbraco.Web/Models/PublishedContentBase.cs index 086fae8b16..4fc08f6ff3 100644 --- a/src/Umbraco.Web/Models/PublishedContentBase.cs +++ b/src/Umbraco.Web/Models/PublishedContentBase.cs @@ -78,56 +78,24 @@ namespace Umbraco.Web.Models /// /// - /// The url of documents are computed by the document url providers. The url of medias are, at the moment, - /// computed here from the 'umbracoFile' property -- but we should move to media url providers at some point. + /// The url of documents are computed by the document url providers. The url of medias are computed by the media url providers. /// public virtual string Url(string culture = null, UrlMode mode = UrlMode.Auto) { + var umbracoContext = UmbracoContextAccessor.UmbracoContext; + + if (umbracoContext == null) + throw new InvalidOperationException("Cannot compute Url for a content item when UmbracoContext is null."); + if (umbracoContext.UrlProvider == null) + throw new InvalidOperationException("Cannot compute Url for a content item when UmbracoContext.UrlProvider is null."); + switch (ContentType.ItemType) { case PublishedItemType.Content: - var umbracoContext = UmbracoContextAccessor.UmbracoContext; - - if (umbracoContext == null) - throw new InvalidOperationException("Cannot compute Url for a content item when UmbracoContext is null."); - if (umbracoContext.UrlProvider == null) - throw new InvalidOperationException("Cannot compute Url for a content item when UmbracoContext.UrlProvider is null."); - return umbracoContext.UrlProvider.GetUrl(this, mode, culture); case PublishedItemType.Media: - if (mode == UrlMode.Absolute) - throw new NotSupportedException("Absolute urls are not supported for media items."); - - var prop = GetProperty(Constants.Conventions.Media.File); - if (prop?.GetValue() == null) - { - return string.Empty; - } - - var propType = ContentType.GetPropertyType(Constants.Conventions.Media.File); - - // TODO: consider implementing media url providers - // note: that one does not support variations - //This is a hack - since we now have 2 properties that support a URL: upload and cropper, we need to detect this since we always - // want to return the normal URL and the cropper stores data as json - switch (propType.EditorAlias) - { - case Constants.PropertyEditors.Aliases.UploadField: - - return prop.GetValue().ToString(); - case Constants.PropertyEditors.Aliases.ImageCropper: - //get the url from the json format - - var stronglyTyped = prop.GetValue() as ImageCropperValue; - if (stronglyTyped != null) - { - return stronglyTyped.Src; - } - return prop.GetValue()?.ToString(); - } - - return string.Empty; + return umbracoContext.UrlProvider.GetMediaUrl(this, Constants.Conventions.Media.File, mode, culture); default: throw new NotSupportedException(); diff --git a/src/Umbraco.Web/PropertyEditors/ValueConverters/NestedContentManyValueConverter.cs b/src/Umbraco.Web/PropertyEditors/ValueConverters/NestedContentManyValueConverter.cs index 74b2af6498..559777786f 100644 --- a/src/Umbraco.Web/PropertyEditors/ValueConverters/NestedContentManyValueConverter.cs +++ b/src/Umbraco.Web/PropertyEditors/ValueConverters/NestedContentManyValueConverter.cs @@ -35,9 +35,9 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters public override Type GetPropertyValueType(IPublishedPropertyType propertyType) { var contentTypes = propertyType.DataType.ConfigurationAs().ContentTypes; - return contentTypes.Length > 1 - ? typeof (IEnumerable) - : typeof (IEnumerable<>).MakeGenericType(ModelType.For(contentTypes[0].Alias)); + return contentTypes.Length == 1 + ? typeof (IEnumerable<>).MakeGenericType(ModelType.For(contentTypes[0].Alias)) + : typeof (IEnumerable); } /// @@ -57,9 +57,9 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters { var configuration = propertyType.DataType.ConfigurationAs(); var contentTypes = configuration.ContentTypes; - var elements = contentTypes.Length > 1 - ? new List() - : PublishedModelFactory.CreateModelList(contentTypes[0].Alias); + var elements = contentTypes.Length == 1 + ? PublishedModelFactory.CreateModelList(contentTypes[0].Alias) + : new List(); var value = (string)inter; if (string.IsNullOrWhiteSpace(value)) return elements; diff --git a/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs b/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs index 7c8b69f9b2..f847a9f2ce 100755 --- a/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs @@ -1378,19 +1378,24 @@ WHERE cmsContentNu.nodeId IN ( long total; do { + // the tree is locked, counting and comparing to total is safe var descendants = _documentRepository.GetPage(query, pageIndex++, groupSize, out total, null, Ordering.By("Path")); var items = new List(); + var count = 0; foreach (var c in descendants) { // always the edited version items.Add(GetDto(c, false)); + // and also the published version if it makes any sense if (c.Published) items.Add(GetDto(c, true)); + + count++; } db.BulkInsertRecords(items); - processed += items.Count; + processed += count; } while (processed < total); } @@ -1445,10 +1450,11 @@ WHERE cmsContentNu.nodeId IN ( long total; do { + // the tree is locked, counting and comparing to total is safe var descendants = _mediaRepository.GetPage(query, pageIndex++, groupSize, out total, null, Ordering.By("Path")); - var items = descendants.Select(m => GetDto(m, false)).ToArray(); + var items = descendants.Select(m => GetDto(m, false)).ToList(); db.BulkInsertRecords(items); - processed += items.Length; + processed += items.Count; } while (processed < total); } diff --git a/src/Umbraco.Web/Routing/DefaultMediaUrlProvider.cs b/src/Umbraco.Web/Routing/DefaultMediaUrlProvider.cs new file mode 100644 index 0000000000..a9c961c280 --- /dev/null +++ b/src/Umbraco.Web/Routing/DefaultMediaUrlProvider.cs @@ -0,0 +1,69 @@ +using System; +using Umbraco.Core; +using Umbraco.Core.Models.PublishedContent; +using Umbraco.Core.PropertyEditors.ValueConverters; + +namespace Umbraco.Web.Routing +{ + /// + /// Default media url provider. + /// + public class DefaultMediaUrlProvider : IMediaUrlProvider + { + /// + public virtual UrlInfo GetMediaUrl(UmbracoContext umbracoContext, IPublishedContent content, + string propertyAlias, + UrlProviderMode mode, string culture, Uri current) + { + var prop = content.GetProperty(propertyAlias); + var value = prop?.GetValue(culture); + if (value == null) + { + return null; + } + + var propType = prop.PropertyType; + string path = null; + + switch (propType.EditorAlias) + { + case Constants.PropertyEditors.Aliases.UploadField: + path = value.ToString(); + break; + case Constants.PropertyEditors.Aliases.ImageCropper: + //get the url from the json format + path = value is ImageCropperValue stronglyTyped ? stronglyTyped.Src : value.ToString(); + break; + } + + var url = AssembleUrl(path, current, mode); + return url == null ? null : UrlInfo.Url(url.ToString(), culture); + } + + private Uri AssembleUrl(string path, Uri current, UrlProviderMode mode) + { + if (string.IsNullOrEmpty(path)) + return null; + + Uri uri; + + if (current == null) + mode = UrlProviderMode.Relative; // best we can do + + switch (mode) + { + case UrlProviderMode.Absolute: + uri = new Uri(current?.GetLeftPart(UriPartial.Authority) + path); + break; + case UrlProviderMode.Relative: + case UrlProviderMode.Auto: + uri = new Uri(path, UriKind.Relative); + break; + default: + throw new ArgumentOutOfRangeException(nameof(mode)); + } + + return UriUtility.MediaUriFromUmbraco(uri); + } + } +} diff --git a/src/Umbraco.Web/Routing/IMediaUrlProvider.cs b/src/Umbraco.Web/Routing/IMediaUrlProvider.cs new file mode 100644 index 0000000000..419e4d78df --- /dev/null +++ b/src/Umbraco.Web/Routing/IMediaUrlProvider.cs @@ -0,0 +1,31 @@ +using System; +using Umbraco.Core.Models.PublishedContent; + +namespace Umbraco.Web.Routing +{ + /// + /// Provides media urls. + /// + public interface IMediaUrlProvider + { + /// + /// Gets the url of a media item. + /// + /// The Umbraco context. + /// The published content. + /// The property alias to resolve the url from. + /// The url mode. + /// The variation language. + /// The current absolute url. + /// The url for the media. + /// + /// The url is absolute or relative depending on mode and on current. + /// If the media is multi-lingual, gets the url for the specified culture or, + /// when no culture is specified, the current culture. + /// The url provider can ignore the mode and always return an absolute url, + /// e.g. a cdn url provider will most likely always return an absolute url. + /// If the provider is unable to provide a url, it returns null. + /// + UrlInfo GetMediaUrl(UmbracoContext umbracoContext, IPublishedContent content, string propertyAlias, UrlProviderMode mode, string culture, Uri current); + } +} diff --git a/src/Umbraco.Web/Routing/IUrlProvider.cs b/src/Umbraco.Web/Routing/IUrlProvider.cs index 854363f110..c0ce1fef39 100644 --- a/src/Umbraco.Web/Routing/IUrlProvider.cs +++ b/src/Umbraco.Web/Routing/IUrlProvider.cs @@ -10,7 +10,7 @@ namespace Umbraco.Web.Routing public interface IUrlProvider { /// - /// Gets the nice url of a published content. + /// Gets the url of a published content. /// /// The Umbraco context. /// The published content. diff --git a/src/Umbraco.Web/Routing/MediaUrlProviderCollection.cs b/src/Umbraco.Web/Routing/MediaUrlProviderCollection.cs new file mode 100644 index 0000000000..eef0cbd3bc --- /dev/null +++ b/src/Umbraco.Web/Routing/MediaUrlProviderCollection.cs @@ -0,0 +1,12 @@ +using System.Collections.Generic; +using Umbraco.Core.Composing; + +namespace Umbraco.Web.Routing +{ + public class MediaUrlProviderCollection : BuilderCollectionBase + { + public MediaUrlProviderCollection(IEnumerable items) + : base(items) + { } + } +} diff --git a/src/Umbraco.Web/Routing/MediaUrlProviderCollectionBuilder.cs b/src/Umbraco.Web/Routing/MediaUrlProviderCollectionBuilder.cs new file mode 100644 index 0000000000..7bfc56ed0d --- /dev/null +++ b/src/Umbraco.Web/Routing/MediaUrlProviderCollectionBuilder.cs @@ -0,0 +1,9 @@ +using Umbraco.Core.Composing; + +namespace Umbraco.Web.Routing +{ + public class MediaUrlProviderCollectionBuilder : OrderedCollectionBuilderBase + { + protected override MediaUrlProviderCollectionBuilder This => this; + } +} diff --git a/src/Umbraco.Web/Routing/UrlProvider.cs b/src/Umbraco.Web/Routing/UrlProvider.cs index d34123c96c..10dde3bbae 100644 --- a/src/Umbraco.Web/Routing/UrlProvider.cs +++ b/src/Umbraco.Web/Routing/UrlProvider.cs @@ -7,6 +7,7 @@ using Umbraco.Core.Models; using Umbraco.Core.Models.PublishedContent; using Umbraco.Core.Services; using Umbraco.Web.Composing; +using Umbraco.Web.Models; namespace Umbraco.Web.Routing { @@ -23,13 +24,15 @@ namespace Umbraco.Web.Routing /// The Umbraco context. /// Routing settings. /// The list of url providers. + /// The list of media url providers. /// The current variation accessor. - public UrlProvider(UmbracoContext umbracoContext, IWebRoutingSection routingSettings, IEnumerable urlProviders, IVariationContextAccessor variationContextAccessor) + public UrlProvider(UmbracoContext umbracoContext, IWebRoutingSection routingSettings, IEnumerable urlProviders, IEnumerable mediaUrlProviders, IVariationContextAccessor variationContextAccessor) { if (routingSettings == null) throw new ArgumentNullException(nameof(routingSettings)); _umbracoContext = umbracoContext ?? throw new ArgumentNullException(nameof(umbracoContext)); _urlProviders = urlProviders; + _mediaUrlProviders = mediaUrlProviders; _variationContextAccessor = variationContextAccessor ?? throw new ArgumentNullException(nameof(variationContextAccessor)); var provider = UrlMode.Auto; Mode = provider; @@ -45,12 +48,14 @@ namespace Umbraco.Web.Routing /// /// The Umbraco context. /// The list of url providers. + /// The list of media url providers /// The current variation accessor. /// An optional provider mode. - public UrlProvider(UmbracoContext umbracoContext, IEnumerable urlProviders, IVariationContextAccessor variationContextAccessor, UrlMode mode = UrlMode.Auto) + public UrlProvider(UmbracoContext umbracoContext, IEnumerable urlProviders, IEnumerable mediaUrlProviders, IVariationContextAccessor variationContextAccessor, UrlMode mode = UrlMode.Auto) { _umbracoContext = umbracoContext ?? throw new ArgumentNullException(nameof(umbracoContext)); _urlProviders = urlProviders; + _mediaUrlProviders = mediaUrlProviders; _variationContextAccessor = variationContextAccessor; Mode = mode; @@ -58,6 +63,7 @@ namespace Umbraco.Web.Routing private readonly UmbracoContext _umbracoContext; private readonly IEnumerable _urlProviders; + private readonly IEnumerable _mediaUrlProviders; private readonly IVariationContextAccessor _variationContextAccessor; /// @@ -84,7 +90,7 @@ namespace Umbraco.Web.Routing => GetUrl(content, Mode, culture, current); /// - /// Gets the nice url of a published content. + /// Gets the url of a published content. /// /// The published content. /// A value indicating whether the url should be absolute in any case. @@ -109,7 +115,7 @@ namespace Umbraco.Web.Routing => GetUrl(GetDocument(id), Mode, culture, current); /// - /// Gets the nice url of a published content. + /// Gets the url of a published content. /// /// The published content identifier. /// A value indicating whether the url should be absolute in any case. @@ -124,7 +130,7 @@ namespace Umbraco.Web.Routing => GetUrl(GetDocument(id), GetMode(absolute), culture, current); /// - /// Gets the nice url of a published content. + /// Gets the url of a published content. /// /// The published content identifier. /// The url mode. @@ -145,7 +151,7 @@ namespace Umbraco.Web.Routing => GetUrl(GetDocument(id), Mode, culture, current); /// - /// Gets the nice url of a published content. + /// Gets the url of a published content. /// /// The published content identifier. /// A value indicating whether the url should be absolute in any case. @@ -160,7 +166,7 @@ namespace Umbraco.Web.Routing => GetUrl(GetDocument(id), GetMode(absolute), culture, current); /// - /// Gets the nice url of a published content. + /// Gets the url of a published content. /// /// The published content identifier. /// The url mode. @@ -171,7 +177,7 @@ namespace Umbraco.Web.Routing => GetUrl(GetDocument(id), mode, culture, current); /// - /// Gets the nice url of a published content. + /// Gets the url of a published content. /// /// The published content. /// The url mode. @@ -250,5 +256,86 @@ namespace Umbraco.Web.Routing } #endregion + + #region GetMediaUrl + + /// + /// Gets the url of a media item. + /// + /// The published content. + /// The property alias to resolve the url from. + /// The variation language. + /// The current absolute url. + /// The url for the media. + /// + /// The url is absolute or relative depending on mode and on current. + /// If the media is multi-lingual, gets the url for the specified culture or, + /// when no culture is specified, the current culture. + /// If the provider is unable to provide a url, it returns . + /// + public string GetMediaUrl(IPublishedContent content, string propertyAlias, UrlMode mode = UrlMode.Wtf, string culture = null, Uri current = null) + => GetMediaUrl(content, propertyAlias, Mode, culture, current); + + /// + /// Gets the url of a media item. + /// + /// The published content. + /// The property alias to resolve the url from. + /// A value indicating whether the url should be absolute in any case. + /// The variation language. + /// The current absolute url. + /// The url for the media. + /// + /// The url is absolute or relative depending on mode and on current. + /// If the media is multi-lingual, gets the url for the specified culture or, + /// when no culture is specified, the current culture. + /// If the provider is unable to provide a url, it returns . + /// + public string GetMediaUrl(IPublishedContent content, string propertyAlias, boolx absolute, string culture = null, Uri current = null) + => GetMediaUrl(content, propertyAlias, GetMode(absolute), culture, current); + /// + /// Gets the url of a media item. + /// + /// The published content. + /// The property alias to resolve the url from. + /// The url mode. + /// The variation language. + /// The current absolute url. + /// The url for the media. + /// + /// The url is absolute or relative depending on mode and on current. + /// If the media is multi-lingual, gets the url for the specified culture or, + /// when no culture is specified, the current culture. + /// If the provider is unable to provide a url, it returns . + /// + public string GetMediaUrl(IPublishedContent content, + string propertyAlias, UrlMode mode = UrlMode.Wtf, + string culture = null, Uri current = null) + { + if (propertyAlias == null) throw new ArgumentNullException(nameof(propertyAlias)); + + if (content == null) + return ""; + + // this the ONLY place where we deal with default culture - IMediaUrlProvider always receive a culture + // be nice with tests, assume things can be null, ultimately fall back to invariant + // (but only for variant content of course) + if (content.ContentType.VariesByCulture()) + { + if (culture == null) + culture = _variationContextAccessor?.VariationContext?.Culture ?? ""; + } + + if (current == null) + current = _umbracoContext.CleanedUmbracoUrl; + + var url = _mediaUrlProviders.Select(provider => + provider.GetMediaUrl(_umbracoContext, content, propertyAlias, mode, culture, current)) + .FirstOrDefault(u => u != null); + + return url?.Text ?? ""; + } + + #endregion } } diff --git a/src/Umbraco.Web/Runtime/WebInitialComposer.cs b/src/Umbraco.Web/Runtime/WebInitialComposer.cs index f1c8fcc12f..5a464701e0 100644 --- a/src/Umbraco.Web/Runtime/WebInitialComposer.cs +++ b/src/Umbraco.Web/Runtime/WebInitialComposer.cs @@ -183,6 +183,9 @@ namespace Umbraco.Web.Runtime .Append() .Append(); + composition.WithCollectionBuilder() + .Append(); + composition.RegisterUnique(); composition.WithCollectionBuilder() diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index a190d6711c..8465a2f016 100755 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -214,7 +214,11 @@ + + + + diff --git a/src/Umbraco.Web/UmbracoContext.cs b/src/Umbraco.Web/UmbracoContext.cs index 04052e8be3..69b9ec1db9 100644 --- a/src/Umbraco.Web/UmbracoContext.cs +++ b/src/Umbraco.Web/UmbracoContext.cs @@ -30,6 +30,7 @@ namespace Umbraco.Web WebSecurity webSecurity, IUmbracoSettingsSection umbracoSettings, IEnumerable urlProviders, + IEnumerable mediaUrlProviders, IGlobalSettings globalSettings, IVariationContextAccessor variationContextAccessor) { @@ -38,6 +39,7 @@ namespace Umbraco.Web if (webSecurity == null) throw new ArgumentNullException(nameof(webSecurity)); if (umbracoSettings == null) throw new ArgumentNullException(nameof(umbracoSettings)); if (urlProviders == null) throw new ArgumentNullException(nameof(urlProviders)); + if (mediaUrlProviders == null) throw new ArgumentNullException(nameof(mediaUrlProviders)); VariationContextAccessor = variationContextAccessor ?? throw new ArgumentNullException(nameof(variationContextAccessor)); _globalSettings = globalSettings ?? throw new ArgumentNullException(nameof(globalSettings)); @@ -68,7 +70,7 @@ namespace Umbraco.Web // OriginalRequestUrl = GetRequestFromContext()?.Url ?? new Uri("http://localhost"); CleanedUmbracoUrl = UriUtility.UriToUmbraco(OriginalRequestUrl); - UrlProvider = new UrlProvider(this, umbracoSettings.WebRouting, urlProviders, variationContextAccessor); + UrlProvider = new UrlProvider(this, umbracoSettings.WebRouting, urlProviders, mediaUrlProviders, variationContextAccessor); } /// diff --git a/src/Umbraco.Web/UmbracoContextFactory.cs b/src/Umbraco.Web/UmbracoContextFactory.cs index 9bb2a79411..2a812036bf 100644 --- a/src/Umbraco.Web/UmbracoContextFactory.cs +++ b/src/Umbraco.Web/UmbracoContextFactory.cs @@ -29,12 +29,13 @@ namespace Umbraco.Web private readonly IUmbracoSettingsSection _umbracoSettings; private readonly IGlobalSettings _globalSettings; private readonly UrlProviderCollection _urlProviders; + private readonly MediaUrlProviderCollection _mediaUrlProviders; private readonly IUserService _userService; /// /// Initializes a new instance of the class. /// - public UmbracoContextFactory(IUmbracoContextAccessor umbracoContextAccessor, IPublishedSnapshotService publishedSnapshotService, IVariationContextAccessor variationContextAccessor, IDefaultCultureAccessor defaultCultureAccessor, IUmbracoSettingsSection umbracoSettings, IGlobalSettings globalSettings, UrlProviderCollection urlProviders, IUserService userService) + public UmbracoContextFactory(IUmbracoContextAccessor umbracoContextAccessor, IPublishedSnapshotService publishedSnapshotService, IVariationContextAccessor variationContextAccessor, IDefaultCultureAccessor defaultCultureAccessor, IUmbracoSettingsSection umbracoSettings, IGlobalSettings globalSettings, UrlProviderCollection urlProviders, MediaUrlProviderCollection mediaUrlProviders, IUserService userService) { _umbracoContextAccessor = umbracoContextAccessor ?? throw new ArgumentNullException(nameof(umbracoContextAccessor)); _publishedSnapshotService = publishedSnapshotService ?? throw new ArgumentNullException(nameof(publishedSnapshotService)); @@ -44,6 +45,7 @@ namespace Umbraco.Web _umbracoSettings = umbracoSettings ?? throw new ArgumentNullException(nameof(umbracoSettings)); _globalSettings = globalSettings ?? throw new ArgumentNullException(nameof(globalSettings)); _urlProviders = urlProviders ?? throw new ArgumentNullException(nameof(urlProviders)); + _mediaUrlProviders = mediaUrlProviders ?? throw new ArgumentNullException(nameof(mediaUrlProviders)); _userService = userService ?? throw new ArgumentNullException(nameof(userService)); } @@ -55,7 +57,7 @@ namespace Umbraco.Web var webSecurity = new WebSecurity(httpContext, _userService, _globalSettings); - return new UmbracoContext(httpContext, _publishedSnapshotService, webSecurity, _umbracoSettings, _urlProviders, _globalSettings, _variationContextAccessor); + return new UmbracoContext(httpContext, _publishedSnapshotService, webSecurity, _umbracoSettings, _urlProviders, _mediaUrlProviders, _globalSettings, _variationContextAccessor); } /// diff --git a/src/Umbraco.Web/UriUtility.cs b/src/Umbraco.Web/UriUtility.cs index 04357a3a5a..9e94a4a20a 100644 --- a/src/Umbraco.Web/UriUtility.cs +++ b/src/Umbraco.Web/UriUtility.cs @@ -72,6 +72,15 @@ namespace Umbraco.Web return uri.Rewrite(path); } + // maps a media umbraco uri to a public uri + // ie with virtual directory - that is all for media + public static Uri MediaUriFromUmbraco(Uri uri) + { + var path = uri.GetSafeAbsolutePath(); + path = ToAbsolute(path); + return uri.Rewrite(path); + } + // maps a public uri to an internal umbraco uri // ie no virtual directory, no .aspx, lowercase... public static Uri UriToUmbraco(Uri uri)