diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index c62ad1c494..0f5a12b34b 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -42,4 +42,8 @@ RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ # Needing to set unsafe-perm as root is the user setup # https://docs.npmjs.com/cli/v6/using-npm/config#unsafe-perm # Default: false if running as root, true otherwise (we are ROOT) -RUN npm -g config set user vscode && npm -g config set unsafe-perm \ No newline at end of file +RUN npm -g config set user vscode && npm -g config set unsafe-perm + +# Generate and trust a local developer certificate for Kestrel +# This is needed for Kestrel to bind on https +RUN dotnet dev-certs https --trust diff --git a/.devcontainer/docker-compose.yml b/.devcontainer/docker-compose.yml index 17312c6bd8..e88327b779 100644 --- a/.devcontainer/docker-compose.yml +++ b/.devcontainer/docker-compose.yml @@ -2,7 +2,7 @@ version: '3' services: app: - build: + build: context: . dockerfile: Dockerfile args: @@ -18,7 +18,7 @@ services: volumes: - ..:/workspace:cached - + # Overrides default command so things don't shut down after the process ends. command: sleep infinity @@ -28,7 +28,7 @@ services: # Uncomment the next line to use a non-root user for all processes. # user: vscode - # Use "forwardPorts" in **devcontainer.json** to forward an app port locally. + # Use "forwardPorts" in **devcontainer.json** to forward an app port locally. # (Adding the "ports" property to this file will not forward from a Codespace.) # DotNetCore ENV Variables @@ -41,7 +41,7 @@ services: - Umbraco__CMS__Unattended__UnattendedUserPassword=password1234 - Umbraco__CMS__Global__Smtp__Host=smtp4dev - Umbraco__CMS__Global__Smtp__Port=25 - - Umbraco__CMS__Global__Smtp__From=warren-env@umbraco.com + - Umbraco__CMS__Global__Smtp__From=noreply@umbraco.test db: image: mcr.microsoft.com/mssql/server:2019-latest diff --git a/.gitattributes b/.gitattributes index 3241b6511c..8ac58cf5fe 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,3 +1,4 @@ +* text=auto *.doc diff=astextplain *.DOC diff=astextplain *.docx diff=astextplain @@ -44,6 +45,7 @@ *.json text=auto *.xml text=auto *.resx text=auto +*.yml text eol=lf core.whitespace whitespace=tab-in-indent,trailing-space,tabwidth=2 *.csproj text=auto merge=union *.vbproj text=auto merge=union diff --git a/build/NuSpecs/tools/Web.config.cloud.xdt b/build/NuSpecs/tools/Web.config.cloud.xdt new file mode 100644 index 0000000000..988c741126 --- /dev/null +++ b/build/NuSpecs/tools/Web.config.cloud.xdt @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/build/build.ps1 b/build/build.ps1 index ea0a50e5b1..ff24079532 100644 --- a/build/build.ps1 +++ b/build/build.ps1 @@ -92,6 +92,7 @@ $src = "$($this.SolutionRoot)\src" $log = "$($this.BuildTemp)\belle.log" + Write-Host "Compile Belle" Write-Host "Logging to $log" @@ -555,7 +556,6 @@ # run if (-not $get) { -cd if ($command.Length -eq 0) { $command = @( "Build" ) diff --git a/build/templates/UmbracoProject/UmbracoProject.csproj b/build/templates/UmbracoProject/UmbracoProject.csproj index 09e95b43b0..d1f40a2740 100644 --- a/build/templates/UmbracoProject/UmbracoProject.csproj +++ b/build/templates/UmbracoProject/UmbracoProject.csproj @@ -1,7 +1,11 @@  + net5.0 Umbraco.Cms.Web.UI + $(DefaultItemExcludes);App_Plugins/**; + $(DefaultItemExcludes);umbraco/**; + $(DefaultItemExcludes);wwwroot/media/**; @@ -10,42 +14,18 @@ - + + - + - - - - - - - - - - - - - - - - - true - Always - - - true - Always - - - true - Always - - - - + + + + + @@ -53,4 +33,5 @@ false false + diff --git a/src/Umbraco.Core/Cache/IValueEditorCache.cs b/src/Umbraco.Core/Cache/IValueEditorCache.cs new file mode 100644 index 0000000000..f283d730b5 --- /dev/null +++ b/src/Umbraco.Core/Cache/IValueEditorCache.cs @@ -0,0 +1,12 @@ +using System.Collections.Generic; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.PropertyEditors; + +namespace Umbraco.Cms.Core.Cache +{ + public interface IValueEditorCache + { + public IDataValueEditor GetValueEditor(IDataEditor dataEditor, IDataType dataType); + public void ClearCache(IEnumerable dataTypeIds); + } +} diff --git a/src/Umbraco.Core/Cache/ValueEditorCache.cs b/src/Umbraco.Core/Cache/ValueEditorCache.cs new file mode 100644 index 0000000000..44aa83d44d --- /dev/null +++ b/src/Umbraco.Core/Cache/ValueEditorCache.cs @@ -0,0 +1,61 @@ +using System.Collections.Generic; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.PropertyEditors; + +namespace Umbraco.Cms.Core.Cache +{ + public class ValueEditorCache : IValueEditorCache + { + private readonly Dictionary> _valueEditorCache; + private readonly object _dictionaryLocker; + + public ValueEditorCache() + { + _valueEditorCache = new Dictionary>(); + _dictionaryLocker = new object(); + } + + public IDataValueEditor GetValueEditor(IDataEditor editor, IDataType dataType) + { + // Lock just in case multiple threads uses the cache at the same time. + lock (_dictionaryLocker) + { + // We try and get the dictionary based on the IDataEditor alias, + // this is here just in case a data type can have more than one value data editor. + // If this is not the case this could be simplified quite a bit, by just using the inner dictionary only. + IDataValueEditor valueEditor; + if (_valueEditorCache.TryGetValue(editor.Alias, out Dictionary dataEditorCache)) + { + if (dataEditorCache.TryGetValue(dataType.Id, out valueEditor)) + { + return valueEditor; + } + + valueEditor = editor.GetValueEditor(dataType.Configuration); + dataEditorCache[dataType.Id] = valueEditor; + return valueEditor; + } + + valueEditor = editor.GetValueEditor(dataType.Configuration); + _valueEditorCache[editor.Alias] = new Dictionary { [dataType.Id] = valueEditor }; + return valueEditor; + } + } + + public void ClearCache(IEnumerable dataTypeIds) + { + lock (_dictionaryLocker) + { + // If a datatype is saved or deleted we have to clear any value editors based on their ID from the cache, + // since it could mean that their configuration has changed. + foreach (var id in dataTypeIds) + { + foreach (Dictionary editors in _valueEditorCache.Values) + { + editors.Remove(id); + } + } + } + } + } +} diff --git a/src/Umbraco.Core/Cache/ValueEditorCacheRefresher.cs b/src/Umbraco.Core/Cache/ValueEditorCacheRefresher.cs new file mode 100644 index 0000000000..c815ca7a71 --- /dev/null +++ b/src/Umbraco.Core/Cache/ValueEditorCacheRefresher.cs @@ -0,0 +1,57 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Umbraco.Cms.Core.Events; +using Umbraco.Cms.Core.Notifications; +using Umbraco.Cms.Core.Serialization; + +namespace Umbraco.Cms.Core.Cache +{ + public sealed class ValueEditorCacheRefresher : PayloadCacheRefresherBase + { + private readonly IValueEditorCache _valueEditorCache; + + public ValueEditorCacheRefresher( + AppCaches appCaches, + IJsonSerializer serializer, + IEventAggregator eventAggregator, + ICacheRefresherNotificationFactory factory, + IValueEditorCache valueEditorCache) : base(appCaches, serializer, eventAggregator, factory) + { + _valueEditorCache = valueEditorCache; + } + + public static readonly Guid UniqueId = Guid.Parse("D28A1DBB-2308-4918-9A92-2F8689B6CBFE"); + public override Guid RefresherUniqueId => UniqueId; + public override string Name => "ValueEditorCacheRefresher"; + + public override void Refresh(DataTypeCacheRefresher.JsonPayload[] payloads) + { + IEnumerable ids = payloads.Select(x => x.Id); + _valueEditorCache.ClearCache(ids); + } + + // these events should never trigger + // everything should be PAYLOAD/JSON + + public override void RefreshAll() + { + throw new NotSupportedException(); + } + + public override void Refresh(int id) + { + throw new NotSupportedException(); + } + + public override void Refresh(Guid id) + { + throw new NotSupportedException(); + } + + public override void Remove(int id) + { + throw new NotSupportedException(); + } + } +} diff --git a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs index 648af8f082..6f6a53df66 100644 --- a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs +++ b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs @@ -253,6 +253,9 @@ namespace Umbraco.Cms.Core.DependencyInjection // register a basic/noop published snapshot service to be replaced Services.AddSingleton(); + + // Register ValueEditorCache used for validation + Services.AddSingleton(); } } } diff --git a/src/Umbraco.Core/IO/PhysicalFileSystem.cs b/src/Umbraco.Core/IO/PhysicalFileSystem.cs index 8c35e80988..db10cae416 100644 --- a/src/Umbraco.Core/IO/PhysicalFileSystem.cs +++ b/src/Umbraco.Core/IO/PhysicalFileSystem.cs @@ -258,16 +258,17 @@ namespace Umbraco.Cms.Core.IO // test URL var path = fullPathOrUrl.Replace('\\', '/'); // ensure URL separator char + // if it starts with the root path, strip it and trim the starting slash to make it relative + // eg "c:/websites/test/root/Media/1234/img.jpg" => "1234/img.jpg" + // or on unix systems "/var/wwwroot/test/Meia/1234/img.jpg" + if (_ioHelper.PathStartsWith(path, _rootPathFwd, '/')) + return path.Substring(_rootPathFwd.Length).TrimStart(Constants.CharArrays.ForwardSlash); + // if it starts with the root URL, strip it and trim the starting slash to make it relative // eg "/Media/1234/img.jpg" => "1234/img.jpg" if (_ioHelper.PathStartsWith(path, _rootUrl, '/')) return path.Substring(_rootUrl.Length).TrimStart(Constants.CharArrays.ForwardSlash); - // if it starts with the root path, strip it and trim the starting slash to make it relative - // eg "c:/websites/test/root/Media/1234/img.jpg" => "1234/img.jpg" - if (_ioHelper.PathStartsWith(path, _rootPathFwd, '/')) - return path.Substring(_rootPathFwd.Length).TrimStart(Constants.CharArrays.ForwardSlash); - // unchanged - what else? return path; } diff --git a/src/Umbraco.Core/IO/SystemFiles.cs b/src/Umbraco.Core/IO/SystemFiles.cs deleted file mode 100644 index 64199a1c51..0000000000 --- a/src/Umbraco.Core/IO/SystemFiles.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System.IO; -using Umbraco.Cms.Core.Hosting; - -namespace Umbraco.Cms.Core.IO -{ - public class SystemFiles - { - public static string TinyMceConfig => Constants.SystemDirectories.Config + "/tinyMceConfig.config"; - - // TODO: Kill this off we don't have umbraco.config XML cache we now have NuCache - public static string GetContentCacheXml(IHostingEnvironment hostingEnvironment) - { - return Path.Combine(hostingEnvironment.LocalTempPath, "umbraco.config"); - } - } -} diff --git a/src/Umbraco.Core/Models/Media.cs b/src/Umbraco.Core/Models/Media.cs index f5b3574be7..7a0264caf3 100644 --- a/src/Umbraco.Core/Models/Media.cs +++ b/src/Umbraco.Core/Models/Media.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Runtime.Serialization; namespace Umbraco.Cms.Core.Models @@ -15,9 +15,9 @@ namespace Umbraco.Cms.Core.Models /// /// name of the Media object /// Parent object - /// MediaType for the current Media object - public Media(string name, IMedia parent, IMediaType contentType) - : this(name, parent, contentType, new PropertyCollection()) + /// MediaType for the current Media object + public Media(string name, IMedia parent, IMediaType mediaType) + : this(name, parent, mediaType, new PropertyCollection()) { } /// @@ -25,10 +25,10 @@ namespace Umbraco.Cms.Core.Models /// /// name of the Media object /// Parent object - /// MediaType for the current Media object + /// MediaType for the current Media object /// Collection of properties - public Media(string name, IMedia parent, IMediaType contentType, IPropertyCollection properties) - : base(name, parent, contentType, properties) + public Media(string name, IMedia parent, IMediaType mediaType, IPropertyCollection properties) + : base(name, parent, mediaType, properties) { } /// @@ -36,9 +36,9 @@ namespace Umbraco.Cms.Core.Models /// /// name of the Media object /// Id of the Parent IMedia - /// MediaType for the current Media object - public Media(string name, int parentId, IMediaType contentType) - : this(name, parentId, contentType, new PropertyCollection()) + /// MediaType for the current Media object + public Media(string name, int parentId, IMediaType mediaType) + : this(name, parentId, mediaType, new PropertyCollection()) { } /// @@ -46,10 +46,10 @@ namespace Umbraco.Cms.Core.Models /// /// Name of the Media object /// Id of the Parent IMedia - /// MediaType for the current Media object + /// MediaType for the current Media object /// Collection of properties - public Media(string name, int parentId, IMediaType contentType, IPropertyCollection properties) - : base(name, parentId, contentType, properties) + public Media(string name, int parentId, IMediaType mediaType, IPropertyCollection properties) + : base(name, parentId, mediaType, properties) { } /// @@ -57,9 +57,9 @@ namespace Umbraco.Cms.Core.Models /// /// New MediaType for this Media /// Leaves PropertyTypes intact after change - internal void ChangeContentType(IMediaType contentType) + internal void ChangeContentType(IMediaType mediaType) { - ChangeContentType(contentType, false); + ChangeContentType(mediaType, false); } /// @@ -68,14 +68,14 @@ namespace Umbraco.Cms.Core.Models /// /// New MediaType for this Media /// Boolean indicating whether to clear PropertyTypes upon change - internal void ChangeContentType(IMediaType contentType, bool clearProperties) + internal void ChangeContentType(IMediaType mediaType, bool clearProperties) { - ChangeContentType(new SimpleContentType(contentType)); + ChangeContentType(new SimpleContentType(mediaType)); if (clearProperties) - Properties.EnsureCleanPropertyTypes(contentType.CompositionPropertyTypes); + Properties.EnsureCleanPropertyTypes(mediaType.CompositionPropertyTypes); else - Properties.EnsurePropertyTypes(contentType.CompositionPropertyTypes); + Properties.EnsurePropertyTypes(mediaType.CompositionPropertyTypes); Properties.ClearCollectionChangedEvents(); // be sure not to double add Properties.CollectionChanged += PropertiesChanged; diff --git a/src/Umbraco.Core/PropertyEditors/DataEditor.cs b/src/Umbraco.Core/PropertyEditors/DataEditor.cs index c8b9f3a51f..e2fc0071be 100644 --- a/src/Umbraco.Core/PropertyEditors/DataEditor.cs +++ b/src/Umbraco.Core/PropertyEditors/DataEditor.cs @@ -21,7 +21,6 @@ namespace Umbraco.Cms.Core.PropertyEditors public class DataEditor : IDataEditor { private IDictionary _defaultConfiguration; - private IDataValueEditor _reusableEditor; /// /// Initializes a new instance of the class. @@ -90,8 +89,7 @@ namespace Umbraco.Cms.Core.PropertyEditors /// simple enough for now. /// // TODO: point of that one? shouldn't we always configure? - public IDataValueEditor GetValueEditor() => ExplicitValueEditor ?? (_reusableEditor ?? (_reusableEditor = CreateValueEditor())); - + public IDataValueEditor GetValueEditor() => ExplicitValueEditor ?? CreateValueEditor(); /// /// diff --git a/src/Umbraco.Infrastructure/Cache/DistributedCacheBinder_Handlers.cs b/src/Umbraco.Infrastructure/Cache/DistributedCacheBinder_Handlers.cs index 35845f3cd0..6e6f549b03 100644 --- a/src/Umbraco.Infrastructure/Cache/DistributedCacheBinder_Handlers.cs +++ b/src/Umbraco.Infrastructure/Cache/DistributedCacheBinder_Handlers.cs @@ -130,6 +130,7 @@ namespace Umbraco.Cms.Core.Cache { _distributedCache.RefreshDataTypeCache(entity); } + _distributedCache.RefreshValueEditorCache(notification.SavedEntities); } public void Handle(DataTypeDeletedNotification notification) @@ -138,6 +139,7 @@ namespace Umbraco.Cms.Core.Cache { _distributedCache.RemoveDataTypeCache(entity); } + _distributedCache.RefreshValueEditorCache(notification.DeletedEntities); } #endregion diff --git a/src/Umbraco.Infrastructure/Cache/DistributedCacheExtensions.cs b/src/Umbraco.Infrastructure/Cache/DistributedCacheExtensions.cs index a0ba0ff128..ceac767a8c 100644 --- a/src/Umbraco.Infrastructure/Cache/DistributedCacheExtensions.cs +++ b/src/Umbraco.Infrastructure/Cache/DistributedCacheExtensions.cs @@ -1,6 +1,7 @@ // Copyright (c) Umbraco. // See LICENSE for more details. +using System.Collections.Generic; using System.Linq; using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Models; @@ -106,6 +107,21 @@ namespace Umbraco.Extensions #endregion + #region ValueEditorCache + + public static void RefreshValueEditorCache(this DistributedCache dc, IEnumerable dataTypes) + { + if (dataTypes is null) + { + return; + } + + var payloads = dataTypes.Select(x => new DataTypeCacheRefresher.JsonPayload(x.Id, x.Key, false)); + dc.RefreshByPayload(ValueEditorCacheRefresher.UniqueId, payloads); + } + + #endregion + #region ContentCache public static void RefreshAllContentCache(this DistributedCache dc) diff --git a/src/Umbraco.Infrastructure/HostedServices/KeepAlive.cs b/src/Umbraco.Infrastructure/HostedServices/KeepAlive.cs index 40c250f86e..644b2c278f 100644 --- a/src/Umbraco.Infrastructure/HostedServices/KeepAlive.cs +++ b/src/Umbraco.Infrastructure/HostedServices/KeepAlive.cs @@ -33,7 +33,7 @@ namespace Umbraco.Cms.Infrastructure.HostedServices /// /// Initializes a new instance of the class. /// - /// Accessor for the current request. + /// The current hosting environment /// Representation of the main application domain. /// The configuration for keep alive settings. /// The typed logger. @@ -86,7 +86,7 @@ namespace Umbraco.Cms.Infrastructure.HostedServices using (_profilingLogger.DebugDuration("Keep alive executing", "Keep alive complete")) { - var umbracoAppUrl = _hostingEnvironment.ApplicationMainUrl.ToString(); + var umbracoAppUrl = _hostingEnvironment.ApplicationMainUrl?.ToString(); if (umbracoAppUrl.IsNullOrWhiteSpace()) { _logger.LogWarning("No umbracoApplicationUrl for service (yet), skip."); diff --git a/src/Umbraco.Infrastructure/Persistence/LocalDb.cs b/src/Umbraco.Infrastructure/Persistence/LocalDb.cs index 0ca98d2b14..0dbe84b07a 100644 --- a/src/Umbraco.Infrastructure/Persistence/LocalDb.cs +++ b/src/Umbraco.Infrastructure/Persistence/LocalDb.cs @@ -927,6 +927,7 @@ namespace Umbraco.Cms.Infrastructure.Persistence return p.ExitCode; } + } /// diff --git a/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorPropertyHandler.cs b/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorPropertyHandler.cs index 24c9b37cde..9a7ecdbf29 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorPropertyHandler.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorPropertyHandler.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Linq; +using Microsoft.Extensions.Logging; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using Umbraco.Cms.Core.Models.Blocks; @@ -17,6 +18,12 @@ namespace Umbraco.Cms.Core.PropertyEditors public class BlockEditorPropertyHandler : ComplexPropertyEditorContentNotificationHandler { private readonly BlockListEditorDataConverter _converter = new BlockListEditorDataConverter(); + private readonly ILogger _logger; + + public BlockEditorPropertyHandler(ILogger logger) + { + _logger = logger; + } protected override string EditorAlias => Constants.PropertyEditors.Aliases.BlockList; @@ -110,8 +117,22 @@ namespace Umbraco.Cms.Core.PropertyEditors // this gets a little ugly because there could be some other complex editor that contains another block editor // and since we would have no idea how to parse that, all we can do is try JSON Path to find another block editor // of our type - var json = JToken.Parse(asString); - if (ProcessJToken(json, createGuid, out var result)) + JToken json = null; + try + { + json = JToken.Parse(asString); + } + catch (Exception e) + { + // See issue https://github.com/umbraco/Umbraco-CMS/issues/10879 + // We are detecting JSON data by seeing if a string is surrounded by [] or {} + // If people enter text like [PLACEHOLDER] JToken parsing fails, it's safe to ignore though + // Logging this just in case in the future we find values that are not safe to ignore + _logger.LogWarning( "The property {PropertyAlias} on content type {ContentTypeKey} has a value of: {BlockItemValue} - this was recognized as JSON but could not be parsed", + data.Key, propertyAliasToBlockItemData.Key, asString); + } + + if (json != null && ProcessJToken(json, createGuid, out var result)) { // need to re-save this back to the RawPropertyValues data.RawPropertyValues[propertyAliasToBlockItemData.Key] = result; diff --git a/src/Umbraco.Infrastructure/Services/Implement/PropertyValidationService.cs b/src/Umbraco.Infrastructure/Services/Implement/PropertyValidationService.cs index 5fd7971976..70580153de 100644 --- a/src/Umbraco.Infrastructure/Services/Implement/PropertyValidationService.cs +++ b/src/Umbraco.Infrastructure/Services/Implement/PropertyValidationService.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Linq; +using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.PropertyEditors; using Umbraco.Extensions; @@ -13,12 +14,18 @@ namespace Umbraco.Cms.Core.Services.Implement private readonly PropertyEditorCollection _propertyEditors; private readonly IDataTypeService _dataTypeService; private readonly ILocalizedTextService _textService; + private readonly IValueEditorCache _valueEditorCache; - public PropertyValidationService(PropertyEditorCollection propertyEditors, IDataTypeService dataTypeService, ILocalizedTextService textService) + public PropertyValidationService( + PropertyEditorCollection propertyEditors, + IDataTypeService dataTypeService, + ILocalizedTextService textService, + IValueEditorCache valueEditorCache) { _propertyEditors = propertyEditors; _dataTypeService = dataTypeService; _textService = textService; + _valueEditorCache = valueEditorCache; } /// @@ -58,7 +65,7 @@ namespace Umbraco.Cms.Core.Services.Implement _textService.Localize("validation", "invalidPattern"), }; - var valueEditor = editor.GetValueEditor(dataType.Configuration); + IDataValueEditor valueEditor = _valueEditorCache.GetValueEditor(editor, dataType); foreach (var validationResult in valueEditor.Validate(postedValue, isRequired, validationRegExp)) { // If we've got custom error messages, we'll replace the default ones that will have been applied in the call to Validate(). diff --git a/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentServiceTests.cs b/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentServiceTests.cs index 60441a013d..d10e269fad 100644 --- a/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentServiceTests.cs +++ b/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentServiceTests.cs @@ -9,6 +9,7 @@ using System.Threading; using Moq; using NUnit.Framework; using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; @@ -72,6 +73,8 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services private IJsonSerializer Serializer => GetRequiredService(); + private IValueEditorCache ValueEditorCache => GetRequiredService(); + [SetUp] public void Setup() => ContentRepositoryBase.ThrowOnWarning = true; @@ -1162,7 +1165,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services Assert.IsFalse(content.HasIdentity); // content cannot publish values because they are invalid - var propertyValidationService = new PropertyValidationService(PropertyEditorCollection, DataTypeService, TextService); + var propertyValidationService = new PropertyValidationService(PropertyEditorCollection, DataTypeService, TextService, ValueEditorCache); bool isValid = propertyValidationService.IsPropertyDataValid(content, out IProperty[] invalidProperties, CultureImpact.Invariant); Assert.IsFalse(isValid); Assert.IsNotEmpty(invalidProperties); diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Core/Cache/ValueEditorCacheTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Core/Cache/ValueEditorCacheTests.cs new file mode 100644 index 0000000000..94cd9924f9 --- /dev/null +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Core/Cache/ValueEditorCacheTests.cs @@ -0,0 +1,133 @@ +using System.Collections.Generic; +using Moq; +using NUnit.Framework; +using Umbraco.Cms.Core.Cache; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.PropertyEditors; + +namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.Cache +{ + [TestFixture] + public class ValueEditorCacheTests + { + + [Test] + public void Caches_ValueEditor() + { + var sut = new ValueEditorCache(); + + var dataEditor = new FakeDataEditor("TestEditor"); + IDataType dataType = CreateDataTypeMock(1).Object; + + // Request the same value editor twice + IDataValueEditor firstEditor = sut.GetValueEditor(dataEditor, dataType); + IDataValueEditor secondEditor = sut.GetValueEditor(dataEditor, dataType); + + Assert.Multiple(() => + { + Assert.AreSame(firstEditor, secondEditor); + Assert.AreEqual(1, dataEditor.ValueEditorCount, "GetValueEditor invoked more than once."); + }); + } + + [Test] + public void Different_Data_Editors_Returns_Different_Value_Editors() + { + var sut = new ValueEditorCache(); + + var dataEditor1 = new FakeDataEditor("Editor1"); + var dataEditor2 = new FakeDataEditor("Editor2"); + IDataType dataType = CreateDataTypeMock(1).Object; + + IDataValueEditor firstEditor = sut.GetValueEditor(dataEditor1, dataType); + IDataValueEditor secondEditor = sut.GetValueEditor(dataEditor2, dataType); + + Assert.AreNotSame(firstEditor, secondEditor); + } + + [Test] + public void Different_Data_Types_Returns_Different_Value_Editors() + { + var sut = new ValueEditorCache(); + var dataEditor = new FakeDataEditor("Editor"); + IDataType dataType1 = CreateDataTypeMock(1).Object; + IDataType dataType2 = CreateDataTypeMock(2).Object; + + IDataValueEditor firstEditor = sut.GetValueEditor(dataEditor, dataType1); + IDataValueEditor secondEditor = sut.GetValueEditor(dataEditor, dataType2); + + Assert.AreNotSame(firstEditor, secondEditor); + } + + [Test] + public void Clear_Cache_Removes_Specific_Editors() + { + var sut = new ValueEditorCache(); + + var dataEditor1 = new FakeDataEditor("Editor 1"); + var dataEditor2 = new FakeDataEditor("Editor 2"); + + IDataType dataType1 = CreateDataTypeMock(1).Object; + IDataType dataType2 = CreateDataTypeMock(2).Object; + + // Load the editors into cache + IDataValueEditor editor1DataType1 = sut.GetValueEditor(dataEditor1, dataType1); + IDataValueEditor editor1Datatype2 = sut.GetValueEditor(dataEditor1, dataType2); + IDataValueEditor editor2DataType1 = sut.GetValueEditor(dataEditor2, dataType1); + IDataValueEditor editor2Datatype2 = sut.GetValueEditor(dataEditor2, dataType2); + + sut.ClearCache(new []{dataType1.Id}); + + // New value editor objects should be created after it's cleared + Assert.AreNotSame(editor1DataType1, sut.GetValueEditor(dataEditor1, dataType1), "Value editor was not cleared from cache"); + Assert.AreNotSame(editor2DataType1, sut.GetValueEditor(dataEditor2, dataType1), "Value editor was not cleared from cache"); + + // But the value editors for data type 2 should be the same + Assert.AreSame(editor1Datatype2, sut.GetValueEditor(dataEditor1, dataType2), "Too many editors was cleared from cache"); + Assert.AreSame(editor2Datatype2, sut.GetValueEditor(dataEditor2, dataType2), "Too many editors was cleared from cache"); + } + + + private Mock CreateDataTypeMock(int id) + { + var mock = new Mock(); + mock.Setup(x => x.Id).Returns(id); + return mock; + } + + /// + /// A fake IDataEditor + /// + /// + /// This is necessary to ensure that different objects are returned from GetValueEditor + /// + private class FakeDataEditor : IDataEditor + { + public FakeDataEditor(string alias) + { + Alias = alias; + } + + public string Alias { get; } + public EditorType Type { get; } + public string Name { get; } + public string Icon { get; } + public string Group { get; } + public bool IsDeprecated { get; } + + public IDataValueEditor GetValueEditor() + { + ValueEditorCount++; + return Mock.Of(); + } + public IDataValueEditor GetValueEditor(object configuration) => GetValueEditor(); + + public IDictionary DefaultConfiguration { get; } + public IConfigurationEditor GetConfigurationEditor() => throw new System.NotImplementedException(); + + public IPropertyIndexValueFactory PropertyIndexValueFactory { get; } + + public int ValueEditorCount; + } + } +} diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Core/Models/VariationTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Core/Models/VariationTests.cs index 892ef696c3..c2d5b785c9 100644 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Core/Models/VariationTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Core/Models/VariationTests.cs @@ -6,6 +6,7 @@ using Microsoft.Extensions.Logging.Abstractions; using Moq; using NUnit.Framework; using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Hosting; using Umbraco.Cms.Core.IO; using Umbraco.Cms.Core.Models; @@ -603,7 +604,8 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.Models return new PropertyValidationService( propertyEditorCollection, dataTypeService, - localizedTextService); + localizedTextService, + new ValueEditorCache()); } } } diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/BlockEditorComponentTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/BlockEditorComponentTests.cs index 590ff58222..b76719888f 100644 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/BlockEditorComponentTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/BlockEditorComponentTests.cs @@ -4,6 +4,8 @@ using System; using System.Collections.Generic; using System.Linq; +using Microsoft.Extensions.Logging; +using Moq; using Newtonsoft.Json; using NUnit.Framework; using Umbraco.Cms.Core; @@ -28,11 +30,15 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.PropertyEditors private const string SubContentGuid1 = "4c44ce6b3a5c4f5f8f15e3dc24819a9e"; private const string SubContentGuid2 = "a062c06d6b0b44ac892b35d90309c7f8"; private const string SubSettingsGuid1 = "4d998d980ffa4eee8afdc23c4abd6d29"; + private static readonly ILogger s_logger = Mock.Of>(); + + + [Test] public void Cannot_Have_Null_Udi() { - var component = new BlockEditorPropertyHandler(); + var component = new BlockEditorPropertyHandler(s_logger); var json = GetBlockListJson(null, string.Empty); Assert.Throws(() => component.ReplaceBlockListUdis(json)); } @@ -48,7 +54,7 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.PropertyEditors var expected = ReplaceGuids(json, guids, ContentGuid1, ContentGuid2, SettingsGuid1); - var component = new BlockEditorPropertyHandler(); + var component = new BlockEditorPropertyHandler(s_logger); var result = component.ReplaceBlockListUdis(json, GuidFactory); var expectedJson = JsonConvert.SerializeObject(JsonConvert.DeserializeObject(expected, _serializerSettings), _serializerSettings); @@ -75,7 +81,7 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.PropertyEditors // get the json with the subFeatures as escaped var json = GetBlockListJson(innerJsonEscaped); - var component = new BlockEditorPropertyHandler(); + var component = new BlockEditorPropertyHandler(s_logger); var result = component.ReplaceBlockListUdis(json, GuidFactory); // the expected result is that the subFeatures data is no longer escaped @@ -119,7 +125,7 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.PropertyEditors SubContentGuid2, SubSettingsGuid1); - var component = new BlockEditorPropertyHandler(); + var component = new BlockEditorPropertyHandler(s_logger); var result = component.ReplaceBlockListUdis(json, GuidFactory); var expectedJson = JsonConvert.SerializeObject(JsonConvert.DeserializeObject(expected, _serializerSettings), _serializerSettings); @@ -147,7 +153,7 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.PropertyEditors var json = GetBlockListJson(complexEditorJsonEscaped); - var component = new BlockEditorPropertyHandler(); + var component = new BlockEditorPropertyHandler(s_logger); var result = component.ReplaceBlockListUdis(json, GuidFactory); // the expected result is that the subFeatures data is no longer escaped diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Services/PropertyValidationServiceTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Services/PropertyValidationServiceTests.cs index bfbe6258cc..942ba8cf4a 100644 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Services/PropertyValidationServiceTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Services/PropertyValidationServiceTests.cs @@ -5,6 +5,7 @@ using System.Threading; using Moq; using NUnit.Framework; using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Hosting; using Umbraco.Cms.Core.IO; using Umbraco.Cms.Core.Models; @@ -45,7 +46,7 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Services var propEditors = new PropertyEditorCollection(new DataEditorCollection(() => new[] { dataEditor })); - validationService = new PropertyValidationService(propEditors, dataTypeService.Object, Mock.Of()); + validationService = new PropertyValidationService(propEditors, dataTypeService.Object, Mock.Of(), new ValueEditorCache()); } [Test] diff --git a/src/Umbraco.Web.Common/Extensions/FriendlyImageCropperTemplateExtensions.cs b/src/Umbraco.Web.Common/Extensions/FriendlyImageCropperTemplateExtensions.cs index 38f6f47235..22ddc15511 100644 --- a/src/Umbraco.Web.Common/Extensions/FriendlyImageCropperTemplateExtensions.cs +++ b/src/Umbraco.Web.Common/Extensions/FriendlyImageCropperTemplateExtensions.cs @@ -22,16 +22,18 @@ namespace Umbraco.Extensions /// /// The IPublishedContent item. /// The crop alias e.g. thumbnail. + /// The url mode. /// /// The URL of the cropped image. /// public static string GetCropUrl( this IPublishedContent mediaItem, - string cropAlias) => - mediaItem.GetCropUrl(cropAlias, ImageUrlGenerator, PublishedValueFallback, PublishedUrlProvider); + string cropAlias, + UrlMode urlMode = UrlMode.Default) => + mediaItem.GetCropUrl(cropAlias, ImageUrlGenerator, PublishedValueFallback, PublishedUrlProvider, urlMode); - public static string GetCropUrl(this MediaWithCrops mediaWithCrops, string cropAlias) - => ImageCropperTemplateCoreExtensions.GetCropUrl(mediaWithCrops, cropAlias, ImageUrlGenerator, PublishedValueFallback, PublishedUrlProvider); + public static string GetCropUrl(this MediaWithCrops mediaWithCrops, string cropAlias, UrlMode urlMode = UrlMode.Default) + => ImageCropperTemplateCoreExtensions.GetCropUrl(mediaWithCrops, cropAlias, ImageUrlGenerator, PublishedValueFallback, PublishedUrlProvider, urlMode); /// /// Gets the crop URL by using only the specified . @@ -39,14 +41,16 @@ namespace Umbraco.Extensions /// The media item. /// The image cropper value. /// The crop alias. + /// The url mode. /// /// The image crop URL. /// public static string GetCropUrl( this IPublishedContent mediaItem, ImageCropperValue imageCropperValue, - string cropAlias) - => ImageCropperTemplateCoreExtensions.GetCropUrl(mediaItem, imageCropperValue, cropAlias, ImageUrlGenerator, PublishedValueFallback, PublishedUrlProvider); + string cropAlias, + UrlMode urlMode = UrlMode.Default) + => ImageCropperTemplateCoreExtensions.GetCropUrl(mediaItem, imageCropperValue, cropAlias, ImageUrlGenerator, PublishedValueFallback, PublishedUrlProvider, urlMode); /// /// Gets the underlying image processing service URL by the crop alias using the specified property containing the image cropper JSON data on the IPublishedContent item. @@ -54,17 +58,19 @@ namespace Umbraco.Extensions /// The IPublishedContent item. /// The property alias of the property containing the JSON data e.g. umbracoFile. /// The crop alias e.g. thumbnail. + /// The url mode. /// /// The URL of the cropped image. /// public static string GetCropUrl( this IPublishedContent mediaItem, string propertyAlias, - string cropAlias) => - mediaItem.GetCropUrl(propertyAlias, cropAlias, ImageUrlGenerator, PublishedValueFallback, PublishedUrlProvider); + string cropAlias, + UrlMode urlMode = UrlMode.Default) => + mediaItem.GetCropUrl(propertyAlias, cropAlias, ImageUrlGenerator, PublishedValueFallback, PublishedUrlProvider, urlMode); - public static string GetCropUrl(this MediaWithCrops mediaWithCrops, string propertyAlias, string cropAlias) - => ImageCropperTemplateCoreExtensions.GetCropUrl(mediaWithCrops, propertyAlias, cropAlias, ImageUrlGenerator, PublishedValueFallback, PublishedUrlProvider); + public static string GetCropUrl(this MediaWithCrops mediaWithCrops, string propertyAlias, string cropAlias, UrlMode urlMode = UrlMode.Default) + => ImageCropperTemplateCoreExtensions.GetCropUrl(mediaWithCrops, propertyAlias, cropAlias, ImageUrlGenerator, PublishedValueFallback, PublishedUrlProvider, urlMode); /// /// Gets the underlying image processing service URL from the IPublishedContent item. @@ -84,6 +90,7 @@ namespace Umbraco.Extensions /// + /// The url mode. /// /// The URL of the cropped image. /// @@ -99,7 +106,8 @@ namespace Umbraco.Extensions bool preferFocalPoint = false, bool useCropDimensions = false, bool cacheBuster = true, - string furtherOptions = null) + string furtherOptions = null, + UrlMode urlMode = UrlMode.Default) => mediaItem.GetCropUrl( ImageUrlGenerator, PublishedValueFallback, @@ -114,7 +122,8 @@ namespace Umbraco.Extensions preferFocalPoint, useCropDimensions, cacheBuster, - furtherOptions + furtherOptions, + urlMode ); /// @@ -215,7 +224,7 @@ namespace Umbraco.Extensions ); - [Obsolete("Use GetCrop to merge local and media crops, get automatic cache buster value and have more parameters.")] + [Obsolete("Use GetCropUrl to merge local and media crops, get automatic cache buster value and have more parameters.")] public static string GetLocalCropUrl( this MediaWithCrops mediaWithCrops, string alias, diff --git a/src/Umbraco.Web.Common/Extensions/ImageCropperTemplateCoreExtensions.cs b/src/Umbraco.Web.Common/Extensions/ImageCropperTemplateCoreExtensions.cs index ae367b1cf9..a3e96ebebb 100644 --- a/src/Umbraco.Web.Common/Extensions/ImageCropperTemplateCoreExtensions.cs +++ b/src/Umbraco.Web.Common/Extensions/ImageCropperTemplateCoreExtensions.cs @@ -20,6 +20,7 @@ namespace Umbraco.Extensions /// The image URL generator. /// The published value fallback. /// The published URL provider. + /// The url mode. /// /// The URL of the cropped image. /// @@ -28,14 +29,16 @@ namespace Umbraco.Extensions string cropAlias, IImageUrlGenerator imageUrlGenerator, IPublishedValueFallback publishedValueFallback, - IPublishedUrlProvider publishedUrlProvider) => mediaItem.GetCropUrl(imageUrlGenerator, publishedValueFallback, publishedUrlProvider, cropAlias: cropAlias, useCropDimensions: true); + IPublishedUrlProvider publishedUrlProvider, + UrlMode urlMode = UrlMode.Default) => mediaItem.GetCropUrl(imageUrlGenerator, publishedValueFallback, publishedUrlProvider, cropAlias: cropAlias, useCropDimensions: true, urlMode: urlMode); public static string GetCropUrl( this MediaWithCrops mediaWithCrops, string cropAlias, IImageUrlGenerator imageUrlGenerator, IPublishedValueFallback publishedValueFallback, - IPublishedUrlProvider publishedUrlProvider) => mediaWithCrops.GetCropUrl(imageUrlGenerator, publishedValueFallback, publishedUrlProvider, cropAlias: cropAlias, useCropDimensions: true); + IPublishedUrlProvider publishedUrlProvider, + UrlMode urlMode = UrlMode.Default) => mediaWithCrops.GetCropUrl(imageUrlGenerator, publishedValueFallback, publishedUrlProvider, cropAlias: cropAlias, useCropDimensions: true, urlMode: urlMode); /// /// Gets the crop URL by using only the specified . @@ -46,6 +49,7 @@ namespace Umbraco.Extensions /// The image URL generator. /// The published value fallback. /// The published URL provider. + /// The url mode.s /// /// The image crop URL. /// @@ -55,7 +59,8 @@ namespace Umbraco.Extensions string cropAlias, IImageUrlGenerator imageUrlGenerator, IPublishedValueFallback publishedValueFallback, - IPublishedUrlProvider publishedUrlProvider) => mediaItem.GetCropUrl(imageUrlGenerator, publishedValueFallback, publishedUrlProvider, imageCropperValue, true, cropAlias: cropAlias, useCropDimensions: true); + IPublishedUrlProvider publishedUrlProvider, + UrlMode urlMode = UrlMode.Default) => mediaItem.GetCropUrl(imageUrlGenerator, publishedValueFallback, publishedUrlProvider, imageCropperValue, true, cropAlias: cropAlias, useCropDimensions: true, urlMode: urlMode); /// /// Gets the underlying image processing service URL by the crop alias using the specified property containing the image cropper JSON data on the IPublishedContent item. @@ -66,6 +71,7 @@ namespace Umbraco.Extensions /// The image URL generator. /// The published value fallback. /// The published URL provider. + /// The url mode. /// /// The URL of the cropped image. /// @@ -75,14 +81,16 @@ namespace Umbraco.Extensions string cropAlias, IImageUrlGenerator imageUrlGenerator, IPublishedValueFallback publishedValueFallback, - IPublishedUrlProvider publishedUrlProvider) => mediaItem.GetCropUrl(imageUrlGenerator, publishedValueFallback, publishedUrlProvider, propertyAlias: propertyAlias, cropAlias: cropAlias, useCropDimensions: true); + IPublishedUrlProvider publishedUrlProvider, + UrlMode urlMode = UrlMode.Default) => mediaItem.GetCropUrl(imageUrlGenerator, publishedValueFallback, publishedUrlProvider, propertyAlias: propertyAlias, cropAlias: cropAlias, useCropDimensions: true, urlMode: urlMode); public static string GetCropUrl(this MediaWithCrops mediaWithCrops, IPublishedValueFallback publishedValueFallback, IPublishedUrlProvider publishedUrlProvider, string propertyAlias, string cropAlias, - IImageUrlGenerator imageUrlGenerator) => mediaWithCrops.GetCropUrl(imageUrlGenerator, publishedValueFallback, publishedUrlProvider, propertyAlias: propertyAlias, cropAlias: cropAlias, useCropDimensions: true); + IImageUrlGenerator imageUrlGenerator, + UrlMode urlMode = UrlMode.Default) => mediaWithCrops.GetCropUrl(imageUrlGenerator, publishedValueFallback, publishedUrlProvider, propertyAlias: propertyAlias, cropAlias: cropAlias, useCropDimensions: true, urlMode: urlMode); /// /// Gets the underlying image processing service URL from the IPublishedContent item. @@ -105,6 +113,7 @@ namespace Umbraco.Extensions /// + /// The url mode. /// /// The URL of the cropped image. /// @@ -123,7 +132,8 @@ namespace Umbraco.Extensions bool preferFocalPoint = false, bool useCropDimensions = false, bool cacheBuster = true, - string furtherOptions = null) => mediaItem.GetCropUrl(imageUrlGenerator, publishedValueFallback, publishedUrlProvider, null, false, width, height, propertyAlias, cropAlias, quality, imageCropMode, imageCropAnchor, preferFocalPoint, useCropDimensions, cacheBuster, furtherOptions); + string furtherOptions = null, + UrlMode urlMode = UrlMode.Default) => mediaItem.GetCropUrl(imageUrlGenerator, publishedValueFallback, publishedUrlProvider, null, false, width, height, propertyAlias, cropAlias, quality, imageCropMode, imageCropAnchor, preferFocalPoint, useCropDimensions, cacheBuster, furtherOptions, urlMode); public static string GetCropUrl( this MediaWithCrops mediaWithCrops, @@ -140,14 +150,15 @@ namespace Umbraco.Extensions bool preferFocalPoint = false, bool useCropDimensions = false, bool cacheBuster = true, - string furtherOptions = null) + string furtherOptions = null, + UrlMode urlMode = UrlMode.Default) { if (mediaWithCrops == null) { throw new ArgumentNullException(nameof(mediaWithCrops)); } - return mediaWithCrops.Content.GetCropUrl(imageUrlGenerator, publishedValueFallback, publishedUrlProvider, mediaWithCrops.LocalCrops, false, width, height, propertyAlias, cropAlias, quality, imageCropMode, imageCropAnchor, preferFocalPoint, useCropDimensions, cacheBuster, furtherOptions); + return mediaWithCrops.Content.GetCropUrl(imageUrlGenerator, publishedValueFallback, publishedUrlProvider, mediaWithCrops.LocalCrops, false, width, height, propertyAlias, cropAlias, quality, imageCropMode, imageCropAnchor, preferFocalPoint, useCropDimensions, cacheBuster, furtherOptions, urlMode); } private static string GetCropUrl( @@ -167,7 +178,8 @@ namespace Umbraco.Extensions bool preferFocalPoint = false, bool useCropDimensions = false, bool cacheBuster = true, - string furtherOptions = null) + string furtherOptions = null, + UrlMode urlMode = UrlMode.Default) { if (mediaItem == null) { @@ -179,7 +191,7 @@ namespace Umbraco.Extensions return null; } - var mediaItemUrl = mediaItem.MediaUrl(publishedUrlProvider, propertyAlias: propertyAlias); + var mediaItemUrl = mediaItem.MediaUrl(publishedUrlProvider, propertyAlias: propertyAlias, mode: urlMode); // Only get crops from media when required and used if (localCropsOnly == false && (imageCropMode == ImageCropMode.Crop || imageCropMode == null)) diff --git a/src/Umbraco.Web.Common/Extensions/UrlHelperExtensions.cs b/src/Umbraco.Web.Common/Extensions/UrlHelperExtensions.cs index aefd102725..ee45db39c9 100644 --- a/src/Umbraco.Web.Common/Extensions/UrlHelperExtensions.cs +++ b/src/Umbraco.Web.Common/Extensions/UrlHelperExtensions.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Linq.Expressions; @@ -209,27 +208,27 @@ namespace Umbraco.Extensions return $"{version}.{runtimeMinifier.CacheBuster}".GenerateHash(); } - public static IHtmlContent GetCropUrl(this IUrlHelper urlHelper, IPublishedContent mediaItem, string cropAlias, bool htmlEncode = true) + public static IHtmlContent GetCropUrl(this IUrlHelper urlHelper, IPublishedContent mediaItem, string cropAlias, bool htmlEncode = true, UrlMode urlMode = UrlMode.Default) { if (mediaItem == null) { return HtmlString.Empty; } - var url = mediaItem.GetCropUrl(cropAlias: cropAlias, useCropDimensions: true); + var url = mediaItem.GetCropUrl(cropAlias: cropAlias, useCropDimensions: true, urlMode: urlMode); return CreateHtmlString(url, htmlEncode); } private static IHtmlContent CreateHtmlString(string url, bool htmlEncode) => htmlEncode ? new HtmlString(HttpUtility.HtmlEncode(url)) : new HtmlString(url); - public static IHtmlContent GetCropUrl(this IUrlHelper urlHelper, IPublishedContent mediaItem, string propertyAlias, string cropAlias, bool htmlEncode = true) + public static IHtmlContent GetCropUrl(this IUrlHelper urlHelper, IPublishedContent mediaItem, string propertyAlias, string cropAlias, bool htmlEncode = true, UrlMode urlMode = UrlMode.Default) { if (mediaItem == null) { return HtmlString.Empty; } - var url = mediaItem.GetCropUrl(propertyAlias: propertyAlias, cropAlias: cropAlias, useCropDimensions: true); + var url = mediaItem.GetCropUrl(propertyAlias: propertyAlias, cropAlias: cropAlias, useCropDimensions: true, urlMode: urlMode); return CreateHtmlString(url, htmlEncode); } @@ -246,7 +245,8 @@ namespace Umbraco.Extensions bool useCropDimensions = false, bool cacheBuster = true, string furtherOptions = null, - bool htmlEncode = true) + bool htmlEncode = true, + UrlMode urlMode = UrlMode.Default) { if (mediaItem == null) { @@ -254,7 +254,7 @@ namespace Umbraco.Extensions } var url = mediaItem.GetCropUrl(width, height, propertyAlias, cropAlias, quality, imageCropMode, - imageCropAnchor, preferFocalPoint, useCropDimensions, cacheBuster, furtherOptions); + imageCropAnchor, preferFocalPoint, useCropDimensions, cacheBuster, furtherOptions, urlMode); return CreateHtmlString(url, htmlEncode); } diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/tree/umbtree.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/tree/umbtree.directive.js index 358223901e..a39475f4da 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/tree/umbtree.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/tree/umbtree.directive.js @@ -3,7 +3,7 @@ * @name umbraco.directives.directive:umbTree * @restrict E **/ -function umbTreeDirective($q, treeService, navigationService, notificationsService) { +function umbTreeDirective($q, treeService, notificationsService) { return { restrict: 'E', @@ -357,8 +357,6 @@ function umbTreeDirective($q, treeService, navigationService, notificationsServi defined on the tree */ $scope.select = function (n, ev) { - - navigationService.hideMenu(); if (n.metaData && n.metaData.noAccess === true) { ev.preventDefault(); diff --git a/src/Umbraco.Web.UI.Client/src/common/services/navigation.service.js b/src/Umbraco.Web.UI.Client/src/common/services/navigation.service.js index 88894ac47a..b6609a73fc 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/navigation.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/navigation.service.js @@ -127,7 +127,7 @@ function navigationService($routeParams, $location, $q, $injector, eventsService var aboveClass = "above-backdrop"; var leftColumn = document.getElementById("leftcolumn"); - if(leftColumn) { + if (leftColumn) { var isLeftColumnOnTop = leftColumn.classList.contains(aboveClass); if (isLeftColumnOnTop) { diff --git a/src/Umbraco.Web.UI.Client/src/less/property-editors.less b/src/Umbraco.Web.UI.Client/src/less/property-editors.less index 87603671dd..b735b6d7e4 100644 --- a/src/Umbraco.Web.UI.Client/src/less/property-editors.less +++ b/src/Umbraco.Web.UI.Client/src/less/property-editors.less @@ -585,7 +585,6 @@ display: inline-block; position: relative; vertical-align: top; - flex:0; } .umb-fileupload, diff --git a/src/Umbraco.Web.UI/umbraco/config/lang/da.xml b/src/Umbraco.Web.UI/umbraco/config/lang/da.xml index 9f7ea3c822..cbd3457b65 100644 --- a/src/Umbraco.Web.UI/umbraco/config/lang/da.xml +++ b/src/Umbraco.Web.UI/umbraco/config/lang/da.xml @@ -2044,4 +2044,8 @@ Mange hilsner fra Umbraco robotten Filskrivning Medie mappeoprettelse + + resultat + resultater +