diff --git a/build/azure-pipelines.yml b/build/azure-pipelines.yml index b088c273f2..c8e370d513 100644 --- a/build/azure-pipelines.yml +++ b/build/azure-pipelines.yml @@ -150,10 +150,9 @@ stages: vmImage: 'windows-latest' steps: - task: UseDotNet@2 - displayName: Use .NET $(dotnetVersion) + displayName: Use .NET SDK from global.json inputs: - version: $(dotnetVersion) - includePreviewVersions: $(dotnetIncludePreviewVersions) + useGlobalJson: true - task: PowerShell@2 displayName: Install DocFX inputs: diff --git a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs index c8ef1fbff7..885bc81a4b 100644 --- a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs +++ b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs @@ -317,8 +317,9 @@ namespace Umbraco.Cms.Core.DependencyInjection Services.AddSingleton(); Services.AddSingleton(); - // Register a noop IHtmlSanitizer to be replaced + // Register a noop IHtmlSanitizer & IMarkdownSanitizer to be replaced Services.AddUnique(); + Services.AddUnique(); Services.AddUnique(); Services.AddUnique(); diff --git a/src/Umbraco.Core/EmbeddedResources/Lang/da.xml b/src/Umbraco.Core/EmbeddedResources/Lang/da.xml index 1a69568c79..15cbe5f30a 100644 --- a/src/Umbraco.Core/EmbeddedResources/Lang/da.xml +++ b/src/Umbraco.Core/EmbeddedResources/Lang/da.xml @@ -1015,6 +1015,8 @@ Opret header Leverancer Der er ikke tilføjet nogen webhook headers + Skift til debug mode for mere information. + Ikke OK status kode. Culture Code diff --git a/src/Umbraco.Core/EmbeddedResources/Lang/en_us.xml b/src/Umbraco.Core/EmbeddedResources/Lang/en_us.xml index dc5a2404e6..608c6943ea 100644 --- a/src/Umbraco.Core/EmbeddedResources/Lang/en_us.xml +++ b/src/Umbraco.Core/EmbeddedResources/Lang/en_us.xml @@ -2033,6 +2033,8 @@ To manage your website, simply open the Umbraco backoffice and start adding cont Types Webhook key Retry count + Toggle debug mode for more information. + Not OK status code The url to call when the webhook is triggered. The events for which the webhook should be triggered. Only trigger the webhook for a specific content type. diff --git a/src/Umbraco.Core/IO/ShadowWrapper.cs b/src/Umbraco.Core/IO/ShadowWrapper.cs index 2f2e0a991c..ca82c67bd2 100644 --- a/src/Umbraco.Core/IO/ShadowWrapper.cs +++ b/src/Umbraco.Core/IO/ShadowWrapper.cs @@ -7,19 +7,21 @@ namespace Umbraco.Cms.Core.IO; internal class ShadowWrapper : IFileSystem, IFileProviderFactory { - private static readonly string ShadowFsPath = Constants.SystemDirectories.TempData.EnsureEndsWith('/') + "ShadowFs"; - private readonly IHostingEnvironment _hostingEnvironment; - private readonly IIOHelper _ioHelper; + private const string ShadowFsPath = "ShadowFs"; - private readonly Func? _isScoped; + private readonly IIOHelper _ioHelper; + private readonly IHostingEnvironment _hostingEnvironment; private readonly ILoggerFactory _loggerFactory; private readonly string _shadowPath; + private readonly Func? _isScoped; + private string? _shadowDir; private ShadowFileSystem? _shadowFileSystem; public ShadowWrapper(IFileSystem innerFileSystem, IIOHelper ioHelper, IHostingEnvironment hostingEnvironment, ILoggerFactory loggerFactory, string shadowPath, Func? isScoped = null) { InnerFileSystem = innerFileSystem; + _ioHelper = ioHelper ?? throw new ArgumentNullException(nameof(ioHelper)); _hostingEnvironment = hostingEnvironment ?? throw new ArgumentNullException(nameof(hostingEnvironment)); _loggerFactory = loggerFactory; @@ -35,18 +37,19 @@ internal class ShadowWrapper : IFileSystem, IFileProviderFactory { get { - if (_isScoped is not null && _shadowFileSystem is not null) + Func? isScoped = _isScoped; + if (isScoped is not null && _shadowFileSystem is not null) { - var isScoped = _isScoped!(); + bool? scoped = isScoped(); // if the filesystem is created *after* shadowing starts, it won't be shadowing - // better not ignore that situation and raised a meaningful (?) exception - if (isScoped.HasValue && isScoped.Value && _shadowFileSystem == null) + // better not ignore that situation and raise a meaningful (?) exception + if (scoped.HasValue && scoped.Value && _shadowFileSystem == null) { throw new Exception("The filesystems are shadowing, but this filesystem is not."); } - return isScoped.HasValue && isScoped.Value + return scoped.HasValue && scoped.Value ? _shadowFileSystem : InnerFileSystem; } @@ -56,8 +59,7 @@ internal class ShadowWrapper : IFileSystem, IFileProviderFactory } /// - public IFileProvider? Create() => - InnerFileSystem.TryCreateFileProvider(out IFileProvider? fileProvider) ? fileProvider : null; + public IFileProvider? Create() => InnerFileSystem.TryCreateFileProvider(out IFileProvider? fileProvider) ? fileProvider : null; public IEnumerable GetDirectories(string path) => FileSystem.GetDirectories(path); @@ -69,8 +71,7 @@ internal class ShadowWrapper : IFileSystem, IFileProviderFactory public void AddFile(string path, Stream stream) => FileSystem.AddFile(path, stream); - public void AddFile(string path, Stream stream, bool overrideExisting) => - FileSystem.AddFile(path, stream, overrideExisting); + public void AddFile(string path, Stream stream, bool overrideExisting) => FileSystem.AddFile(path, stream, overrideExisting); public IEnumerable GetFiles(string path) => FileSystem.GetFiles(path); @@ -107,8 +108,7 @@ internal class ShadowWrapper : IFileSystem, IFileProviderFactory { var id = GuidUtils.ToBase32String(Guid.NewGuid(), idLength); - var virt = ShadowFsPath + "/" + id; - var shadowDir = hostingEnvironment.MapPathContentRoot(virt); + var shadowDir = Path.Combine(hostingEnvironment.LocalTempPath, ShadowFsPath, id); if (Directory.Exists(shadowDir)) { continue; @@ -129,10 +129,10 @@ internal class ShadowWrapper : IFileSystem, IFileProviderFactory // note: no thread-safety here, because ShadowFs is thread-safe due to the check // on ShadowFileSystemsScope.None - and if None is false then we should be running // in a single thread anyways - var virt = Path.Combine(ShadowFsPath, id, _shadowPath); - _shadowDir = _hostingEnvironment.MapPathContentRoot(virt); + var rootUrl = Path.Combine(ShadowFsPath, id, _shadowPath); + _shadowDir = Path.Combine(_hostingEnvironment.LocalTempPath, rootUrl); Directory.CreateDirectory(_shadowDir); - var tempfs = new PhysicalFileSystem(_ioHelper, _hostingEnvironment, _loggerFactory.CreateLogger(), _shadowDir, _hostingEnvironment.ToAbsolute(virt)); + var tempfs = new PhysicalFileSystem(_ioHelper, _hostingEnvironment, _loggerFactory.CreateLogger(), _shadowDir, rootUrl); _shadowFileSystem = new ShadowFileSystem(InnerFileSystem, tempfs); } @@ -160,7 +160,7 @@ internal class ShadowWrapper : IFileSystem, IFileProviderFactory // shadowPath make be path/to/dir, remove each dir = dir!.Replace('/', Path.DirectorySeparatorChar); - var min = _hostingEnvironment.MapPathContentRoot(ShadowFsPath).Length; + var min = Path.Combine(_hostingEnvironment.LocalTempPath, ShadowFsPath).Length; var pos = dir.LastIndexOf(Path.DirectorySeparatorChar); while (pos > min) { diff --git a/src/Umbraco.Core/PropertyEditors/MarkDownPropertyValueEditor.cs b/src/Umbraco.Core/PropertyEditors/MarkDownPropertyValueEditor.cs new file mode 100644 index 0000000000..637a971d68 --- /dev/null +++ b/src/Umbraco.Core/PropertyEditors/MarkDownPropertyValueEditor.cs @@ -0,0 +1,39 @@ +using Umbraco.Cms.Core.IO; +using Umbraco.Cms.Core.Models.Editors; +using Umbraco.Cms.Core.Security; +using Umbraco.Cms.Core.Serialization; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Strings; +using Umbraco.Extensions; + +namespace Umbraco.Cms.Core.PropertyEditors; + +/// +/// A custom value editor to ensure that macro syntax is parsed when being persisted and formatted correctly for +/// display in the editor +/// +internal class MarkDownPropertyValueEditor : DataValueEditor +{ + private readonly IMarkdownSanitizer _markdownSanitizer; + + public MarkDownPropertyValueEditor( + ILocalizedTextService localizedTextService, + IShortStringHelper shortStringHelper, + IJsonSerializer jsonSerializer, + IIOHelper ioHelper, + DataEditorAttribute attribute, + IMarkdownSanitizer markdownSanitizer) + : base(localizedTextService, shortStringHelper, jsonSerializer, ioHelper, attribute) => _markdownSanitizer = markdownSanitizer; + + public override object? FromEditor(ContentPropertyData editorValue, object? currentValue) + { + if (string.IsNullOrWhiteSpace(editorValue.Value?.ToString())) + { + return null; + } + + var sanitized = _markdownSanitizer.Sanitize(editorValue.Value.ToString()!); + + return sanitized.NullOrWhiteSpaceAsNull(); + } +} diff --git a/src/Umbraco.Core/PropertyEditors/MarkdownPropertyEditor.cs b/src/Umbraco.Core/PropertyEditors/MarkdownPropertyEditor.cs index 8b1b181c8c..531262f0b2 100644 --- a/src/Umbraco.Core/PropertyEditors/MarkdownPropertyEditor.cs +++ b/src/Umbraco.Core/PropertyEditors/MarkdownPropertyEditor.cs @@ -4,6 +4,7 @@ using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.IO; +using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Services; namespace Umbraco.Cms.Core.PropertyEditors; @@ -50,4 +51,11 @@ public class MarkdownPropertyEditor : DataEditor /// protected override IConfigurationEditor CreateConfigurationEditor() => new MarkdownConfigurationEditor(_ioHelper, _editorConfigurationParser); + + /// + /// Create a custom value editor + /// + /// + protected override IDataValueEditor CreateValueEditor() => + DataValueEditorFactory.Create(Attribute!); } diff --git a/src/Umbraco.Core/Security/IMarkdownSanitizer.cs b/src/Umbraco.Core/Security/IMarkdownSanitizer.cs new file mode 100644 index 0000000000..ed1fed2a2c --- /dev/null +++ b/src/Umbraco.Core/Security/IMarkdownSanitizer.cs @@ -0,0 +1,14 @@ +namespace Umbraco.Cms.Core.Security; + +/// +/// Sanitizer service for the markdown editor. +/// +public interface IMarkdownSanitizer +{ + /// + /// Sanitizes Markdown + /// + /// Markdown to be sanitized + /// Sanitized Markdown + string Sanitize(string markdown); +} diff --git a/src/Umbraco.Core/Security/NoopMarkdownSanitizer.cs b/src/Umbraco.Core/Security/NoopMarkdownSanitizer.cs new file mode 100644 index 0000000000..3da03b0e63 --- /dev/null +++ b/src/Umbraco.Core/Security/NoopMarkdownSanitizer.cs @@ -0,0 +1,8 @@ +namespace Umbraco.Cms.Core.Security; + +/// +public class NoopMarkdownSanitizer : IMarkdownSanitizer +{ + /// + public string Sanitize(string markdown) => markdown; +} diff --git a/src/Umbraco.Web.BackOffice/Mapping/WebhookMapDefinition.cs b/src/Umbraco.Web.BackOffice/Mapping/WebhookMapDefinition.cs index c96096b7d0..1d1558026c 100644 --- a/src/Umbraco.Web.BackOffice/Mapping/WebhookMapDefinition.cs +++ b/src/Umbraco.Web.BackOffice/Mapping/WebhookMapDefinition.cs @@ -1,5 +1,9 @@ +using Microsoft.Extensions.DependencyInjection; +using Umbraco.Cms.Core.DependencyInjection; +using Umbraco.Cms.Core.Hosting; using Umbraco.Cms.Core.Mapping; using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Webhooks; using Umbraco.Cms.Web.Common.Models; @@ -7,6 +11,24 @@ namespace Umbraco.Cms.Web.BackOffice.Mapping; public class WebhookMapDefinition : IMapDefinition { + private readonly IHostingEnvironment _hostingEnvironment; + private readonly ILocalizedTextService _localizedTextService; + + [Obsolete("Use non-obsolete constructor. This will be removed in Umbraco 15.")] + public WebhookMapDefinition() : this( + StaticServiceProvider.Instance.GetRequiredService(), + StaticServiceProvider.Instance.GetRequiredService() + ) + { + + } + + public WebhookMapDefinition(IHostingEnvironment hostingEnvironment, ILocalizedTextService localizedTextService) + { + _hostingEnvironment = hostingEnvironment; + _localizedTextService = localizedTextService; + } + public void DefineMaps(IUmbracoMapper mapper) { mapper.Define((_, _) => new Webhook(string.Empty), Map); @@ -41,13 +63,22 @@ public class WebhookMapDefinition : IMapDefinition target.EventAlias = source.EventAlias; target.Key = source.Key; target.RequestBody = source.RequestBody ?? string.Empty; - target.ResponseBody = source.ResponseBody; target.RetryCount = source.RetryCount; - target.StatusCode = source.StatusCode; target.Url = source.Url; target.RequestHeaders = source.RequestHeaders; - target.ResponseHeaders = source.ResponseHeaders; target.WebhookKey = source.WebhookKey; - target.ExceptionOccured = source.ExceptionOccured; + + if (_hostingEnvironment.IsDebugMode) + { + target.ExceptionOccured = source.ExceptionOccured; + target.ResponseBody = source.ResponseBody; + target.ResponseHeaders = source.ResponseHeaders; + target.StatusCode = source.StatusCode; + } + else + { + target.ResponseBody = _localizedTextService.Localize("webhooks", "toggleDebug", Thread.CurrentThread.CurrentUICulture); + target.StatusCode = source.StatusCode is "OK (200)" ? source.StatusCode : _localizedTextService.Localize("webhooks", "statusNotOk", Thread.CurrentThread.CurrentUICulture); + } } } diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/PublishedCache/PublishedSnapshotServiceCollectionTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/PublishedCache/PublishedSnapshotServiceCollectionTests.cs index b516b4fffb..df645b0470 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/PublishedCache/PublishedSnapshotServiceCollectionTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/PublishedCache/PublishedSnapshotServiceCollectionTests.cs @@ -1,5 +1,3 @@ -using System.Collections.Generic; -using System.Linq; using NUnit.Framework; using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Models;