From 70a00901b4955921ef245fc0cb9e34ff5904345a Mon Sep 17 00:00:00 2001 From: Nikolaj Geisle <70372949+Zeegaan@users.noreply.github.com> Date: Fri, 1 Apr 2022 14:35:18 +0200 Subject: [PATCH] Finish up Web.Backoffice --- src/Umbraco.Core/GuidUtils.cs | 10 +- src/Umbraco.Core/IO/IMediaPathScheme.cs | 2 +- src/Umbraco.Core/IO/MediaFileManager.cs | 2 +- .../CombinedGuidsMediaPathScheme.cs | 4 +- .../TwoGuidsMediaPathScheme.cs | 4 +- .../MediaPathSchemes/UniqueMediaPathScheme.cs | 2 +- .../Models/ContentEditing/ContentBaseSave.cs | 2 +- .../Models/ContentEditing/ContentItemSave.cs | 4 +- .../Models/ContentEditing/IContentSave.cs | 2 +- .../Models/ContentRepositoryExtensions.cs | 2 +- .../Models/ContentScheduleCollection.cs | 6 +- src/Umbraco.Core/Models/CultureImpact.cs | 2 +- src/Umbraco.Core/Models/IContentBase.cs | 2 +- .../Routing/IPublishedUrlProvider.cs | 2 +- .../Services/ContentServiceExtensions.cs | 2 +- ...peServiceBaseOfTRepositoryTItemTService.cs | 20 +- .../Services/ContentTypeServiceExtensions.cs | 2 +- src/Umbraco.Core/Services/IContentService.cs | 2 +- .../Services/IContentTypeServiceBase.cs | 6 +- .../LocalizedTextServiceExtensions.cs | 2 +- src/Umbraco.Core/Services/OperationResult.cs | 6 +- src/Umbraco.Core/Services/PublishResult.cs | 2 +- .../Services/UserServiceExtensions.cs | 2 +- .../BlockEditorPropertyEditor.cs | 2 +- .../ColorPickerConfigurationEditor.cs | 2 +- .../PropertyEditors/ComplexEditorValidator.cs | 2 +- .../FileUploadPropertyValueEditor.cs | 2 +- .../GridConfigurationEditor.cs | 2 +- .../ImageCropperPropertyValueEditor.cs | 2 +- .../UploadFileTypeValidator.cs | 2 +- .../ValueListUniqueValueValidator.cs | 2 +- .../ContentPermissionsResource.cs | 2 +- .../Controllers/ContentController.cs | 397 +++++++++++------- .../Controllers/ContentTypeControllerBase.cs | 141 ++++--- .../Controllers/EntityController.cs | 279 ++++++------ .../Controllers/TemplateQueryController.cs | 2 +- .../Extensions/LinkGeneratorExtensions.cs | 6 +- .../Extensions/ModelStateExtensions.cs | 12 +- .../CheckIfUserTicketDataIsStaleAttribute.cs | 5 +- .../Mapping/ContentMapDefinition.cs | 2 +- .../Validation/ValidationResultConverter.cs | 2 +- .../Extensions/ViewDataExtensions.cs | 2 +- .../Security/IBackOfficeSignInManager.cs | 2 +- .../CombineGuidBenchmarks.cs | 2 +- .../Umbraco.Core/GuidUtilsTests.cs | 2 +- 45 files changed, 552 insertions(+), 410 deletions(-) diff --git a/src/Umbraco.Core/GuidUtils.cs b/src/Umbraco.Core/GuidUtils.cs index 6a8938dcf0..347ddd573e 100644 --- a/src/Umbraco.Core/GuidUtils.cs +++ b/src/Umbraco.Core/GuidUtils.cs @@ -17,7 +17,7 @@ namespace Umbraco.Cms.Core /// The seconds guid. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Guid Combine(Guid a, Guid b) + public static Guid? Combine(Guid? a, Guid? b) { var ad = new DecomposedGuid(a); var bd = new DecomposedGuid(b); @@ -34,11 +34,11 @@ namespace Umbraco.Cms.Core [StructLayout(LayoutKind.Explicit)] private struct DecomposedGuid { - [FieldOffset(00)] public Guid Value; + [FieldOffset(00)] public Guid? Value; [FieldOffset(00)] public long Hi; [FieldOffset(08)] public long Lo; - public DecomposedGuid(Guid value) : this() => this.Value = value; + public DecomposedGuid(Guid? value) : this() => this.Value = value; } private static readonly char[] Base32Table = @@ -58,12 +58,12 @@ namespace Umbraco.Cms.Core /// that is case insensitive (base-64 is case sensitive). /// Length must be 1-26, anything else becomes 26. /// - public static string ToBase32String(Guid guid, int length = 26) + public static string ToBase32String(Guid? guid, int length = 26) { if (length <= 0 || length > 26) length = 26; - var bytes = guid.ToByteArray(); // a Guid is 128 bits ie 16 bytes + var bytes = guid?.ToByteArray(); // a Guid is 128 bits ie 16 bytes // this could be optimized by making it unsafe, // and fixing the table + bytes + chars (see Convert.ToBase64CharArray) diff --git a/src/Umbraco.Core/IO/IMediaPathScheme.cs b/src/Umbraco.Core/IO/IMediaPathScheme.cs index da9a06d1b1..61ab191d77 100644 --- a/src/Umbraco.Core/IO/IMediaPathScheme.cs +++ b/src/Umbraco.Core/IO/IMediaPathScheme.cs @@ -16,7 +16,7 @@ namespace Umbraco.Cms.Core.IO /// The file name. /// /// The filesystem-relative complete file path. - string GetFilePath(MediaFileManager fileManager, Guid itemGuid, Guid propertyGuid, string filename); + string GetFilePath(MediaFileManager fileManager, Guid? itemGuid, Guid? propertyGuid, string filename); /// /// Gets the directory that can be deleted when the file is deleted. diff --git a/src/Umbraco.Core/IO/MediaFileManager.cs b/src/Umbraco.Core/IO/MediaFileManager.cs index c3462913ff..01b6f09424 100644 --- a/src/Umbraco.Core/IO/MediaFileManager.cs +++ b/src/Umbraco.Core/IO/MediaFileManager.cs @@ -102,7 +102,7 @@ namespace Umbraco.Cms.Core.IO /// The unique identifier of the property type owning the file. /// The filesystem-relative path to the media file. /// With the old media path scheme, this CREATES a new media path each time it is invoked. - public string GetMediaPath(string? filename, Guid cuid, Guid puid) + public string GetMediaPath(string? filename, Guid? cuid, Guid? puid) { filename = Path.GetFileName(filename); if (filename == null) diff --git a/src/Umbraco.Core/IO/MediaPathSchemes/CombinedGuidsMediaPathScheme.cs b/src/Umbraco.Core/IO/MediaPathSchemes/CombinedGuidsMediaPathScheme.cs index 5adc81276b..afbdb371bc 100644 --- a/src/Umbraco.Core/IO/MediaPathSchemes/CombinedGuidsMediaPathScheme.cs +++ b/src/Umbraco.Core/IO/MediaPathSchemes/CombinedGuidsMediaPathScheme.cs @@ -12,13 +12,13 @@ namespace Umbraco.Cms.Core.IO.MediaPathSchemes public class CombinedGuidsMediaPathScheme : IMediaPathScheme { /// - public string GetFilePath(MediaFileManager fileManager, Guid itemGuid, Guid propertyGuid, string filename) + public string GetFilePath(MediaFileManager fileManager, Guid? itemGuid, Guid? propertyGuid, string filename) { // assumes that cuid and puid keys can be trusted - and that a single property type // for a single content cannot store two different files with the same name var combinedGuid = GuidUtils.Combine(itemGuid, propertyGuid); - var directory = HexEncoder.Encode(combinedGuid.ToByteArray()/*'/', 2, 4*/); // could use ext to fragment path eg 12/e4/f2/... + var directory = HexEncoder.Encode(combinedGuid?.ToByteArray()/*'/', 2, 4*/); // could use ext to fragment path eg 12/e4/f2/... return Path.Combine(directory, filename).Replace('\\', '/'); } diff --git a/src/Umbraco.Core/IO/MediaPathSchemes/TwoGuidsMediaPathScheme.cs b/src/Umbraco.Core/IO/MediaPathSchemes/TwoGuidsMediaPathScheme.cs index 1ee821e3ed..49921b299a 100644 --- a/src/Umbraco.Core/IO/MediaPathSchemes/TwoGuidsMediaPathScheme.cs +++ b/src/Umbraco.Core/IO/MediaPathSchemes/TwoGuidsMediaPathScheme.cs @@ -12,9 +12,9 @@ namespace Umbraco.Cms.Core.IO.MediaPathSchemes public class TwoGuidsMediaPathScheme : IMediaPathScheme { /// - public string GetFilePath(MediaFileManager fileManager, Guid itemGuid, Guid propertyGuid, string filename) + public string GetFilePath(MediaFileManager fileManager, Guid? itemGuid, Guid? propertyGuid, string filename) { - return Path.Combine(itemGuid.ToString("N"), propertyGuid.ToString("N"), filename).Replace('\\', '/'); + return Path.Combine(itemGuid?.ToString("N"), propertyGuid?.ToString("N"), filename).Replace('\\', '/'); } /// diff --git a/src/Umbraco.Core/IO/MediaPathSchemes/UniqueMediaPathScheme.cs b/src/Umbraco.Core/IO/MediaPathSchemes/UniqueMediaPathScheme.cs index a3fe36bde9..feee7091c0 100644 --- a/src/Umbraco.Core/IO/MediaPathSchemes/UniqueMediaPathScheme.cs +++ b/src/Umbraco.Core/IO/MediaPathSchemes/UniqueMediaPathScheme.cs @@ -14,7 +14,7 @@ namespace Umbraco.Cms.Core.IO.MediaPathSchemes private const int DirectoryLength = 8; /// - public string GetFilePath(MediaFileManager fileManager, Guid itemGuid, Guid propertyGuid, string filename) + public string GetFilePath(MediaFileManager fileManager, Guid? itemGuid, Guid? propertyGuid, string filename) { var combinedGuid = GuidUtils.Combine(itemGuid, propertyGuid); var directory = GuidUtils.ToBase32String(combinedGuid, DirectoryLength); diff --git a/src/Umbraco.Core/Models/ContentEditing/ContentBaseSave.cs b/src/Umbraco.Core/Models/ContentEditing/ContentBaseSave.cs index 294dc3386e..13d3c667e9 100644 --- a/src/Umbraco.Core/Models/ContentEditing/ContentBaseSave.cs +++ b/src/Umbraco.Core/Models/ContentEditing/ContentBaseSave.cs @@ -36,7 +36,7 @@ namespace Umbraco.Cms.Core.Models.ContentEditing //These need explicit implementation because we are using internal models /// [IgnoreDataMember] - TPersisted? IContentSave.PersistedContent { get; set; } + TPersisted IContentSave.PersistedContent { get; set; } //Non explicit internal getter so we don't need to explicitly cast in our own code [IgnoreDataMember] diff --git a/src/Umbraco.Core/Models/ContentEditing/ContentItemSave.cs b/src/Umbraco.Core/Models/ContentEditing/ContentItemSave.cs index fb142048dd..acf2c5d789 100644 --- a/src/Umbraco.Core/Models/ContentEditing/ContentItemSave.cs +++ b/src/Umbraco.Core/Models/ContentEditing/ContentItemSave.cs @@ -50,11 +50,11 @@ namespace Umbraco.Cms.Core.Models.ContentEditing //These need explicit implementation because we are using internal models /// [IgnoreDataMember] - IContent? IContentSave.PersistedContent { get; set; } + IContent IContentSave.PersistedContent { get; set; } //Non explicit internal getter so we don't need to explicitly cast in our own code [IgnoreDataMember] - public IContent? PersistedContent + public IContent PersistedContent { get => ((IContentSave)this).PersistedContent; set => ((IContentSave)this).PersistedContent = value; diff --git a/src/Umbraco.Core/Models/ContentEditing/IContentSave.cs b/src/Umbraco.Core/Models/ContentEditing/IContentSave.cs index a6c500e820..dfaf183479 100644 --- a/src/Umbraco.Core/Models/ContentEditing/IContentSave.cs +++ b/src/Umbraco.Core/Models/ContentEditing/IContentSave.cs @@ -18,6 +18,6 @@ /// /// This is not used for outgoing model information. /// - TPersisted? PersistedContent { get; set; } + TPersisted PersistedContent { get; set; } } } diff --git a/src/Umbraco.Core/Models/ContentRepositoryExtensions.cs b/src/Umbraco.Core/Models/ContentRepositoryExtensions.cs index 5fec2930b5..4ab39f1669 100644 --- a/src/Umbraco.Core/Models/ContentRepositoryExtensions.cs +++ b/src/Umbraco.Core/Models/ContentRepositoryExtensions.cs @@ -215,7 +215,7 @@ namespace Umbraco.Extensions } // sets the edited cultures on the content - public static void SetCultureEdited(this IContent content, IEnumerable cultures) + public static void SetCultureEdited(this IContent content, IEnumerable? cultures) { if (cultures == null) content.EditedCultures = null; diff --git a/src/Umbraco.Core/Models/ContentScheduleCollection.cs b/src/Umbraco.Core/Models/ContentScheduleCollection.cs index a2691a4c8e..42d1b07404 100644 --- a/src/Umbraco.Core/Models/ContentScheduleCollection.cs +++ b/src/Umbraco.Core/Models/ContentScheduleCollection.cs @@ -59,7 +59,7 @@ namespace Umbraco.Cms.Core.Models /// /// /// true if successfully added, false if validation fails - public bool Add(string culture, DateTime? releaseDate, DateTime? expireDate) + public bool Add(string? culture, DateTime? releaseDate, DateTime? expireDate) { if (culture == null) throw new ArgumentNullException(nameof(culture)); if (releaseDate.HasValue && expireDate.HasValue && releaseDate >= expireDate) @@ -130,7 +130,7 @@ namespace Umbraco.Cms.Core.Models /// /// /// If specified, will clear all entries with dates less than or equal to the value - public void Clear(string culture, ContentScheduleAction action, DateTime? date = null) + public void Clear(string? culture, ContentScheduleAction action, DateTime? date = null) { if (!_schedule.TryGetValue(culture, out var schedules)) return; @@ -176,7 +176,7 @@ namespace Umbraco.Cms.Core.Models /// /// /// - public IEnumerable GetSchedule(string culture, ContentScheduleAction? action = null) + public IEnumerable GetSchedule(string? culture, ContentScheduleAction? action = null) { if (_schedule.TryGetValue(culture, out var changes)) return action == null ? changes.Values : changes.Values.Where(x => x.Action == action.Value); diff --git a/src/Umbraco.Core/Models/CultureImpact.cs b/src/Umbraco.Core/Models/CultureImpact.cs index dea741ab01..2f4364b61b 100644 --- a/src/Umbraco.Core/Models/CultureImpact.cs +++ b/src/Umbraco.Core/Models/CultureImpact.cs @@ -70,7 +70,7 @@ namespace Umbraco.Cms.Core.Models /// /// The culture code. /// A value indicating whether the culture is the default culture. - public static CultureImpact Explicit(string culture, bool isDefault) + public static CultureImpact Explicit(string? culture, bool isDefault) { if (culture == null) throw new ArgumentException("Culture is not explicit."); diff --git a/src/Umbraco.Core/Models/IContentBase.cs b/src/Umbraco.Core/Models/IContentBase.cs index 07fd44397b..20e78816ae 100644 --- a/src/Umbraco.Core/Models/IContentBase.cs +++ b/src/Umbraco.Core/Models/IContentBase.cs @@ -43,7 +43,7 @@ namespace Umbraco.Cms.Core.Models /// When is not null, throws if the content /// type does not vary by culture. /// - void SetCultureName(string? value, string culture); + void SetCultureName(string? value, string? culture); /// /// Gets the name of the content item for a specified language. diff --git a/src/Umbraco.Core/Routing/IPublishedUrlProvider.cs b/src/Umbraco.Core/Routing/IPublishedUrlProvider.cs index 1a2ef5d508..fd52bc7805 100644 --- a/src/Umbraco.Core/Routing/IPublishedUrlProvider.cs +++ b/src/Umbraco.Core/Routing/IPublishedUrlProvider.cs @@ -99,6 +99,6 @@ namespace Umbraco.Cms.Core.Routing /// when no culture is specified, the current culture. /// If the provider is unable to provide a url, it returns . /// - string GetMediaUrl(IPublishedContent content, UrlMode mode = UrlMode.Default, string? culture = null, string propertyAlias = Constants.Conventions.Media.File, Uri? current = null); + string GetMediaUrl(IPublishedContent? content, UrlMode mode = UrlMode.Default, string? culture = null, string propertyAlias = Constants.Conventions.Media.File, Uri? current = null); } } diff --git a/src/Umbraco.Core/Services/ContentServiceExtensions.cs b/src/Umbraco.Core/Services/ContentServiceExtensions.cs index b3cb16e5f5..726c5b4435 100644 --- a/src/Umbraco.Core/Services/ContentServiceExtensions.cs +++ b/src/Umbraco.Core/Services/ContentServiceExtensions.cs @@ -21,7 +21,7 @@ namespace Umbraco.Extensions private static readonly Regex AnchorRegex = new Regex("", RegexOptions.Compiled); - public static IEnumerable GetAnchorValuesFromRTEs(this IContentService contentService, int id, string culture = "*") + public static IEnumerable GetAnchorValuesFromRTEs(this IContentService contentService, int id, string? culture = "*") { var result = new List(); var content = contentService.GetById(id); diff --git a/src/Umbraco.Core/Services/ContentTypeServiceBaseOfTRepositoryTItemTService.cs b/src/Umbraco.Core/Services/ContentTypeServiceBaseOfTRepositoryTItemTService.cs index d9eb95a4be..36e9ac26c5 100644 --- a/src/Umbraco.Core/Services/ContentTypeServiceBaseOfTRepositoryTItemTService.cs +++ b/src/Umbraco.Core/Services/ContentTypeServiceBaseOfTRepositoryTItemTService.cs @@ -69,7 +69,7 @@ namespace Umbraco.Cms.Core.Services #region Validation - public Attempt ValidateComposition(TItem compo) + public Attempt ValidateComposition(TItem? compo) { try { @@ -86,7 +86,7 @@ namespace Umbraco.Cms.Core.Services } } - protected void ValidateLocked(TItem compositionContentType) + protected void ValidateLocked(TItem? compositionContentType) { // performs business-level validation of the composition // should ensure that it is absolutely safe to save the composition @@ -449,8 +449,13 @@ namespace Umbraco.Cms.Core.Services #region Save - public void Save(TItem item, int userId = Cms.Core.Constants.Security.SuperUserId) + public void Save(TItem? item, int userId = Cms.Core.Constants.Security.SuperUserId) { + if (item is null) + { + return; + } + using (IScope scope = ScopeProvider.CreateScope()) { EventMessages eventMessages = EventMessagesFactory.Get(); @@ -957,13 +962,18 @@ namespace Umbraco.Cms.Core.Services } } - public IEnumerable? GetContainers(TItem item) + public IEnumerable? GetContainers(TItem? item) { - var ancestorIds = item.Path.Split(Constants.CharArrays.Comma, StringSplitOptions.RemoveEmptyEntries) + var ancestorIds = item?.Path.Split(Constants.CharArrays.Comma, StringSplitOptions.RemoveEmptyEntries) .Select(x => int.TryParse(x, NumberStyles.Integer, CultureInfo.InvariantCulture, out var asInt) ? asInt : int.MinValue) .Where(x => x != int.MinValue && x != item.Id) .ToArray(); + if (ancestorIds is null) + { + return null; + } + return GetContainers(ancestorIds); } diff --git a/src/Umbraco.Core/Services/ContentTypeServiceExtensions.cs b/src/Umbraco.Core/Services/ContentTypeServiceExtensions.cs index cd3c5273a1..6c30b24b67 100644 --- a/src/Umbraco.Core/Services/ContentTypeServiceExtensions.cs +++ b/src/Umbraco.Core/Services/ContentTypeServiceExtensions.cs @@ -45,7 +45,7 @@ namespace Umbraco.Extensions /// Whether the composite content types should be applicable for an element type /// public static ContentTypeAvailableCompositionsResults GetAvailableCompositeContentTypes(this IContentTypeService ctService, - IContentTypeComposition source, + IContentTypeComposition? source, IContentTypeComposition[] allContentTypes, string[]? filterContentTypes = null, string[]? filterPropertyTypes = null, diff --git a/src/Umbraco.Core/Services/IContentService.cs b/src/Umbraco.Core/Services/IContentService.cs index 9899861a18..222f01d7e9 100644 --- a/src/Umbraco.Core/Services/IContentService.cs +++ b/src/Umbraco.Core/Services/IContentService.cs @@ -344,7 +344,7 @@ namespace Umbraco.Cms.Core.Services /// /// Sorts documents. /// - OperationResult Sort(IEnumerable ids, int userId = Constants.Security.SuperUserId); + OperationResult Sort(IEnumerable? ids, int userId = Constants.Security.SuperUserId); #endregion diff --git a/src/Umbraco.Core/Services/IContentTypeServiceBase.cs b/src/Umbraco.Core/Services/IContentTypeServiceBase.cs index 3835491cb4..17923c61da 100644 --- a/src/Umbraco.Core/Services/IContentTypeServiceBase.cs +++ b/src/Umbraco.Core/Services/IContentTypeServiceBase.cs @@ -56,13 +56,13 @@ namespace Umbraco.Cms.Core.Services bool HasChildren(int id); bool HasChildren(Guid id); - void Save(TItem item, int userId = Constants.Security.SuperUserId); + void Save(TItem? item, int userId = Constants.Security.SuperUserId); void Save(IEnumerable items, int userId = Constants.Security.SuperUserId); void Delete(TItem item, int userId = Constants.Security.SuperUserId); void Delete(IEnumerable item, int userId = Constants.Security.SuperUserId); - Attempt ValidateComposition(TItem compo); + Attempt ValidateComposition(TItem? compo); /// /// Given the path of a content item, this will return true if the content item exists underneath a list view content item @@ -83,7 +83,7 @@ namespace Umbraco.Cms.Core.Services EntityContainer? GetContainer(int containerId); EntityContainer? GetContainer(Guid containerId); IEnumerable? GetContainers(int[] containerIds); - IEnumerable? GetContainers(TItem contentType); + IEnumerable? GetContainers(TItem? contentType); IEnumerable? GetContainers(string folderName, int level); Attempt DeleteContainer(int containerId, int userId = Constants.Security.SuperUserId); Attempt?> RenameContainer(int id, string name, int userId = Constants.Security.SuperUserId); diff --git a/src/Umbraco.Core/Services/LocalizedTextServiceExtensions.cs b/src/Umbraco.Core/Services/LocalizedTextServiceExtensions.cs index 4d926338f3..19ee3a4239 100644 --- a/src/Umbraco.Core/Services/LocalizedTextServiceExtensions.cs +++ b/src/Umbraco.Core/Services/LocalizedTextServiceExtensions.cs @@ -31,7 +31,7 @@ namespace Umbraco.Extensions /// /// /// - public static string Localize(this ILocalizedTextService manager, string area, string alias, string?[]? tokens) + public static string Localize(this ILocalizedTextService manager, string? area, string alias, string?[]? tokens) => manager.Localize(area, alias, Thread.CurrentThread.CurrentUICulture, ConvertToDictionaryVars(tokens)); /// diff --git a/src/Umbraco.Core/Services/OperationResult.cs b/src/Umbraco.Core/Services/OperationResult.cs index 9d4323553f..a69dc6ee12 100644 --- a/src/Umbraco.Core/Services/OperationResult.cs +++ b/src/Umbraco.Core/Services/OperationResult.cs @@ -20,7 +20,7 @@ namespace Umbraco.Cms.Core.Services /// /// Initializes a new instance of the class. /// - public OperationResult(TResultType result, EventMessages eventMessages) + public OperationResult(TResultType result, EventMessages? eventMessages) { Result = result; EventMessages = eventMessages; @@ -50,7 +50,7 @@ namespace Umbraco.Cms.Core.Services /// /// Gets the event messages produced by the operation. /// - public EventMessages EventMessages { get; } + public EventMessages? EventMessages { get; } } /// @@ -80,7 +80,7 @@ namespace Umbraco.Cms.Core.Services /// /// Initializes a new instance of the class. /// - public OperationResult(TResultType result, EventMessages eventMessages, TEntity entity) + public OperationResult(TResultType result, EventMessages? eventMessages, TEntity? entity) : base(result, eventMessages) { Entity = entity; diff --git a/src/Umbraco.Core/Services/PublishResult.cs b/src/Umbraco.Core/Services/PublishResult.cs index ad5cae80ec..0ab820e7a6 100644 --- a/src/Umbraco.Core/Services/PublishResult.cs +++ b/src/Umbraco.Core/Services/PublishResult.cs @@ -13,7 +13,7 @@ namespace Umbraco.Cms.Core.Services /// /// Initializes a new instance of the class. /// - public PublishResult(PublishResultType resultType, EventMessages eventMessages, IContent content) + public PublishResult(PublishResultType resultType, EventMessages? eventMessages, IContent? content) : base(resultType, eventMessages, content) { } diff --git a/src/Umbraco.Core/Services/UserServiceExtensions.cs b/src/Umbraco.Core/Services/UserServiceExtensions.cs index 227550e0d9..087e262101 100644 --- a/src/Umbraco.Core/Services/UserServiceExtensions.cs +++ b/src/Umbraco.Core/Services/UserServiceExtensions.cs @@ -10,7 +10,7 @@ namespace Umbraco.Extensions { public static class UserServiceExtensions { - public static EntityPermission? GetPermissions(this IUserService userService, IUser user, string path) + public static EntityPermission? GetPermissions(this IUserService userService, IUser? user, string path) { var ids = path.Split(Constants.CharArrays.Comma, StringSplitOptions.RemoveEmptyEntries) .Select(x => int.TryParse(x, NumberStyles.Integer, CultureInfo.InvariantCulture, out var value) ? Attempt.Succeed(value) : Attempt.Fail()) diff --git a/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorPropertyEditor.cs b/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorPropertyEditor.cs index f4b6ee62fd..8b6663051e 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorPropertyEditor.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorPropertyEditor.cs @@ -258,7 +258,7 @@ namespace Umbraco.Cms.Core.PropertyEditors _textService = textService; } - public IEnumerable Validate(object? value, string valueType, object? dataTypeConfiguration) + public IEnumerable Validate(object? value, string? valueType, object? dataTypeConfiguration) { var blockConfig = (BlockListConfiguration?)dataTypeConfiguration; if (blockConfig == null) yield break; diff --git a/src/Umbraco.Infrastructure/PropertyEditors/ColorPickerConfigurationEditor.cs b/src/Umbraco.Infrastructure/PropertyEditors/ColorPickerConfigurationEditor.cs index 67ac2c05e7..b143ecded9 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/ColorPickerConfigurationEditor.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/ColorPickerConfigurationEditor.cs @@ -154,7 +154,7 @@ namespace Umbraco.Cms.Core.PropertyEditors internal class ColorListValidator : IValueValidator { - public IEnumerable Validate(object? value, string valueType, object? dataTypeConfiguration) + public IEnumerable Validate(object? value, string? valueType, object? dataTypeConfiguration) { if (!(value is JArray json)) yield break; diff --git a/src/Umbraco.Infrastructure/PropertyEditors/ComplexEditorValidator.cs b/src/Umbraco.Infrastructure/PropertyEditors/ComplexEditorValidator.cs index d224e506c0..4e0694301f 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/ComplexEditorValidator.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/ComplexEditorValidator.cs @@ -32,7 +32,7 @@ namespace Umbraco.Cms.Core.PropertyEditors /// /// /// - public IEnumerable Validate(object? value, string valueType, object? dataTypeConfiguration) + public IEnumerable Validate(object? value, string? valueType, object? dataTypeConfiguration) { var elementTypeValues = GetElementTypeValidation(value).ToList(); var rowResults = GetNestedValidationResults(elementTypeValues).ToList(); diff --git a/src/Umbraco.Infrastructure/PropertyEditors/FileUploadPropertyValueEditor.cs b/src/Umbraco.Infrastructure/PropertyEditors/FileUploadPropertyValueEditor.cs index 573b4ac736..668986fbb5 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/FileUploadPropertyValueEditor.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/FileUploadPropertyValueEditor.cs @@ -112,7 +112,7 @@ namespace Umbraco.Cms.Core.PropertyEditors } - private string? ProcessFile(ContentPropertyFile file, object? dataTypeConfiguration, Guid cuid, Guid puid) + private string? ProcessFile(ContentPropertyFile file, object? dataTypeConfiguration, Guid? cuid, Guid? puid) { // process the file // no file, invalid file, reject change diff --git a/src/Umbraco.Infrastructure/PropertyEditors/GridConfigurationEditor.cs b/src/Umbraco.Infrastructure/PropertyEditors/GridConfigurationEditor.cs index 65367d039d..788a4ae496 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/GridConfigurationEditor.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/GridConfigurationEditor.cs @@ -24,7 +24,7 @@ namespace Umbraco.Cms.Core.PropertyEditors public class GridValidator : IValueValidator { - public IEnumerable Validate(object? rawValue, string valueType, object? dataTypeConfiguration) + public IEnumerable Validate(object? rawValue, string? valueType, object? dataTypeConfiguration) { if (rawValue == null) yield break; diff --git a/src/Umbraco.Infrastructure/PropertyEditors/ImageCropperPropertyValueEditor.cs b/src/Umbraco.Infrastructure/PropertyEditors/ImageCropperPropertyValueEditor.cs index c9635ee121..8a13ded02f 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/ImageCropperPropertyValueEditor.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/ImageCropperPropertyValueEditor.cs @@ -175,7 +175,7 @@ namespace Umbraco.Cms.Core.PropertyEditors return editorJson.ToString(Formatting.None); } - private string? ProcessFile(ContentPropertyFile file, Guid cuid, Guid puid) + private string? ProcessFile(ContentPropertyFile file, Guid? cuid, Guid? puid) { // process the file // no file, invalid file, reject change diff --git a/src/Umbraco.Infrastructure/PropertyEditors/UploadFileTypeValidator.cs b/src/Umbraco.Infrastructure/PropertyEditors/UploadFileTypeValidator.cs index 6e5c46e643..5f1ab43404 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/UploadFileTypeValidator.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/UploadFileTypeValidator.cs @@ -27,7 +27,7 @@ namespace Umbraco.Cms.Core.PropertyEditors contentSettings.OnChange(x => _contentSettings = x); } - public IEnumerable Validate(object? value, string valueType, object? dataTypeConfiguration) + public IEnumerable Validate(object? value, string? valueType, object? dataTypeConfiguration) { string? selectedFiles = null; if (value is JObject jobject && jobject["selectedFiles"] is JToken jToken) diff --git a/src/Umbraco.Infrastructure/PropertyEditors/ValueListUniqueValueValidator.cs b/src/Umbraco.Infrastructure/PropertyEditors/ValueListUniqueValueValidator.cs index 1f74df8851..ab47886005 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/ValueListUniqueValueValidator.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/ValueListUniqueValueValidator.cs @@ -14,7 +14,7 @@ namespace Umbraco.Cms.Core.PropertyEditors /// public class ValueListUniqueValueValidator : IValueValidator { - public IEnumerable Validate(object? value, string valueType, object? dataTypeConfiguration) + public IEnumerable Validate(object? value, string? valueType, object? dataTypeConfiguration) { // the value we get should be a JArray // [ { "value": , "sortOrder": 1 }, { ... }, ... ] diff --git a/src/Umbraco.Web.BackOffice/Authorization/ContentPermissionsResource.cs b/src/Umbraco.Web.BackOffice/Authorization/ContentPermissionsResource.cs index ae02db6d4f..cac7ac7917 100644 --- a/src/Umbraco.Web.BackOffice/Authorization/ContentPermissionsResource.cs +++ b/src/Umbraco.Web.BackOffice/Authorization/ContentPermissionsResource.cs @@ -16,7 +16,7 @@ namespace Umbraco.Cms.Web.BackOffice.Authorization /// /// The content. /// The permission to authorize. - public ContentPermissionsResource(IContent content, char permissionToCheck) + public ContentPermissionsResource(IContent? content, char permissionToCheck) { PermissionsToCheck = new List { permissionToCheck }; Content = content; diff --git a/src/Umbraco.Web.BackOffice/Controllers/ContentController.cs b/src/Umbraco.Web.BackOffice/Controllers/ContentController.cs index 578d4e4b10..5e531c1eb4 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/ContentController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/ContentController.cs @@ -126,7 +126,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers public IEnumerable GetByIds([FromQuery] int[] ids) { var foundContent = _contentService.GetByIds(ids); - return foundContent.Select(MapToDisplay); + return foundContent.Select(MapToDisplay).WhereNotNull(); } /// @@ -273,7 +273,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers return display; } - public ActionResult GetBlueprintById(int id) + public ActionResult GetBlueprintById(int id) { var foundContent = _contentService.GetBlueprintById(id); if (foundContent == null) @@ -283,7 +283,10 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers var content = MapToDisplay(foundContent); - SetupBlueprint(content, foundContent); + if (content is not null) + { + SetupBlueprint(content, foundContent); + } return content; } @@ -313,7 +316,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers /// [OutgoingEditorModelEvent] [Authorize(Policy = AuthorizationPolicies.ContentPermissionBrowseById)] - public ActionResult GetById(int id) + public ActionResult GetById(int id) { var foundContent = GetObjectFromRequest(() => _contentService.GetById(id)); if (foundContent == null) @@ -331,7 +334,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers /// [OutgoingEditorModelEvent] [Authorize(Policy = AuthorizationPolicies.ContentPermissionBrowseById)] - public ActionResult GetById(Guid id) + public ActionResult GetById(Guid id) { var foundContent = GetObjectFromRequest(() => _contentService.GetById(id)); if (foundContent == null) @@ -348,7 +351,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers /// [OutgoingEditorModelEvent] [Authorize(Policy = AuthorizationPolicies.ContentPermissionBrowseById)] - public ActionResult GetById(Udi id) + public ActionResult GetById(Udi id) { var guidUdi = id as GuidUdi; if (guidUdi != null) @@ -365,7 +368,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers /// /// [OutgoingEditorModelEvent] - public ActionResult GetEmpty(string contentTypeAlias, int parentId) + public ActionResult GetEmpty(string contentTypeAlias, int parentId) { var contentType = _contentTypeService.Get(contentTypeAlias); if (contentType == null) @@ -400,7 +403,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers /// /// [OutgoingEditorModelEvent] - public ActionResult GetEmptyByKey(Guid contentTypeKey, int parentId) + public ActionResult GetEmptyByKey(Guid contentTypeKey, int parentId) { using (var scope = _scopeProvider.CreateScope()) { @@ -417,11 +420,15 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers } } - private ContentItemDisplay GetEmptyInner(IContentType contentType, int parentId) + private ContentItemDisplay? GetEmptyInner(IContentType contentType, int parentId) { var emptyContent = _contentService.Create("", parentId, contentType.Alias, _backofficeSecurityAccessor.BackOfficeSecurity?.GetUserId().Result ?? -1); var mapped = MapToDisplay(emptyContent); + if (mapped is null) + { + return null; + } return CleanContentItemDisplay(mapped); } @@ -694,8 +701,13 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers return notificationModel; } - private bool EnsureUniqueName(string? name, IContent content, string modelName) + private bool EnsureUniqueName(string? name, IContent? content, string modelName) { + if (content is null) + { + return false; + } + var existing = _contentService.GetBlueprintsForContentTypes(content.ContentTypeId); if (existing?.Any(x => x.Name == name && x.Id != content.Id) ?? false) { @@ -711,13 +723,13 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers /// [FileUploadCleanupFilter] [ContentSaveValidation] - public async Task>> PostSaveBlueprint([ModelBinder(typeof(BlueprintItemBinder))] ContentItemSave contentItem) + public async Task?>?> PostSaveBlueprint([ModelBinder(typeof(BlueprintItemBinder))] ContentItemSave contentItem) { var contentItemDisplay = await PostSaveInternal( contentItem, (content, _) => { - if (!EnsureUniqueName(content.Name, content, "Name") || contentItem.PersistedContent is null) + if (!EnsureUniqueName(content?.Name, content, "Name") || contentItem.PersistedContent is null) { return OperationResult.Cancel(new EventMessages()); } @@ -730,7 +742,11 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers content => { var display = MapToDisplay(content); - SetupBlueprint(display, content); + if (display is not null) + { + SetupBlueprint(display, content); + } + return display; }); @@ -743,7 +759,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers [FileUploadCleanupFilter] [ContentSaveValidation] [OutgoingEditorModelEvent] - public async Task>> PostSave([ModelBinder(typeof(ContentItemBinder))] ContentItemSave contentItem) + public async Task?>> PostSave([ModelBinder(typeof(ContentItemBinder))] ContentItemSave contentItem) { var contentItemDisplay = await PostSaveInternal( contentItem, @@ -753,7 +769,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers return contentItemDisplay; } - private async Task>> PostSaveInternal(ContentItemSave contentItem, Func saveMethod, Func> mapToDisplay) + private async Task?>> PostSaveInternal(ContentItemSave contentItem, Func? saveMethod, Func?> mapToDisplay) where TVariant : ContentVariantDisplay { // Recent versions of IE/Edge may send in the full client side file path instead of just the file name. @@ -864,17 +880,20 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers { var variantErrors = ModelState.GetVariantsWithErrors(cultureForInvariantErrors); - var validVariants = contentItem.Variants - .Where(x => x.Save && !variantErrors.Contains((x.Culture, x.Segment))) - .Select(x => (culture: x.Culture, segment: x.Segment)); - - foreach (var (culture, segment) in validVariants) + if (variantErrors is not null) { - var variantName = GetVariantName(culture, segment); + var validVariants = contentItem.Variants + .Where(x => x.Save && !variantErrors.Contains((x.Culture, x.Segment))) + .Select(x => (culture: x.Culture, segment: x.Segment)); - AddSuccessNotification(notifications, culture, segment, - _localizedTextService.Localize("speechBubbles", "editContentSendToPublish"), - _localizedTextService.Localize("speechBubbles", "editVariantSendToPublishText", new[] { variantName })); + foreach (var (culture, segment) in validVariants) + { + var variantName = GetVariantName(culture, segment); + + AddSuccessNotification(notifications, culture, segment, + _localizedTextService.Localize("speechBubbles", "editContentSendToPublish"), + _localizedTextService.Localize("speechBubbles", "editVariantSendToPublishText", new[] { variantName })); + } } } else if (ModelState.IsValid) @@ -935,11 +954,14 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers var display = mapToDisplay(contentItem.PersistedContent); //merge the tracked success messages with the outgoing model - display.Notifications.AddRange(globalNotifications.Notifications); - foreach (var v in display.Variants.Where(x => x.Language != null)) + display?.Notifications.AddRange(globalNotifications.Notifications); + if (display?.Variants is not null) { - if (notifications.TryGetValue(v.Language.IsoCode, out var n)) - v.Notifications.AddRange(n.Notifications); + foreach (var v in display.Variants.Where(x => x.Language != null)) + { + if (v.Language?.IsoCode is not null && notifications.TryGetValue(v.Language.IsoCode, out var n)) + v.Notifications.AddRange(n.Notifications); + } } //lastly, if it is not valid, add the model state to the outgoing object and throw a 400 @@ -962,12 +984,15 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers } } - display.PersistedContent = contentItem.PersistedContent; + if (display is not null) + { + display.PersistedContent = contentItem.PersistedContent; + } return display; } - private void AddPublishStatusNotifications(IReadOnlyCollection publishStatus, SimpleNotificationModel globalNotifications, Dictionary variantNotifications, string[] successfulCultures) + private void AddPublishStatusNotifications(IReadOnlyCollection publishStatus, SimpleNotificationModel globalNotifications, Dictionary variantNotifications, string[]? successfulCultures) { //global notifications AddMessageForPublishStatus(publishStatus, globalNotifications, successfulCultures); @@ -1006,7 +1031,11 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers //ensure the variant has all critical required data to be persisted if (!RequiredForPersistenceAttribute.HasRequiredValuesForPersistence(variant)) { - variantCriticalValidationErrors.Add(variant.Culture); + if (variant.Culture is not null) + { + variantCriticalValidationErrors.Add(variant.Culture); + } + //if there's no Name, it cannot be persisted at all reset the flags, this cannot be saved or published variant.Save = variant.Publish = false; @@ -1056,21 +1085,21 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers /// /// Method is used for normal Saving and Scheduled Publishing /// - private void SaveAndNotify(ContentItemSave contentItem, Func saveMethod, int variantCount, + private void SaveAndNotify(ContentItemSave contentItem, Func? saveMethod, int variantCount, Dictionary notifications, SimpleNotificationModel globalNotifications, string savedContentHeaderLocalizationAlias, string invariantSavedLocalizationAlias, string variantSavedLocalizationAlias, string? cultureForInvariantErrors, ContentScheduleCollection? contentSchedule, out bool wasCancelled) { - var saveResult = saveMethod(contentItem.PersistedContent, contentSchedule); - wasCancelled = saveResult.Success == false && saveResult.Result == OperationResultType.FailedCancelledByEvent; - if (saveResult.Success) + var saveResult = saveMethod?.Invoke(contentItem.PersistedContent, contentSchedule); + wasCancelled = saveResult?.Success == false && saveResult.Result == OperationResultType.FailedCancelledByEvent; + if (saveResult?.Success ?? false) { if (variantCount > 1) { var variantErrors = ModelState.GetVariantsWithErrors(cultureForInvariantErrors); var savedWithoutErrors = contentItem.Variants - .Where(x => x.Save && !variantErrors.Contains((x.Culture, x.Segment))) + .Where(x => x.Save && (!variantErrors?.Contains((x.Culture, x.Segment)) ?? false)) .Select(x => (culture: x.Culture, segment: x.Segment)); foreach (var (culture, segment) in savedWithoutErrors) @@ -1098,7 +1127,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers /// private bool SaveSchedule(ContentItemSave contentItem, ContentScheduleCollection contentSchedule, SimpleNotificationModel globalNotifications) { - if (!contentItem.PersistedContent.ContentType.VariesByCulture()) + if (!contentItem.PersistedContent?.ContentType.VariesByCulture() ?? false) return SaveScheduleInvariant(contentItem, contentSchedule, globalNotifications); else return SaveScheduleVariant(contentItem, contentSchedule); @@ -1192,7 +1221,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers var nonMandatoryVariants = new List<(string culture, bool isPublished, List releaseDates)>(); foreach (var groupedSched in contentSchedule.FullSchedule.GroupBy(x => x.Culture)) { - var isPublished = contentItem.PersistedContent.Published && contentItem.PersistedContent.IsCulturePublished(groupedSched.Key); + var isPublished = (contentItem.PersistedContent?.Published ?? false) && contentItem.PersistedContent.IsCulturePublished(groupedSched.Key); var releaseDates = groupedSched.Where(x => x.Action == ContentScheduleAction.Release).Select(x => x.Date).ToList(); if (mandatoryCultures.Contains(groupedSched.Key, StringComparer.InvariantCultureIgnoreCase)) mandatoryVariants.Add((groupedSched.Key, isPublished, releaseDates)); @@ -1271,7 +1300,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers /// global notifications will be shown if all variant processing is successful and the save/publish dialog is closed, otherwise /// variant specific notifications are used to show success messages in the save/publish dialog. /// - private static void AddSuccessNotification(IDictionary notifications, string culture, string segment, string header, string msg) + private static void AddSuccessNotification(IDictionary notifications, string? culture, string? segment, string header, string msg) { //add the global notification (which will display globally if all variants are successfully processed) notifications[string.Empty].AddSuccessNotification(header, msg); @@ -1293,13 +1322,13 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers return authorizationResult.Succeeded; } - private IEnumerable PublishBranchInternal(ContentItemSave contentItem, bool force, string cultureForInvariantErrors, - out bool wasCancelled, out string[] successfulCultures) + private IEnumerable PublishBranchInternal(ContentItemSave contentItem, bool force, string? cultureForInvariantErrors, + out bool wasCancelled, out string[]? successfulCultures) { - if (!contentItem.PersistedContent.ContentType.VariesByCulture()) + if (!contentItem.PersistedContent?.ContentType.VariesByCulture() ?? false) { //its invariant, proceed normally - var publishStatus = _contentService.SaveAndPublishBranch(contentItem.PersistedContent, force, userId: _backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser.Id); + var publishStatus = _contentService.SaveAndPublishBranch(contentItem.PersistedContent!, force, userId: _backofficeSecurityAccessor.BackOfficeSecurity?.CurrentUser?.Id ?? -1); // TODO: Deal with multiple cancellations wasCancelled = publishStatus.Any(x => x.Result == PublishResultType.FailedPublishCancelledByEvent); successfulCultures = null; //must be null! this implies invariant @@ -1325,7 +1354,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers foreach (var variant in contentItem.Variants) { - if (variantErrors.Contains((variant.Culture, variant.Segment))) + if (variantErrors?.Contains((variant.Culture, variant.Segment)) ?? false) variant.Publish = false; } @@ -1334,16 +1363,16 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers if (canPublish) { //proceed to publish if all validation still succeeds - var publishStatus = _contentService.SaveAndPublishBranch(contentItem.PersistedContent, force, culturesToPublish, _backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser.Id); + var publishStatus = _contentService.SaveAndPublishBranch(contentItem.PersistedContent!, force, culturesToPublish.WhereNotNull().ToArray(), _backofficeSecurityAccessor.BackOfficeSecurity?.CurrentUser?.Id ?? -1); // TODO: Deal with multiple cancellations wasCancelled = publishStatus.Any(x => x.Result == PublishResultType.FailedPublishCancelledByEvent); - successfulCultures = contentItem.Variants.Where(x => x.Publish).Select(x => x.Culture).ToArray(); + successfulCultures = contentItem.Variants.Where(x => x.Publish).Select(x => x.Culture).WhereNotNull().ToArray(); return publishStatus; } else { //can only save - var saveResult = _contentService.Save(contentItem.PersistedContent, _backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser.Id); + var saveResult = _contentService.Save(contentItem.PersistedContent!, _backofficeSecurityAccessor.BackOfficeSecurity?.CurrentUser?.Id ?? -1); var publishStatus = new[] { new PublishResult(PublishResultType.FailedPublishMandatoryCultureMissing, null, contentItem.PersistedContent) @@ -1366,12 +1395,12 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers /// /// If this is a culture variant than we need to do some validation, if it's not we'll publish as normal /// - private PublishResult PublishInternal(ContentItemSave contentItem, string defaultCulture, string cultureForInvariantErrors, out bool wasCancelled, out string[] successfulCultures) + private PublishResult PublishInternal(ContentItemSave contentItem, string? defaultCulture, string? cultureForInvariantErrors, out bool wasCancelled, out string[]? successfulCultures) { - if (!contentItem.PersistedContent.ContentType.VariesByCulture()) + if (!contentItem.PersistedContent?.ContentType.VariesByCulture() ?? false) { //its invariant, proceed normally - var publishStatus = _contentService.SaveAndPublish(contentItem.PersistedContent, userId: _backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser.Id); + var publishStatus = _contentService.SaveAndPublish(contentItem.PersistedContent!, userId: _backofficeSecurityAccessor.BackOfficeSecurity?.CurrentUser?.Id ?? -1); wasCancelled = publishStatus.Result == PublishResultType.FailedPublishCancelledByEvent; successfulCultures = null; //must be null! this implies invariant return publishStatus; @@ -1398,25 +1427,25 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers //It is a requirement that this is performed AFTER ValidatePublishingMandatoryLanguages. foreach (var variant in contentItem.Variants) { - if (variantErrors.Contains((variant.Culture, variant.Segment))) + if (variantErrors?.Contains((variant.Culture, variant.Segment)) ?? false) variant.Publish = false; } //At this stage all variants might have failed validation which means there are no cultures flagged for publishing! - var culturesToPublish = variants.Where(x => x.Publish).Select(x => x.Culture).ToArray(); + var culturesToPublish = variants.Where(x => x.Publish).Select(x => x.Culture).WhereNotNull().ToArray(); canPublish = canPublish && culturesToPublish.Length > 0; if (canPublish) { //try to publish all the values on the model - this will generally only fail if someone is tampering with the request //since there's no reason variant rules would be violated in normal cases. - canPublish = PublishCulture(contentItem.PersistedContent, variants, defaultCulture); + canPublish = PublishCulture(contentItem.PersistedContent!, variants, defaultCulture); } if (canPublish) { //proceed to publish if all validation still succeeds - var publishStatus = _contentService.SaveAndPublish(contentItem.PersistedContent, culturesToPublish, _backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser.Id); + var publishStatus = _contentService.SaveAndPublish(contentItem.PersistedContent!, culturesToPublish, _backofficeSecurityAccessor.BackOfficeSecurity?.CurrentUser?.Id ?? -1); wasCancelled = publishStatus.Result == PublishResultType.FailedPublishCancelledByEvent; successfulCultures = culturesToPublish; @@ -1425,7 +1454,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers else { //can only save - var saveResult = _contentService.Save(contentItem.PersistedContent, _backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser.Id); + var saveResult = _contentService.Save(contentItem.PersistedContent!, _backofficeSecurityAccessor.BackOfficeSecurity?.CurrentUser?.Id ?? -1); var publishStatus = new PublishResult(PublishResultType.FailedPublishMandatoryCultureMissing, null, contentItem.PersistedContent); wasCancelled = saveResult.Result == OperationResultType.FailedCancelledByEvent; successfulCultures = Array.Empty(); @@ -1433,7 +1462,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers } } - private void AddDomainWarnings(IEnumerable publishResults, string[] culturesPublished, + private void AddDomainWarnings(IEnumerable publishResults, string[]? culturesPublished, SimpleNotificationModel globalNotifications) { foreach (PublishResult publishResult in publishResults) @@ -1452,7 +1481,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers /// /// /// - internal void AddDomainWarnings(IContent persistedContent, string[] culturesPublished, SimpleNotificationModel globalNotifications) + internal void AddDomainWarnings(IContent? persistedContent, string[]? culturesPublished, SimpleNotificationModel globalNotifications) { // Don't try to verify if no cultures were published if (culturesPublished is null) @@ -1468,35 +1497,40 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers } // If more than a single culture is published we need to verify that there's a domain registered for each published culture - var assignedDomains = _domainService.GetAssignedDomains(persistedContent.Id, true).ToHashSet(); - // We also have to check all of the ancestors, if any of those has the appropriate culture assigned we don't need to warn - foreach (var ancestorID in persistedContent.GetAncestorIds()) + var assignedDomains = persistedContent is null ? null : _domainService.GetAssignedDomains(persistedContent.Id, true)?.ToHashSet(); + + var ancestorIds = persistedContent?.GetAncestorIds(); + if (ancestorIds is not null && assignedDomains is not null) { - assignedDomains.UnionWith(_domainService.GetAssignedDomains(ancestorID, true)); + // We also have to check all of the ancestors, if any of those has the appropriate culture assigned we don't need to warn + foreach (var ancestorID in ancestorIds) + { + assignedDomains.UnionWith(_domainService.GetAssignedDomains(ancestorID, true) ?? Enumerable.Empty()); + } } // No domains at all, add a warning, to add domains. - if (assignedDomains.Count == 0) + if (assignedDomains is null || assignedDomains.Count == 0) { globalNotifications.AddWarningNotification( _localizedTextService.Localize("auditTrails", "publish"), _localizedTextService.Localize("speechBubbles", "publishWithNoDomains")); _logger.LogWarning("The root node {RootNodeName} was published with multiple cultures, but no domains are configured, this will cause routing and caching issues, please register domains for: {Cultures}", - persistedContent.Name, string.Join(", ", publishedCultures)); + persistedContent?.Name, string.Join(", ", publishedCultures)); return; } // If there is some domains, verify that there's a domain for each of the published cultures foreach (var culture in culturesPublished - .Where(culture => assignedDomains.Any(x => x.LanguageIsoCode.Equals(culture, StringComparison.OrdinalIgnoreCase)) is false)) + .Where(culture => assignedDomains.Any(x => x.LanguageIsoCode?.Equals(culture, StringComparison.OrdinalIgnoreCase) ?? false) is false)) { globalNotifications.AddWarningNotification( _localizedTextService.Localize("auditTrails", "publish"), _localizedTextService.Localize("speechBubbles", "publishWithMissingDomain", new []{culture})); _logger.LogWarning("The root node {RootNodeName} was published in culture {Culture}, but there's no domain configured for it, this will cause routing and caching issues, please register a domain for it", - persistedContent.Name, culture); + persistedContent?.Name, culture); } } @@ -1510,7 +1544,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers /// /// private bool ValidatePublishingMandatoryLanguages( - IReadOnlyCollection<(string culture, string segment)> variantsWithValidationErrors, + IReadOnlyCollection<(string? culture, string? segment)>? variantsWithValidationErrors, ContentItemSave contentItem, IReadOnlyCollection variants, IReadOnlyList mandatoryCultures, @@ -1525,15 +1559,15 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers var mandatoryVariant = variants.First(x => x.Culture.InvariantEquals(culture)); - var isPublished = contentItem.PersistedContent.Published && contentItem.PersistedContent.IsCulturePublished(culture); + var isPublished = (contentItem.PersistedContent?.Published ?? false) && contentItem.PersistedContent.IsCulturePublished(culture); var isPublishing = isPublished || publishingCheck(mandatoryVariant); - var isValid = !variantsWithValidationErrors.Select(v => v.culture).InvariantContains(culture); + var isValid = !variantsWithValidationErrors?.Select(v => v.culture!).InvariantContains(culture) ?? false; result.Add((mandatoryVariant, isPublished || isPublishing, isValid)); } //iterate over the results by invalid first - string firstInvalidMandatoryCulture = null; + string? firstInvalidMandatoryCulture = null; foreach (var r in result.OrderBy(x => x.isValid)) { if (!r.isValid) @@ -1571,7 +1605,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers /// /// This would generally never fail unless someone is tampering with the request /// - private bool PublishCulture(IContent persistentContent, IEnumerable cultureVariants, string defaultCulture) + private bool PublishCulture(IContent persistentContent, IEnumerable cultureVariants, string? defaultCulture) { foreach (var variant in cultureVariants.Where(x => x.Publish)) { @@ -1587,26 +1621,28 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers return true; } - private IEnumerable GetPublishedCulturesFromAncestors(IContent content) + private IEnumerable GetPublishedCulturesFromAncestors(IContent? content) { - if (content.ParentId == -1) + if (content?.ParentId == -1) { return content.PublishedCultures; } HashSet publishedCultures = new (); - publishedCultures.UnionWith(content.PublishedCultures); + publishedCultures.UnionWith(content?.PublishedCultures ?? Enumerable.Empty()); - IEnumerable ancestorIds = content.GetAncestorIds(); + IEnumerable? ancestorIds = content?.GetAncestorIds(); - foreach (var id in ancestorIds) + if (ancestorIds is not null) { - IEnumerable cultures = _contentService.GetById(id).PublishedCultures; - publishedCultures.UnionWith(cultures); + foreach (var id in ancestorIds) + { + IEnumerable? cultures = _contentService.GetById(id)?.PublishedCultures; + publishedCultures.UnionWith(cultures ?? Enumerable.Empty()); + } } return publishedCultures; - } /// /// Adds a generic culture error for use in displaying the culture validation error in the save/publish/etc... dialogs @@ -1617,7 +1653,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers /// /// The culture used in the localization message, null by default which means will be used. /// - private void AddVariantValidationError(string culture, string segment, string localizationArea,string localizationAlias, string cultureToken = null) + private void AddVariantValidationError(string? culture, string? segment, string localizationArea,string localizationAlias, string? cultureToken = null) { var cultureToUse = cultureToken ?? culture; var variantName = GetVariantName(cultureToUse, segment); @@ -1633,7 +1669,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers /// Culture /// Segment /// - private string GetVariantName(string culture, string segment) + private string GetVariantName(string? culture, string? segment) { if (culture.IsNullOrWhiteSpace() && segment.IsNullOrWhiteSpace()) { @@ -1667,7 +1703,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers return HandleContentNotFound(id); } - var publishResult = _contentService.SaveAndPublish(foundContent, userId: _backofficeSecurityAccessor.BackOfficeSecurity.GetUserId().Result ?? 0); + var publishResult = _contentService.SaveAndPublish(foundContent, userId: _backofficeSecurityAccessor.BackOfficeSecurity?.GetUserId().Result ?? 0); if (publishResult.Success == false) { var notificationModel = new SimpleNotificationModel(); @@ -1719,7 +1755,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers //if the current item is in the recycle bin if (foundContent.Trashed == false) { - var moveResult = _contentService.MoveToRecycleBin(foundContent, _backofficeSecurityAccessor.BackOfficeSecurity.GetUserId().Result ?? 0); + var moveResult = _contentService.MoveToRecycleBin(foundContent, _backofficeSecurityAccessor.BackOfficeSecurity?.GetUserId().Result ?? -1); if (moveResult.Success == false) { return ValidationProblem(); @@ -1727,7 +1763,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers } else { - var deleteResult = _contentService.Delete(foundContent, _backofficeSecurityAccessor.BackOfficeSecurity.GetUserId().Result ?? 0); + var deleteResult = _contentService.Delete(foundContent, _backofficeSecurityAccessor.BackOfficeSecurity?.GetUserId().Result ?? -1); if (deleteResult.Success == false) { return ValidationProblem(); @@ -1749,7 +1785,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers [Authorize(Policy = AuthorizationPolicies.ContentPermissionEmptyRecycleBin)] public IActionResult EmptyRecycleBin() { - _contentService.EmptyRecycleBin(_backofficeSecurityAccessor.BackOfficeSecurity.GetUserId().Result ?? -1); + _contentService.EmptyRecycleBin(_backofficeSecurityAccessor.BackOfficeSecurity?.GetUserId().Result ?? -1); return Ok(_localizedTextService.Localize("defaultdialogs", "recycleBinIsEmpty")); } @@ -1767,7 +1803,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers } //if there's nothing to sort just return ok - if (sorted.IdSortOrder.Length == 0) + if (sorted.IdSortOrder?.Length == 0) { return Ok(); } @@ -1783,7 +1819,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers try { // Save content with new sort order and update content xml in db accordingly - var sortResult = _contentService.Sort(sorted.IdSortOrder, _backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser.Id); + var sortResult = _contentService.Sort(sorted.IdSortOrder, _backofficeSecurityAccessor.BackOfficeSecurity?.CurrentUser?.Id ?? -1); if (!sortResult.Success) { _logger.LogWarning("Content sorting failed, this was probably caused by an event being cancelled"); @@ -1805,7 +1841,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers /// /// /// - public async Task PostMove(MoveOrCopy move) + public async Task PostMove(MoveOrCopy move) { // Authorize... var resource = new ContentPermissionsResource(_contentService.GetById(move.ParentId), ActionMove.ActionLetter); @@ -1822,7 +1858,11 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers } var toMove = toMoveResult.Value; - _contentService.Move(toMove, move.ParentId, _backofficeSecurityAccessor.BackOfficeSecurity.GetUserId().Result ?? 0); + if (toMove is null) + { + return null; + } + _contentService.Move(toMove, move.ParentId, _backofficeSecurityAccessor.BackOfficeSecurity?.GetUserId().Result ?? -1); return Content(toMove.Path, MediaTypeNames.Text.Plain, Encoding.UTF8); } @@ -1832,7 +1872,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers /// /// /// - public async Task> PostCopy(MoveOrCopy copy) + public async Task?> PostCopy(MoveOrCopy copy) { // Authorize... var resource = new ContentPermissionsResource(_contentService.GetById(copy.ParentId), ActionCopy.ActionLetter); @@ -1848,7 +1888,17 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers return toCopyResult.Result; } var toCopy = toCopyResult.Value; - var c = _contentService.Copy(toCopy, copy.ParentId, copy.RelateToOriginal, copy.Recursive, _backofficeSecurityAccessor.BackOfficeSecurity.GetUserId().Result ?? 0); + if (toCopy is null) + { + return null; + } + + var c = _contentService.Copy(toCopy, copy.ParentId, copy.RelateToOriginal, copy.Recursive, _backofficeSecurityAccessor.BackOfficeSecurity?.GetUserId().Result ?? -1); + + if (c is null) + { + return null; + } return Content(c.Path, MediaTypeNames.Text.Plain, Encoding.UTF8); } @@ -1859,7 +1909,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers /// The content and variants to unpublish /// [OutgoingEditorModelEvent] - public async Task> PostUnpublish(UnpublishContent model) + public async Task> PostUnpublish(UnpublishContent model) { var foundContent = _contentService.GetById(model.Id); @@ -1877,10 +1927,10 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers } var languageCount = _allLangs.Value.Count(); - if (model.Cultures.Length == 0 || model.Cultures.Length == languageCount) + if (model.Cultures?.Length == 0 || model.Cultures?.Length == languageCount) { //this means that the entire content item will be unpublished - var unpublishResult = _contentService.Unpublish(foundContent, userId: _backofficeSecurityAccessor.BackOfficeSecurity.GetUserId().Result ?? 0); + var unpublishResult = _contentService.Unpublish(foundContent, userId: _backofficeSecurityAccessor.BackOfficeSecurity?.GetUserId().Result ?? -1); var content = MapToDisplayWithSchedule(foundContent); @@ -1891,7 +1941,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers } else { - content.AddSuccessNotification( + content?.AddSuccessNotification( _localizedTextService.Localize("content", "unpublish"), _localizedTextService.Localize("speechBubbles", "contentUnpublished")); return content; @@ -1901,14 +1951,17 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers { //we only want to unpublish some of the variants var results = new Dictionary(); - foreach (var c in model.Cultures) + if (model.Cultures is not null) { - var result = _contentService.Unpublish(foundContent, culture: c, userId: _backofficeSecurityAccessor.BackOfficeSecurity.GetUserId().Result ?? 0); - results[c] = result; - if (result.Result == PublishResultType.SuccessUnpublishMandatoryCulture) + foreach (var c in model.Cultures) { - //if this happens, it means they are all unpublished, we don't need to continue - break; + var result = _contentService.Unpublish(foundContent, culture: c, userId: _backofficeSecurityAccessor.BackOfficeSecurity?.GetUserId().Result ?? -1); + results[c] = result; + if (result.Result == PublishResultType.SuccessUnpublishMandatoryCulture) + { + //if this happens, it means they are all unpublished, we don't need to continue + break; + } } } @@ -1917,7 +1970,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers //check for this status and return the correct message if (results.Any(x => x.Value.Result == PublishResultType.SuccessUnpublishMandatoryCulture)) { - content.AddSuccessNotification( + content?.AddSuccessNotification( _localizedTextService.Localize("content", "unpublish"), _localizedTextService.Localize("speechBubbles", "contentMandatoryCultureUnpublished")); return content; @@ -1926,7 +1979,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers //otherwise add a message for each one unpublished foreach (var r in results) { - content.AddSuccessNotification( + content?.AddSuccessNotification( _localizedTextService.Localize("conten", "unpublish"), _localizedTextService.Localize("speechBubbles", "contentCultureUnpublished", new[] { _allLangs.Value[r.Key].CultureName })); } @@ -1938,9 +1991,9 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers public ContentDomainsAndCulture GetCultureAndDomains(int id) { - var nodeDomains = _domainService.GetAssignedDomains(id, true).ToArray(); - var wildcard = nodeDomains.FirstOrDefault(d => d.IsWildcard); - var domains = nodeDomains.Where(d => !d.IsWildcard).Select(d => new DomainDisplay(d.DomainName, d.LanguageId.GetValueOrDefault(0))); + var nodeDomains = _domainService.GetAssignedDomains(id, true)?.ToArray(); + var wildcard = nodeDomains?.FirstOrDefault(d => d.IsWildcard); + var domains = nodeDomains?.Where(d => !d.IsWildcard).Select(d => new DomainDisplay(d.DomainName, d.LanguageId.GetValueOrDefault(0))); return new ContentDomainsAndCulture { Domains = domains, @@ -1951,15 +2004,18 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers [HttpPost] public ActionResult PostSaveLanguageAndDomains(DomainSave model) { - foreach (var domain in model.Domains) + if (model.Domains is not null) { - try + foreach (var domain in model.Domains) { - var uri = DomainUtilities.ParseUriFromDomainName(domain.Name, new Uri(Request.GetEncodedUrl())); - } - catch (UriFormatException) - { - return ValidationProblem(_localizedTextService.Localize("assignDomain", "invalidDomain")); + try + { + var uri = DomainUtilities.ParseUriFromDomainName(domain.Name, new Uri(Request.GetEncodedUrl())); + } + catch (UriFormatException) + { + return ValidationProblem(_localizedTextService.Localize("assignDomain", "invalidDomain")); + } } } @@ -1971,17 +2027,17 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers return NotFound("There is no content node with id {model.NodeId}."); } - var permission = _userService.GetPermissions(_backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser, node.Path); + var permission = _userService.GetPermissions(_backofficeSecurityAccessor.BackOfficeSecurity?.CurrentUser, node.Path); - if (permission.AssignedPermissions.Contains(ActionAssignDomain.ActionLetter.ToString(), StringComparer.Ordinal) == false) + if (permission?.AssignedPermissions.Contains(ActionAssignDomain.ActionLetter.ToString(), StringComparer.Ordinal) == false) { HttpContext.SetReasonPhrase("Permission Denied."); return BadRequest("You do not have permission to assign domains on that node."); } model.Valid = true; - var domains = _domainService.GetAssignedDomains(model.NodeId, true).ToArray(); + var domains = _domainService.GetAssignedDomains(model.NodeId, true)?.ToArray(); var languages = _localizationService.GetAllLanguages().ToArray(); var language = model.Language > 0 ? languages.FirstOrDefault(l => l.Id == model.Language) : null; @@ -1989,7 +2045,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers if (language != null) { // yet there is a race condition here... - var wildcard = domains.FirstOrDefault(d => d.IsWildcard); + var wildcard = domains?.FirstOrDefault(d => d.IsWildcard); if (wildcard != null) { wildcard.LanguageId = language.Id; @@ -2006,13 +2062,13 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers var saveAttempt = _domainService.Save(wildcard); if (saveAttempt == false) { - HttpContext.SetReasonPhrase(saveAttempt.Result.Result.ToString()); + HttpContext.SetReasonPhrase(saveAttempt.Result?.Result.ToString()); return BadRequest("Saving domain failed"); } } else { - var wildcard = domains.FirstOrDefault(d => d.IsWildcard); + var wildcard = domains?.FirstOrDefault(d => d.IsWildcard); if (wildcard != null) { _domainService.Delete(wildcard); @@ -2021,7 +2077,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers // process domains // delete every (non-wildcard) domain, that exists in the DB yet is not in the model - foreach (var domain in domains.Where(d => d.IsWildcard == false && model.Domains.All(m => m.Name.InvariantEquals(d.DomainName) == false))) + foreach (var domain in domains?.Where(d => d.IsWildcard == false && (model.Domains?.All(m => m.Name.InvariantEquals(d.DomainName) == false) ?? false)) ?? Array.Empty()) { _domainService.Delete(domain); } @@ -2029,7 +2085,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers var names = new List(); // create or update domains in the model - foreach (var domainModel in model.Domains.Where(m => string.IsNullOrWhiteSpace(m.Name) == false)) + foreach (var domainModel in model.Domains?.Where(m => string.IsNullOrWhiteSpace(m.Name) == false) ?? Array.Empty()) { language = languages.FirstOrDefault(l => l.Id == domainModel.Lang); if (language == null) @@ -2044,7 +2100,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers continue; } names.Add(name); - var domain = domains.FirstOrDefault(d => d.DomainName.InvariantEquals(domainModel.Name)); + var domain = domains?.FirstOrDefault(d => d.DomainName.InvariantEquals(domainModel.Name)); if (domain != null) { domain.LanguageId = language.Id; @@ -2054,14 +2110,17 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers { domainModel.Duplicate = true; var xdomain = _domainService.GetByName(domainModel.Name); - var xrcid = xdomain.RootContentId; + var xrcid = xdomain?.RootContentId; if (xrcid.HasValue) { var xcontent = _contentService.GetById(xrcid.Value); var xnames = new List(); while (xcontent != null) { - xnames.Add(xcontent.Name); + if (xcontent.Name is not null) + { + xnames.Add(xcontent.Name); + } if (xcontent.ParentId < -1) xnames.Add("Recycle Bin"); xcontent = _contentService.GetParent(xcontent); @@ -2081,13 +2140,13 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers var saveAttempt = _domainService.Save(newDomain); if (saveAttempt == false) { - HttpContext.SetReasonPhrase(saveAttempt.Result.Result.ToString()); + HttpContext.SetReasonPhrase(saveAttempt.Result?.Result.ToString()); return BadRequest("Saving new domain failed"); } } } - model.Valid = model.Domains.All(m => m.Duplicate == false); + model.Valid = model.Domains?.All(m => m.Duplicate == false) ?? false; return model; } @@ -2100,17 +2159,20 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers /// /// This is required to wire up the validation in the save/publish dialog /// - private void HandleInvalidModelState(ContentItemDisplay display, string cultureForInvariantErrors) + private void HandleInvalidModelState(ContentItemDisplay? display, string? cultureForInvariantErrors) where TVariant : ContentVariantDisplay { - if (!ModelState.IsValid && display.Variants.Count() > 1) + if (!ModelState.IsValid && display?.Variants?.Count() > 1) { //Add any culture specific errors here var variantErrors = ModelState.GetVariantsWithErrors(cultureForInvariantErrors); - foreach (var (culture, segment) in variantErrors) + if (variantErrors is not null) { - AddVariantValidationError(culture, segment, "speechBubbles", "contentCultureValidationError"); + foreach (var (culture, segment) in variantErrors) + { + AddVariantValidationError(culture, segment, "speechBubbles", "contentCultureValidationError"); + } } } } @@ -2122,10 +2184,10 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers private void MapValuesForPersistence(ContentItemSave contentSave) { // inline method to determine the culture and segment to persist the property - (string culture, string segment) PropertyCultureAndSegment(IProperty property, ContentVariantSave variant) + (string? culture, string? segment) PropertyCultureAndSegment(IProperty? property, ContentVariantSave variant) { - var culture = property.PropertyType.VariesByCulture() ? variant.Culture : null; - var segment = property.PropertyType.VariesBySegment() ? variant.Segment : null; + var culture = property?.PropertyType.VariesByCulture() ?? false ? variant.Culture : null; + var segment = property?.PropertyType.VariesBySegment() ?? false ? variant.Segment : null; return (culture, segment); } @@ -2144,7 +2206,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers // Don't update the name if it is empty if (!variant.Name.IsNullOrWhiteSpace()) { - if (contentSave.PersistedContent.ContentType.VariesByCulture()) + if (contentSave.PersistedContent?.ContentType.VariesByCulture() ?? false) { if (variant.Culture.IsNullOrWhiteSpace()) { @@ -2154,14 +2216,17 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers contentSave.PersistedContent.SetCultureName(variant.Name, variant.Culture); // If the variant culture is the default culture we also want to update the name on the Content itself. - if (variant.Culture.Equals(defaultCulture, StringComparison.InvariantCultureIgnoreCase)) + if (variant.Culture?.Equals(defaultCulture, StringComparison.InvariantCultureIgnoreCase) ?? false) { contentSave.PersistedContent.Name = variant.Name; } } else { - contentSave.PersistedContent.Name = variant.Name; + if (contentSave.PersistedContent is not null) + { + contentSave.PersistedContent.Name = variant.Name; + } } } @@ -2172,8 +2237,8 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers ? variant.PropertyCollectionDto : new ContentPropertyCollectionDto { - Properties = variant.PropertyCollectionDto.Properties.Where( - x => !x.Culture.IsNullOrWhiteSpace() || !x.Segment.IsNullOrWhiteSpace()), + Properties = variant.PropertyCollectionDto?.Properties.Where( + x => !x.Culture.IsNullOrWhiteSpace() || !x.Segment.IsNullOrWhiteSpace()) ?? Enumerable.Empty(), }; // for each variant, map the property values @@ -2184,13 +2249,13 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers { // Get property value (var culture, var segment) = PropertyCultureAndSegment(property, variant); - return property.GetValue(culture, segment); + return property?.GetValue(culture, segment); }, (save, property, v) => { // Set property value (var culture, var segment) = PropertyCultureAndSegment(property, variant); - property.SetValue(v, culture, segment); + property?.SetValue(v, culture, segment); }, variant.Culture); @@ -2198,28 +2263,28 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers } // Map IsDirty cultures to edited cultures, to make it easier to verify changes on specific variants on Saving and Saved events. - IEnumerable editedCultures = contentSave.PersistedContent.CultureInfos.Values + IEnumerable? editedCultures = contentSave.PersistedContent?.CultureInfos?.Values .Where(x => x.IsDirty()) .Select(x => x.Culture); - contentSave.PersistedContent.SetCultureEdited(editedCultures); + contentSave.PersistedContent?.SetCultureEdited(editedCultures); // handle template if (string.IsNullOrWhiteSpace(contentSave.TemplateAlias)) // cleared: clear if not already null { - if (contentSave.PersistedContent.TemplateId != null) + if (contentSave.PersistedContent?.TemplateId != null) { contentSave.PersistedContent.TemplateId = null; } } else // set: update if different { - ITemplate template = _fileService.GetTemplate(contentSave.TemplateAlias); + ITemplate? template = _fileService.GetTemplate(contentSave.TemplateAlias); if (template is null) { // ModelState.AddModelError("Template", "No template exists with the specified alias: " + contentItem.TemplateAlias); _logger.LogWarning("No template exists with the specified alias: {TemplateAlias}", contentSave.TemplateAlias); } - else if (template.Id != contentSave.PersistedContent.TemplateId) + else if (contentSave.PersistedContent is not null && template.Id != contentSave.PersistedContent.TemplateId) { contentSave.PersistedContent.TemplateId = template.Id; } @@ -2263,7 +2328,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers var parentContentType = _contentTypeService.Get(parent.ContentTypeId); //check if the item is allowed under this one - if (parentContentType.AllowedContentTypes.Select(x => x.Id).ToArray() + if (parentContentType?.AllowedContentTypes?.Select(x => x.Id).ToArray() .Any(x => x.Value == toMove.ContentType.Id) == false) { return ValidationProblem( @@ -2289,7 +2354,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers /// /// This is null when dealing with invariant content, else it's the cultures that were successfully published /// - private void AddMessageForPublishStatus(IReadOnlyCollection statuses, INotificationModel display, string[] successfulCultures = null) + private void AddMessageForPublishStatus(IReadOnlyCollection statuses, INotificationModel display, string[]? successfulCultures = null) { var totalStatusCount = statuses.Count(); @@ -2389,7 +2454,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers case PublishResultType.FailedPublishPathNotPublished: { //TODO: This doesn't take into account variations with the successfulCultures param - var names = string.Join(", ", status.Select(x => $"'{x.Content.Name}'")); + var names = string.Join(", ", status.Select(x => $"'{x.Content?.Name}'")); display.AddWarningNotification( _localizedTextService.Localize(null,"publish"), _localizedTextService.Localize("publish", "contentPublishedFailedByParent", @@ -2399,14 +2464,14 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers case PublishResultType.FailedPublishCancelledByEvent: { //TODO: This doesn't take into account variations with the successfulCultures param - var names = string.Join(", ", status.Select(x => $"'{x.Content.Name}'")); + var names = string.Join(", ", status.Select(x => $"'{x.Content?.Name}'")); AddCancelMessage(display, "publish","contentPublishedFailedByEvent", messageParams: new[] { names }); } break; case PublishResultType.FailedPublishAwaitingRelease: { //TODO: This doesn't take into account variations with the successfulCultures param - var names = string.Join(", ", status.Select(x => $"'{x.Content.Name}'")); + var names = string.Join(", ", status.Select(x => $"'{x.Content?.Name}'")); display.AddWarningNotification( _localizedTextService.Localize(null,"publish"), _localizedTextService.Localize("publish", "contentPublishedFailedAwaitingRelease", @@ -2416,7 +2481,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers case PublishResultType.FailedPublishHasExpired: { //TODO: This doesn't take into account variations with the successfulCultures param - var names = string.Join(", ", status.Select(x => $"'{x.Content.Name}'")); + var names = string.Join(", ", status.Select(x => $"'{x.Content?.Name}'")); display.AddWarningNotification( _localizedTextService.Localize(null,"publish"), _localizedTextService.Localize("publish", "contentPublishedFailedExpired", @@ -2426,7 +2491,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers case PublishResultType.FailedPublishIsTrashed: { //TODO: This doesn't take into account variations with the successfulCultures param - var names = string.Join(", ", status.Select(x => $"'{x.Content.Name}'")); + var names = string.Join(", ", status.Select(x => $"'{x.Content?.Name}'")); display.AddWarningNotification( _localizedTextService.Localize(null,"publish"), _localizedTextService.Localize("publish", "contentPublishedFailedIsTrashed", @@ -2437,7 +2502,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers { if (successfulCultures == null) { - var names = string.Join(", ", status.Select(x => $"'{x.Content.Name}'")); + var names = string.Join(", ", status.Select(x => $"'{x.Content?.Name}'")); display.AddWarningNotification( _localizedTextService.Localize(null,"publish"), _localizedTextService.Localize("publish", "contentPublishedFailedInvalid", @@ -2447,7 +2512,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers { foreach (var c in successfulCultures) { - var names = string.Join(", ", status.Select(x => $"'{(x.Content.ContentType.VariesByCulture() ? x.Content.GetCultureName(c) : x.Content.Name)}'")); + var names = string.Join(", ", status.Select(x => $"'{(x.Content?.ContentType.VariesByCulture() ?? false ? x.Content.GetCultureName(c) : x.Content?.Name)}'")); display.AddWarningNotification( _localizedTextService.Localize(null,"publish"), _localizedTextService.Localize("publish", "contentPublishedFailedInvalid", @@ -2472,20 +2537,30 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers /// /// /// - private ContentItemDisplay MapToDisplay(IContent? content) => + private ContentItemDisplay? MapToDisplay(IContent? content) => MapToDisplay(content, context => { - context.Items["CurrentUser"] = _backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser; + context.Items["CurrentUser"] = _backofficeSecurityAccessor.BackOfficeSecurity?.CurrentUser; }); - private ContentItemDisplayWithSchedule MapToDisplayWithSchedule(IContent content) + private ContentItemDisplayWithSchedule? MapToDisplayWithSchedule(IContent? content) { - ContentItemDisplayWithSchedule display = _umbracoMapper.Map(content, context => + if (content is null) { - context.Items["CurrentUser"] = _backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser; + return null; + } + + ContentItemDisplayWithSchedule? display = _umbracoMapper.Map(content, context => + { + context.Items["CurrentUser"] = _backofficeSecurityAccessor.BackOfficeSecurity?.CurrentUser; context.Items["Schedule"] = _contentService.GetContentScheduleByContentId(content.Id); }); - display.AllowPreview = display.AllowPreview && content.Trashed == false && content.ContentType.IsElement == false; + + if (display is not null) + { + display.AllowPreview = display.AllowPreview && content?.Trashed == false && content.ContentType.IsElement == false; + } + return display; } @@ -2642,8 +2717,8 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers var content = MapToDisplay(version); return culture == null - ? content.Variants?.FirstOrDefault() //No culture set - so this is an invariant node - so just list me the first item in here - : content.Variants?.FirstOrDefault(x => x.Language?.IsoCode == culture); + ? content?.Variants?.FirstOrDefault() //No culture set - so this is an invariant node - so just list me the first item in here + : content?.Variants?.FirstOrDefault(x => x.Language?.IsoCode == culture); } [Authorize(Policy = AuthorizationPolicies.ContentPermissionRollbackById)] diff --git a/src/Umbraco.Web.BackOffice/Controllers/ContentTypeControllerBase.cs b/src/Umbraco.Web.BackOffice/Controllers/ContentTypeControllerBase.cs index 38f3c82008..3c867ac88e 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/ContentTypeControllerBase.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/ContentTypeControllerBase.cs @@ -76,7 +76,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers /// /// Whether the composite content types should be applicable for an element type /// - protected ActionResult>> PerformGetAvailableCompositeContentTypes( + protected ActionResult>> PerformGetAvailableCompositeContentTypes( int contentTypeId, UmbracoObjectTypes type, string[]? filterContentTypes, @@ -146,33 +146,39 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers return availableCompositions.Results .Select(x => - new Tuple(UmbracoMapper.Map(x.Composition), + new Tuple(UmbracoMapper.Map(x.Composition), x.Allowed)) .Select(x => { //we need to ensure that the item is enabled if it is already selected // but do not allow it if it is any of the ancestors - if (compAliases.Contains(x.Item1.Alias) && ancestors.Contains(x.Item1.Alias) == false) + if (compAliases.Contains(x.Item1?.Alias) && ancestors.Contains(x.Item1?.Alias) == false) { //re-set x to be allowed (NOTE: I didn't know you could set an enumerable item in a lambda!) - x = new Tuple(x.Item1, true); + x = new Tuple(x.Item1, true); } //translate the name - x.Item1.Name = TranslateItem(x.Item1.Name); + if (x.Item1 is not null) + { + x.Item1.Name = TranslateItem(x.Item1.Name); + } - IContentTypeComposition contentType = allContentTypes.FirstOrDefault(c => c.Key == x.Item1.Key); - EntityContainer[] containers = GetEntityContainers(contentType, type)?.ToArray(); + IContentTypeComposition? contentType = allContentTypes.FirstOrDefault(c => c.Key == x.Item1?.Key); + EntityContainer[]? containers = GetEntityContainers(contentType, type)?.ToArray(); var containerPath = $"/{(containers != null && containers.Any() ? $"{string.Join("/", containers.Select(c => c.Name))}/" : null)}"; - x.Item1.AdditionalData["containerPath"] = containerPath; + if (x.Item1 is not null) + { + x.Item1.AdditionalData["containerPath"] = containerPath; + } return x; }) .ToList(); } - private IEnumerable GetEntityContainers(IContentTypeComposition contentType, + private IEnumerable? GetEntityContainers(IContentTypeComposition? contentType, UmbracoObjectTypes type) { if (contentType == null) @@ -206,7 +212,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers if (contentTypeId > 0) { - IContentTypeComposition source; + IContentTypeComposition? source; switch (type) { @@ -262,6 +268,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers return composedOf .Select(UmbracoMapper.Map) + .WhereNotNull() .Select(TranslateName) .ToList(); } @@ -279,20 +286,20 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers } text = text.Substring(1); - return CultureDictionary[text].IfNullOrWhiteSpace(text); + return CultureDictionary[text]?.IfNullOrWhiteSpace(text); } - protected ActionResult PerformPostSave( + protected ActionResult PerformPostSave( TContentTypeSave contentTypeSave, Func getContentType, - Action saveContentType, + Action saveContentType, Action? beforeCreateNew = null) where TContentTypeDisplay : ContentTypeCompositionDisplay where TContentTypeSave : ContentTypeSave where TPropertyType : PropertyTypeBasic { var ctId = Convert.ToInt32(contentTypeSave.Id); - TContentType ct = ctId > 0 ? getContentType(ctId) : null; + TContentType? ct = ctId > 0 ? getContentType(ctId) : null; if (ctId > 0 && ct == null) { return NotFound(); @@ -304,7 +311,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers // works since that is based on aliases. IEnumerable allAliases = ContentTypeService.GetAllContentTypeAliases(); var exists = allAliases.InvariantContains(contentTypeSave.Alias); - if (exists && (ctId == 0 || !ct.Alias.InvariantEquals(contentTypeSave.Alias))) + if (exists && (ctId == 0 || (!ct?.Alias.InvariantEquals(contentTypeSave.Alias) ?? false))) { ModelState.AddModelError("Alias", LocalizedTextService.Localize("editcontenttype", "aliasAlreadyExists")); @@ -315,7 +322,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers if (ModelState.IsValid == false) { - TContentTypeDisplay err = + TContentTypeDisplay? err = CreateModelStateValidationEror(ctId, contentTypeSave, ct); return ValidationProblem(err); } @@ -338,7 +345,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers } catch (Exception ex) { - TContentTypeDisplay responseEx = + TContentTypeDisplay? responseEx = CreateInvalidCompositionResponseException( ex, contentTypeSave, ct, ctId); if (responseEx != null) @@ -347,7 +354,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers } } - TContentTypeDisplay exResult = + TContentTypeDisplay? exResult = CreateCompositionValidationExceptionIfInvalid( contentTypeSave, ct); if (exResult != null) @@ -381,7 +388,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers //save as new - TContentType newCt = null; + TContentType? newCt = null; try { //This mapping will cause a lot of content type validation to occur which we need to deal with @@ -389,7 +396,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers } catch (Exception ex) { - TContentTypeDisplay responseEx = + TContentTypeDisplay? responseEx = CreateInvalidCompositionResponseException( ex, contentTypeSave, ct, ctId); if (responseEx is null) @@ -400,7 +407,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers return ValidationProblem(responseEx); } - TContentTypeDisplay exResult = + TContentTypeDisplay? exResult = CreateCompositionValidationExceptionIfInvalid( contentTypeSave, newCt); if (exResult != null) @@ -419,7 +426,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers if (allowItselfAsChild && newCt != null) { newCt.AllowedContentTypes = - newCt.AllowedContentTypes.Union( + newCt.AllowedContentTypes?.Union( new[] { new ContentTypeSort(newCt.Id, allowIfselfAsChildSortOrder) } ); saveContentType(newCt); @@ -441,7 +448,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers foreach (ValidationResult r in validationResults) foreach (var m in r.MemberNames) { - modelState.AddModelError(m, r.ErrorMessage); + modelState.AddModelError(m, r.ErrorMessage ?? string.Empty); } } @@ -469,7 +476,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers return Content(toMove.Path, MediaTypeNames.Text.Plain, Encoding.UTF8); } - switch (result.Result.Result) + switch (result.Result?.Result) { case MoveOperationStatusType.FailedParentNotFound: return NotFound(); @@ -494,19 +501,19 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers Func getContentType, Func?>> doCopy) { - TContentType toMove = getContentType(move.Id); + TContentType? toMove = getContentType(move.Id); if (toMove == null) { return NotFound(); } - Attempt> result = doCopy(toMove, move.ParentId); + Attempt?> result = doCopy(toMove, move.ParentId); if (result.Success) { return Content(toMove.Path, MediaTypeNames.Text.Plain, Encoding.UTF8); } - switch (result.Result.Result) + switch (result.Result?.Result) { case MoveOperationStatusType.FailedParentNotFound: return NotFound(); @@ -526,19 +533,19 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers /// /// /// - private TContentTypeDisplay CreateCompositionValidationExceptionIfInvalid(TContentTypeSave contentTypeSave, TContentType composition) + private TContentTypeDisplay? CreateCompositionValidationExceptionIfInvalid(TContentTypeSave contentTypeSave, TContentType? composition) where TContentTypeSave : ContentTypeSave where TPropertyType : PropertyTypeBasic where TContentTypeDisplay : ContentTypeCompositionDisplay { - IContentTypeBaseService service = GetContentTypeService(); - Attempt validateAttempt = service.ValidateComposition(composition); + IContentTypeBaseService? service = GetContentTypeService(); + Attempt validateAttempt = service?.ValidateComposition(composition) ?? Attempt.Fail(); if (validateAttempt == false) { // if it's not successful then we need to return some model state for the property type and property group // aliases that are duplicated - IEnumerable duplicatePropertyTypeAliases = validateAttempt.Result.Distinct(); + IEnumerable? duplicatePropertyTypeAliases = validateAttempt.Result?.Distinct(); var invalidPropertyGroupAliases = (validateAttempt.Exception as InvalidCompositionException)?.PropertyGroupAliases ?? Array.Empty(); @@ -546,17 +553,21 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers AddCompositionValidationErrors(contentTypeSave, duplicatePropertyTypeAliases, invalidPropertyGroupAliases); - TContentTypeDisplay display = UmbracoMapper.Map(composition); + TContentTypeDisplay? display = UmbracoMapper.Map(composition); //map the 'save' data on top display = UmbracoMapper.Map(contentTypeSave, display); - display.Errors = ModelState.ToErrorDictionary(); + if (display is not null) + { + display.Errors = ModelState.ToErrorDictionary(); + } + return display; } return null; } - public IContentTypeBaseService GetContentTypeService() + public IContentTypeBaseService? GetContentTypeService() where T : IContentTypeComposition { if (typeof(T).Implements()) @@ -585,32 +596,38 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers /// /// private void AddCompositionValidationErrors(TContentTypeSave contentTypeSave, - IEnumerable duplicatePropertyTypeAliases, IEnumerable invalidPropertyGroupAliases) + IEnumerable? duplicatePropertyTypeAliases, IEnumerable? invalidPropertyGroupAliases) where TContentTypeSave : ContentTypeSave where TPropertyType : PropertyTypeBasic { - foreach (var propertyTypeAlias in duplicatePropertyTypeAliases) + if (duplicatePropertyTypeAliases is not null) { - // Find the property type relating to these - TPropertyType property = contentTypeSave.Groups.SelectMany(x => x.Properties) - .Single(x => x.Alias == propertyTypeAlias); - PropertyGroupBasic group = - contentTypeSave.Groups.Single(x => x.Properties.Contains(property)); - var propertyIndex = group.Properties.IndexOf(property); - var groupIndex = contentTypeSave.Groups.IndexOf(group); + foreach (var propertyTypeAlias in duplicatePropertyTypeAliases) + { + // Find the property type relating to these + TPropertyType property = contentTypeSave.Groups.SelectMany(x => x.Properties) + .Single(x => x.Alias == propertyTypeAlias); + PropertyGroupBasic group = + contentTypeSave.Groups.Single(x => x.Properties.Contains(property)); + var propertyIndex = group.Properties.IndexOf(property); + var groupIndex = contentTypeSave.Groups.IndexOf(group); - var key = $"Groups[{groupIndex}].Properties[{propertyIndex}].Alias"; - ModelState.AddModelError(key, "Duplicate property aliases aren't allowed between compositions"); + var key = $"Groups[{groupIndex}].Properties[{propertyIndex}].Alias"; + ModelState.AddModelError(key, "Duplicate property aliases aren't allowed between compositions"); + } } - foreach (var propertyGroupAlias in invalidPropertyGroupAliases) + if (invalidPropertyGroupAliases is not null) { - // Find the property group relating to these - PropertyGroupBasic group = - contentTypeSave.Groups.Single(x => x.Alias == propertyGroupAlias); - var groupIndex = contentTypeSave.Groups.IndexOf(group); - var key = $"Groups[{groupIndex}].Name"; - ModelState.AddModelError(key, "Different group types aren't allowed between compositions"); + foreach (var propertyGroupAlias in invalidPropertyGroupAliases) + { + // Find the property group relating to these + PropertyGroupBasic group = + contentTypeSave.Groups.Single(x => x.Alias == propertyGroupAlias); + var groupIndex = contentTypeSave.Groups.IndexOf(group); + var key = $"Groups[{groupIndex}].Name"; + ModelState.AddModelError(key, "Different group types aren't allowed between compositions"); + } } } @@ -625,14 +642,14 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers /// /// /// - private TContentTypeDisplay CreateInvalidCompositionResponseException( - Exception ex, TContentTypeSave contentTypeSave, TContentType ct, int ctId) + Exception ex, TContentTypeSave contentTypeSave, TContentType? ct, int ctId) where TContentTypeDisplay : ContentTypeCompositionDisplay where TContentTypeSave : ContentTypeSave where TPropertyType : PropertyTypeBasic { - InvalidCompositionException invalidCompositionException = null; + InvalidCompositionException? invalidCompositionException = null; if (ex is InvalidCompositionException) { invalidCompositionException = (InvalidCompositionException)ex; @@ -660,12 +677,12 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers /// /// /// - private TContentTypeDisplay CreateModelStateValidationEror(int ctId, - TContentTypeSave contentTypeSave, TContentType ct) + private TContentTypeDisplay? CreateModelStateValidationEror(int ctId, + TContentTypeSave contentTypeSave, TContentType? ct) where TContentTypeDisplay : ContentTypeCompositionDisplay where TContentTypeSave : ContentTypeSave { - TContentTypeDisplay forDisplay; + TContentTypeDisplay? forDisplay; if (ctId > 0) { //Required data is invalid so we cannot continue @@ -679,7 +696,11 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers forDisplay = UmbracoMapper.Map(contentTypeSave); } - forDisplay.Errors = ModelState.ToErrorDictionary(); + if (forDisplay is not null) + { + forDisplay.Errors = ModelState.ToErrorDictionary(); + } + return forDisplay; } } diff --git a/src/Umbraco.Web.BackOffice/Controllers/EntityController.cs b/src/Umbraco.Web.BackOffice/Controllers/EntityController.cs index a01b88501f..a31ee6b27e 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/EntityController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/EntityController.cs @@ -160,7 +160,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers /// If set used to look up whether user and group start node permissions will be ignored. /// [HttpGet] - public IEnumerable Search(string query, UmbracoEntityTypes type, string searchFrom = null, + public IEnumerable Search(string query, UmbracoEntityTypes type, string? searchFrom = null, Guid? dataTypeKey = null) { // NOTE: Theoretically you shouldn't be able to see member data if you don't have access to members right? ... but there is a member picker, so can't really do that @@ -202,27 +202,31 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers return result; } - var allowedSections = _backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser.AllowedSections.ToArray(); + var allowedSections = _backofficeSecurityAccessor.BackOfficeSecurity?.CurrentUser?.AllowedSections.ToArray(); foreach (KeyValuePair searchableTree in _searchableTreeCollection .SearchableApplicationTrees.OrderBy(t => t.Value.SortOrder)) { - if (allowedSections.Contains(searchableTree.Value.AppAlias)) + if (allowedSections?.Contains(searchableTree.Value.AppAlias) ?? false) { - Tree tree = _treeService.GetByAlias(searchableTree.Key); + Tree? tree = _treeService.GetByAlias(searchableTree.Key); if (tree == null) { continue; //shouldn't occur } - result[Tree.GetRootNodeDisplayName(tree, _localizedTextService)] = new TreeSearchResult + var rootNodeDisplayName = Tree.GetRootNodeDisplayName(tree, _localizedTextService); + if (rootNodeDisplayName is not null) { - Results = searchableTree.Value.SearchableTree.Search(query, 200, 0, out var total), - TreeAlias = searchableTree.Key, - AppAlias = searchableTree.Value.AppAlias, - JsFormatterService = searchableTree.Value.FormatterService, - JsFormatterMethod = searchableTree.Value.FormatterMethod - }; + result[rootNodeDisplayName] = new TreeSearchResult + { + Results = searchableTree.Value.SearchableTree.Search(query, 200, 0, out var total).WhereNotNull(), + TreeAlias = searchableTree.Key, + AppAlias = searchableTree.Value.AppAlias, + JsFormatterService = searchableTree.Value.FormatterService, + JsFormatterMethod = searchableTree.Value.FormatterMethod + }; + } } } @@ -237,8 +241,8 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers /// public IConvertToActionResult GetPath(int id, UmbracoEntityTypes type) { - ActionResult foundContentResult = GetResultForId(id, type); - EntityBasic foundContent = foundContentResult.Value; + ActionResult foundContentResult = GetResultForId(id, type); + EntityBasic? foundContent = foundContentResult.Value; if (foundContent is null) { return foundContentResult; @@ -257,8 +261,8 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers /// public IConvertToActionResult GetPath(Guid id, UmbracoEntityTypes type) { - ActionResult foundContentResult = GetResultForKey(id, type); - EntityBasic foundContent = foundContentResult.Value; + ActionResult foundContentResult = GetResultForKey(id, type); + EntityBasic? foundContent = foundContentResult.Value; if (foundContent is null) { return foundContentResult; @@ -333,14 +337,14 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers /// [HttpGet] [HttpPost] - public IDictionary GetUrlsByIds([FromJsonPath] int[] ids, [FromQuery] UmbracoEntityTypes type, [FromQuery] string culture = null) + public IDictionary GetUrlsByIds([FromJsonPath] int[] ids, [FromQuery] UmbracoEntityTypes type, [FromQuery] string? culture = null) { if (ids == null || !ids.Any()) { - return new Dictionary(); + return new Dictionary(); } - string MediaOrDocumentUrl(int id) + string? MediaOrDocumentUrl(int id) { switch (type) { @@ -349,7 +353,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers case UmbracoEntityTypes.Media: { - IPublishedContent media = _publishedContentQuery.Media(id); + IPublishedContent? media = _publishedContentQuery.Media(id); // NOTE: If culture is passed here we get an empty string rather than a media item URL. return _publishedUrlProvider.GetMediaUrl(media, culture: null); @@ -382,14 +386,14 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers /// [HttpGet] [HttpPost] - public IDictionary GetUrlsByIds([FromJsonPath] Guid[] ids, [FromQuery] UmbracoEntityTypes type, [FromQuery] string culture = null) + public IDictionary GetUrlsByIds([FromJsonPath] Guid[] ids, [FromQuery] UmbracoEntityTypes type, [FromQuery] string? culture = null) { if (ids == null || !ids.Any()) { - return new Dictionary(); + return new Dictionary(); } - string MediaOrDocumentUrl(Guid id) + string? MediaOrDocumentUrl(Guid id) { return type switch { @@ -424,15 +428,15 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers /// [HttpGet] [HttpPost] - public IDictionary GetUrlsByIds([FromJsonPath] Udi[] ids, [FromQuery] UmbracoEntityTypes type, [FromQuery] string culture = null) + public IDictionary GetUrlsByIds([FromJsonPath] Udi[] ids, [FromQuery] UmbracoEntityTypes type, [FromQuery] string? culture = null) { if (ids == null || !ids.Any()) { - return new Dictionary(); + return new Dictionary(); } // TODO: PMJ 2021-09-27 - Should GetUrl(Udi) exist as an extension method on UrlProvider/IUrlProvider (in v9) - string MediaOrDocumentUrl(Udi id) + string? MediaOrDocumentUrl(Udi id) { if (id is not GuidUdi guidUdi) { @@ -472,11 +476,11 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers [HttpGet] [HttpPost] [Obsolete("Use GetUrlsByIds instead.")] - public IDictionary GetUrlsByUdis([FromJsonPath] Udi[] udis, string? culture = null) + public IDictionary GetUrlsByUdis([FromJsonPath] Udi[] udis, string? culture = null) { if (udis == null || !udis.Any()) { - return new Dictionary(); + return new Dictionary(); } var udiEntityType = udis.First().EntityType; @@ -546,7 +550,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers /// /// /// - public ActionResult GetByQuery(string query, int nodeContextId, UmbracoEntityTypes type) + public ActionResult? GetByQuery(string query, int nodeContextId, UmbracoEntityTypes type) { // TODO: Rename this!!! It's misleading, it should be GetByXPath @@ -558,7 +562,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers var q = ParseXPathQuery(query, nodeContextId); - IPublishedContent node = _publishedContentQuery.ContentSingleAtXPath(q); + IPublishedContent? node = _publishedContentQuery.ContentSingleAtXPath(q); if (node == null) { @@ -575,8 +579,8 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers id, nodeid => { - IEntitySlim ent = _entityService.Get(nodeid); - return ent.Path.Split(Constants.CharArrays.Comma).Reverse(); + IEntitySlim? ent = _entityService.Get(nodeid); + return ent?.Path.Split(Constants.CharArrays.Comma).Reverse(); }, i => _publishedContentQuery.Content(i) != null); @@ -593,7 +597,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers } [HttpGet] - public UrlAndAnchors GetUrlAndAnchors(int id, string culture = "*") + public UrlAndAnchors GetUrlAndAnchors(int id, string? culture = "*") { culture = culture ?? ClientCulture(); @@ -606,6 +610,11 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers [HttpPost] public IEnumerable GetAnchors(AnchorsModel model) { + if (model.RteContent is null) + { + return Enumerable.Empty(); + } + IEnumerable anchorValues = _contentService.GetAnchorValuesFromRTEContent(model.RteContent); return anchorValues; } @@ -631,15 +640,15 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers return Enumerable.Empty(); } - var pr = new List(nodes.Select(_umbracoMapper.Map)); + var pr = new List(nodes.Select(_umbracoMapper.Map).WhereNotNull()); return pr; } // else proceed as usual return _entityService.GetChildren(id, objectType.Value) - .WhereNotNull() - .Select(_umbracoMapper.Map); + .Select(_umbracoMapper.Map) + .WhereNotNull(); } //now we need to convert the unknown ones @@ -768,7 +777,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers out totalRecords, filter.IsNullOrWhiteSpace() ? null - : _sqlContext.Query().Where(x => x.Name.Contains(filter)), + : _sqlContext.Query().Where(x => x.Name!.Contains(filter)), Ordering.By(orderBy, orderDirection)); @@ -782,15 +791,19 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers { Items = entities.Select(source => { - EntityBasic target = _umbracoMapper.Map(source, context => + EntityBasic? target = _umbracoMapper.Map(source, context => { context.SetCulture(culture); context.SetCulture(culture); }); - //TODO: Why is this here and not in the mapping? - target.AdditionalData["hasChildren"] = source.HasChildren; + + if (target is not null) + { + //TODO: Why is this here and not in the mapping? + target.AdditionalData["hasChildren"] = source.HasChildren; + } return target; - }) + }).WhereNotNull(), }; return pagedResult; @@ -815,11 +828,11 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers switch (type) { case UmbracoEntityTypes.Document: - return _backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser.CalculateContentStartNodeIds( - _entityService, _appCaches); + return _backofficeSecurityAccessor.BackOfficeSecurity?.CurrentUser?.CalculateContentStartNodeIds( + _entityService, _appCaches) ?? Array.Empty(); case UmbracoEntityTypes.Media: - return _backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser.CalculateMediaStartNodeIds( - _entityService, _appCaches); + return _backofficeSecurityAccessor.BackOfficeSecurity?.CurrentUser?.CalculateMediaStartNodeIds( + _entityService, _appCaches) ?? Array.Empty(); default: return Array.Empty(); } @@ -864,18 +877,18 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers entities = aids == null || aids.Contains(Constants.System.Root) || ignoreUserStartNodes ? _entityService.GetPagedDescendants(objectType.Value, pageNumber - 1, pageSize, out totalRecords, - _sqlContext.Query().Where(x => x.Name.Contains(filter)), + _sqlContext.Query().Where(x => x.Name!.Contains(filter)), Ordering.By(orderBy, orderDirection), false) : _entityService.GetPagedDescendants(aids, objectType.Value, pageNumber - 1, pageSize, out totalRecords, - _sqlContext.Query().Where(x => x.Name.Contains(filter)), + _sqlContext.Query().Where(x => x.Name!.Contains(filter)), Ordering.By(orderBy, orderDirection)); } else { entities = _entityService.GetPagedDescendants(id, objectType.Value, pageNumber - 1, pageSize, out totalRecords, - _sqlContext.Query().Where(x => x.Name.Contains(filter)), + _sqlContext.Query().Where(x => x.Name!.Contains(filter)), Ordering.By(orderBy, orderDirection)); } @@ -886,7 +899,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers var pagedResult = new PagedResult(totalRecords, pageNumber, pageSize) { - Items = entities.Select(MapEntities()) + Items = entities.Select(MapEntities()).WhereNotNull(), }; return pagedResult; @@ -920,7 +933,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers [ModelBinder(typeof(HttpQueryStringModelBinder))] FormCollection queryStrings) { - IEntitySlim entity = _entityService.Get(id); + IEntitySlim? entity = _entityService.Get(id); if (entity is null) { return NotFound(); @@ -938,7 +951,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers /// If set to true, user and group start node permissions will be ignored. /// private IEnumerable ExamineSearch(string query, UmbracoEntityTypes entityType, - string searchFrom = null, bool ignoreUserStartNodes = false) + string? searchFrom = null, bool ignoreUserStartNodes = false) { var culture = ClientCulture(); return _treeSearcher.ExamineSearch(query, entityType, 200, 0, out _, culture, searchFrom, @@ -953,8 +966,8 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers // TODO: Need to check for Object types that support hierarchic here, some might not. return _entityService.GetChildren(id, objectType.Value) - .WhereNotNull() - .Select(MapEntities()); + .Select(MapEntities()) + .WhereNotNull(); } //now we need to convert the unknown ones @@ -970,30 +983,30 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers } private IEnumerable GetResultForAncestors(int id, UmbracoEntityTypes entityType, - FormCollection queryStrings = null) + FormCollection? queryStrings = null) { UmbracoObjectTypes? objectType = ConvertToObjectType(entityType); if (objectType.HasValue) { // TODO: Need to check for Object types that support hierarchic here, some might not. - var ids = _entityService.Get(id).Path.Split(Constants.CharArrays.Comma) + var ids = _entityService.Get(id)?.Path.Split(Constants.CharArrays.Comma) .Select(s => int.Parse(s, CultureInfo.InvariantCulture)).Distinct().ToArray(); var ignoreUserStartNodes = IsDataTypeIgnoringUserStartNodes(queryStrings?.GetValue("dataTypeId")); if (ignoreUserStartNodes == false) { - int[] aids = null; + int[]? aids = null; switch (entityType) { case UmbracoEntityTypes.Document: - aids = _backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser + aids = _backofficeSecurityAccessor.BackOfficeSecurity?.CurrentUser? .CalculateContentStartNodeIds(_entityService, _appCaches); break; case UmbracoEntityTypes.Media: aids = - _backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser.CalculateMediaStartNodeIds( + _backofficeSecurityAccessor.BackOfficeSecurity?.CurrentUser?.CalculateMediaStartNodeIds( _entityService, _appCaches); break; } @@ -1002,18 +1015,21 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers { var lids = new List(); var ok = false; - foreach (var i in ids) + if (ids is not null) { - if (ok) + foreach (var i in ids) { - lids.Add(i); - continue; - } + if (ok) + { + lids.Add(i); + continue; + } - if (aids.Contains(i)) - { - lids.Add(i); - ok = true; + if (aids.Contains(i)) + { + lids.Add(i); + ok = true; + } } } @@ -1023,12 +1039,12 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers var culture = queryStrings?.GetValue("culture"); - return ids.Length == 0 + return ids is null || ids.Length == 0 ? Enumerable.Empty() : _entityService.GetAll(objectType.Value, ids) - .WhereNotNull() .OrderBy(x => x.Level) - .Select(MapEntities(culture)); + .Select(MapEntities(culture)) + .WhereNotNull(); } //now we need to convert the unknown ones @@ -1056,13 +1072,13 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers if (objectType.HasValue) { IEnumerable entities = _entityService.GetAll(objectType.Value, keys) - .WhereNotNull() - .Select(MapEntities()); + .Select(MapEntities()) + .WhereNotNull(); // entities are in "some" order, put them back in order var xref = entities.ToDictionary(x => x.Key); IEnumerable result = keys.Select(x => xref.ContainsKey(x) ? xref[x] : null) - .Where(x => x != null); + .WhereNotNull(); return result; } @@ -1092,13 +1108,13 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers if (objectType.HasValue) { IEnumerable entities = _entityService.GetAll(objectType.Value, ids) - .WhereNotNull() - .Select(MapEntities()); + .Select(MapEntities()) + .WhereNotNull(); // entities are in "some" order, put them back in order - var xref = entities.ToDictionary(x => x.Id); + var xref = entities.Where(x => x.Id != null).ToDictionary(x => x.Id!); IEnumerable result = ids.Select(x => xref.ContainsKey(x) ? xref[x] : null) - .Where(x => x != null); + .WhereNotNull(); return result; } @@ -1117,12 +1133,12 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers } } - private ActionResult GetResultForKey(Guid key, UmbracoEntityTypes entityType) + private ActionResult GetResultForKey(Guid key, UmbracoEntityTypes entityType) { UmbracoObjectTypes? objectType = ConvertToObjectType(entityType); if (objectType.HasValue) { - IEntitySlim found = _entityService.Get(key, objectType.Value); + IEntitySlim? found = _entityService.Get(key, objectType.Value); if (found == null) { return NotFound(); @@ -1145,7 +1161,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers case UmbracoEntityTypes.Macro: case UmbracoEntityTypes.Template: - ITemplate template = _fileService.GetTemplate(key); + ITemplate? template = _fileService.GetTemplate(key); if (template is null) { return NotFound(); @@ -1159,12 +1175,12 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers } } - private ActionResult GetResultForId(int id, UmbracoEntityTypes entityType) + private ActionResult GetResultForId(int id, UmbracoEntityTypes entityType) { UmbracoObjectTypes? objectType = ConvertToObjectType(entityType); if (objectType.HasValue) { - IEntitySlim found = _entityService.Get(id, objectType.Value); + IEntitySlim? found = _entityService.Get(id, objectType.Value); if (found == null) { return NotFound(); @@ -1187,7 +1203,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers case UmbracoEntityTypes.Macro: case UmbracoEntityTypes.Template: - ITemplate template = _fileService.GetTemplate(id); + ITemplate? template = _fileService.GetTemplate(id); if (template is null) { return NotFound(); @@ -1235,7 +1251,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers /// ignored. /// /// - public IEnumerable GetAll(UmbracoEntityTypes type, string postFilter) => + public IEnumerable? GetAll(UmbracoEntityTypes type, string postFilter) => GetResultForAll(type, postFilter); /// @@ -1244,14 +1260,14 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers /// /// A string where filter that will filter the results dynamically with linq - optional /// - private IEnumerable GetResultForAll(UmbracoEntityTypes entityType, string postFilter = null) + private IEnumerable? GetResultForAll(UmbracoEntityTypes entityType, string? postFilter = null) { UmbracoObjectTypes? objectType = ConvertToObjectType(entityType); if (objectType.HasValue) { // TODO: Should we order this by something ? IEnumerable entities = - _entityService.GetAll(objectType.Value).WhereNotNull().Select(MapEntities()); + _entityService.GetAll(objectType.Value).Select(MapEntities()).WhereNotNull(); return ExecutePostFilter(entities, postFilter); } @@ -1259,15 +1275,15 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers switch (entityType) { case UmbracoEntityTypes.Template: - IEnumerable templates = _fileService.GetTemplates(); - IEnumerable filteredTemplates = ExecutePostFilter(templates, postFilter); - return filteredTemplates.Select(MapEntities()); + IEnumerable? templates = _fileService.GetTemplates(); + IEnumerable? filteredTemplates = ExecutePostFilter(templates, postFilter); + return filteredTemplates?.Select(MapEntities()).WhereNotNull(); case UmbracoEntityTypes.Macro: //Get all macros from the macro service IOrderedEnumerable macros = _macroService.GetAll().WhereNotNull().OrderBy(x => x.Name); - IEnumerable filteredMacros = ExecutePostFilter(macros, postFilter); - return filteredMacros.Select(MapEntities()); + IEnumerable? filteredMacros = ExecutePostFilter(macros, postFilter); + return filteredMacros?.Select(MapEntities()).WhereNotNull(); case UmbracoEntityTypes.PropertyType: @@ -1278,8 +1294,8 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers .ToArray() .SelectMany(x => x.PropertyTypes) .DistinctBy(composition => composition.Alias); - IEnumerable filteredPropertyTypes = ExecutePostFilter(propertyTypes, postFilter); - return _umbracoMapper.MapEnumerable(filteredPropertyTypes); + IEnumerable? filteredPropertyTypes = ExecutePostFilter(propertyTypes, postFilter); + return _umbracoMapper.MapEnumerable(filteredPropertyTypes ?? Enumerable.Empty()).WhereNotNull(); case UmbracoEntityTypes.PropertyGroup: @@ -1290,14 +1306,14 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers .ToArray() .SelectMany(x => x.PropertyGroups) .DistinctBy(composition => composition.Name); - IEnumerable filteredpropertyGroups = ExecutePostFilter(propertyGroups, postFilter); - return _umbracoMapper.MapEnumerable(filteredpropertyGroups); + IEnumerable? filteredpropertyGroups = ExecutePostFilter(propertyGroups, postFilter); + return _umbracoMapper.MapEnumerable(filteredpropertyGroups ?? Enumerable.Empty()).WhereNotNull(); case UmbracoEntityTypes.User: IEnumerable users = _userService.GetAll(0, int.MaxValue, out _); - IEnumerable filteredUsers = ExecutePostFilter(users, postFilter); - return _umbracoMapper.MapEnumerable(filteredUsers); + IEnumerable? filteredUsers = ExecutePostFilter(users, postFilter); + return _umbracoMapper.MapEnumerable(filteredUsers ?? Enumerable.Empty()).WhereNotNull(); case UmbracoEntityTypes.Stylesheet: @@ -1306,7 +1322,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers throw new NotSupportedException("Filtering on stylesheets is not currently supported"); } - return _fileService.GetStylesheets().Select(MapEntities()); + return _fileService.GetStylesheets().Select(MapEntities()).WhereNotNull(); case UmbracoEntityTypes.Script: @@ -1315,7 +1331,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers throw new NotSupportedException("Filtering on scripts is not currently supported"); } - return _fileService.GetScripts().Select(MapEntities()); + return _fileService.GetScripts().Select(MapEntities()).WhereNotNull(); case UmbracoEntityTypes.PartialView: @@ -1324,7 +1340,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers throw new NotSupportedException("Filtering on partial views is not currently supported"); } - return _fileService.GetPartialViews().Select(MapEntities()); + return _fileService.GetPartialViews().Select(MapEntities()).WhereNotNull(); case UmbracoEntityTypes.Language: @@ -1333,7 +1349,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers throw new NotSupportedException("Filtering on languages is not currently supported"); } - return _localizationService.GetAllLanguages().Select(MapEntities()); + return _localizationService.GetAllLanguages().Select(MapEntities()).WhereNotNull(); case UmbracoEntityTypes.DictionaryItem: if (!postFilter.IsNullOrWhiteSpace()) @@ -1349,31 +1365,31 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers } } - private IEnumerable ExecutePostFilter(IEnumerable entities, string postFilter) + private IEnumerable? ExecutePostFilter(IEnumerable? entities, string? postFilter) { if (postFilter.IsNullOrWhiteSpace()) { return entities; } - var postFilterConditions = postFilter.Split(Constants.CharArrays.Ampersand); + var postFilterConditions = postFilter!.Split(Constants.CharArrays.Ampersand); foreach (var postFilterCondition in postFilterConditions) { - QueryCondition queryCondition = BuildQueryCondition(postFilterCondition); + QueryCondition? queryCondition = BuildQueryCondition(postFilterCondition); if (queryCondition != null) { Expression> whereClauseExpression = queryCondition.BuildCondition("x"); - entities = entities.Where(whereClauseExpression.Compile()); + entities = entities?.Where(whereClauseExpression.Compile()); } } return entities; } - private static QueryCondition BuildQueryCondition(string postFilter) + private static QueryCondition? BuildQueryCondition(string postFilter) { var postFilterParts = postFilter.Split(_postFilterSplitStrings, 2, StringSplitOptions.RemoveEmptyEntries); @@ -1399,7 +1415,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers } Type type = typeof(T); - PropertyInfo property = type.GetProperty(propertyName, BindingFlags.Public | BindingFlags.Instance); + PropertyInfo? property = type.GetProperty(propertyName, BindingFlags.Public | BindingFlags.Instance); if (property == null) { @@ -1419,19 +1435,19 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers return queryCondition; } - private Func MapEntities(string culture = null) + private Func MapEntities(string? culture = null) { culture = culture ?? ClientCulture(); return x => MapEntity(x, culture); } - private EntityBasic MapEntity(object entity, string? culture = null) + private EntityBasic? MapEntity(object entity, string? culture = null) { culture = culture ?? ClientCulture(); return _umbracoMapper.Map(entity, context => { context.SetCulture(culture); }); } - private string ClientCulture() => Request.ClientCulture(); + private string? ClientCulture() => Request.ClientCulture(); #region GetById @@ -1442,7 +1458,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers /// /// /// - public ActionResult GetById(int id, UmbracoEntityTypes type) => GetResultForId(id, type); + public ActionResult GetById(int id, UmbracoEntityTypes type) => GetResultForId(id, type); /// /// Gets an entity by it's key @@ -1450,7 +1466,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers /// /// /// - public ActionResult GetById(Guid id, UmbracoEntityTypes type) => GetResultForKey(id, type); + public ActionResult GetById(Guid id, UmbracoEntityTypes type) => GetResultForKey(id, type); /// /// Gets an entity by it's UDI @@ -1458,7 +1474,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers /// /// /// - public ActionResult GetById(Udi id, UmbracoEntityTypes type) + public ActionResult GetById(Udi id, UmbracoEntityTypes type) { var guidUdi = id as GuidUdi; if (guidUdi != null) @@ -1564,12 +1580,20 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers { var list = new List(); - foreach (IDictionaryItem dictionaryItem in _localizationService.GetRootDictionaryItems() - .OrderBy(DictionaryItemSort())) + var rootDictionaryItems = _localizationService.GetRootDictionaryItems(); + if (rootDictionaryItems is not null) { - EntityBasic item = _umbracoMapper.Map(dictionaryItem); - list.Add(item); - GetChildItemsForList(dictionaryItem, list); + foreach (IDictionaryItem dictionaryItem in rootDictionaryItems + .OrderBy(DictionaryItemSort())) + { + EntityBasic? item = _umbracoMapper.Map(dictionaryItem); + if (item is not null) + { + list.Add(item); + } + + GetChildItemsForList(dictionaryItem, list); + } } return list; @@ -1579,14 +1603,23 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers private void GetChildItemsForList(IDictionaryItem dictionaryItem, ICollection list) { - foreach (IDictionaryItem childItem in _localizationService.GetDictionaryItemChildren(dictionaryItem.Key) - .OrderBy(DictionaryItemSort())) - { - EntityBasic item = _umbracoMapper.Map(childItem); - list.Add(item); + var itemChildren = _localizationService.GetDictionaryItemChildren(dictionaryItem.Key); - GetChildItemsForList(childItem, list); + if (itemChildren is not null) + { + foreach (IDictionaryItem childItem in itemChildren + .OrderBy(DictionaryItemSort())) + { + EntityBasic? item = _umbracoMapper.Map(childItem); + if (item is not null) + { + list.Add(item); + } + + GetChildItemsForList(childItem, list); + } } + } #endregion diff --git a/src/Umbraco.Web.BackOffice/Controllers/TemplateQueryController.cs b/src/Umbraco.Web.BackOffice/Controllers/TemplateQueryController.cs index 86a65ac977..e7a61da7cf 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/TemplateQueryController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/TemplateQueryController.cs @@ -174,7 +174,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers queryExpression.AppendFormat(model.Sort.Direction == "ascending" ? ".OrderBy(x => x.{0})" : ".OrderByDescending(x => x.{0})" - , model.Sort.Property.Alias); + , model.Sort?.Property?.Alias); } // take diff --git a/src/Umbraco.Web.BackOffice/Extensions/LinkGeneratorExtensions.cs b/src/Umbraco.Web.BackOffice/Extensions/LinkGeneratorExtensions.cs index 23c44e2be4..1644ec6f21 100644 --- a/src/Umbraco.Web.BackOffice/Extensions/LinkGeneratorExtensions.cs +++ b/src/Umbraco.Web.BackOffice/Extensions/LinkGeneratorExtensions.cs @@ -8,17 +8,17 @@ namespace Umbraco.Extensions /// /// Returns the URL for the installer /// - public static string GetInstallerUrl(this LinkGenerator linkGenerator) + public static string? GetInstallerUrl(this LinkGenerator linkGenerator) => linkGenerator.GetPathByAction(nameof(InstallController.Index), ControllerExtensions.GetControllerName(), new { area = Cms.Core.Constants.Web.Mvc.InstallArea }); /// /// Returns the URL for the installer api /// - public static string GetInstallerApiUrl(this LinkGenerator linkGenerator) + public static string? GetInstallerApiUrl(this LinkGenerator linkGenerator) => linkGenerator.GetPathByAction( nameof(InstallApiController.GetSetup), ControllerExtensions.GetControllerName(), - new { area = Cms.Core.Constants.Web.Mvc.InstallArea }).TrimEnd(nameof(InstallApiController.GetSetup)); + new { area = Cms.Core.Constants.Web.Mvc.InstallArea })?.TrimEnd(nameof(InstallApiController.GetSetup)); } diff --git a/src/Umbraco.Web.BackOffice/Extensions/ModelStateExtensions.cs b/src/Umbraco.Web.BackOffice/Extensions/ModelStateExtensions.cs index 7729c60b01..514af6a41b 100644 --- a/src/Umbraco.Web.BackOffice/Extensions/ModelStateExtensions.cs +++ b/src/Umbraco.Web.BackOffice/Extensions/ModelStateExtensions.cs @@ -53,7 +53,7 @@ namespace Umbraco.Extensions /// /// internal static void AddVariantValidationError(this ModelStateDictionary modelState, - string culture, string segment, string errMsg) + string? culture, string? segment, string errMsg) { var key = "_content_variant_" + (culture.IsNullOrWhiteSpace() ? "invariant" : culture) + "_" + (segment.IsNullOrWhiteSpace() ? "null" : segment) + "_"; if (modelState.ContainsKey(key)) @@ -73,8 +73,8 @@ namespace Umbraco.Extensions /// /// A list of cultures that have property validation errors. The default culture will be returned for any invariant property errors. /// - internal static IReadOnlyList<(string culture, string? segment)>? GetVariantsWithPropertyErrors(this ModelStateDictionary modelState, - string cultureForInvariantErrors) + internal static IReadOnlyList<(string? culture, string? segment)>? GetVariantsWithPropertyErrors(this ModelStateDictionary modelState, + string? cultureForInvariantErrors) { //Add any variant specific errors here var variantErrors = modelState.Keys @@ -106,12 +106,12 @@ namespace Umbraco.Extensions /// /// A list of cultures that have validation errors. The default culture will be returned for any invariant errors. /// - internal static IReadOnlyList<(string culture, string? segment)>? GetVariantsWithErrors(this ModelStateDictionary modelState, string cultureForInvariantErrors) + internal static IReadOnlyList<(string? culture, string? segment)>? GetVariantsWithErrors(this ModelStateDictionary modelState, string? cultureForInvariantErrors) { - IReadOnlyList<(string culture, string? segment)>? propertyVariantErrors = modelState.GetVariantsWithPropertyErrors(cultureForInvariantErrors); + IReadOnlyList<(string? culture, string? segment)>? propertyVariantErrors = modelState.GetVariantsWithPropertyErrors(cultureForInvariantErrors); //now check the other special variant errors that are - IEnumerable<(string culture, string? segment)>? genericVariantErrors = modelState.Keys + IEnumerable<(string? culture, string? segment)>? genericVariantErrors = modelState.Keys .Where(x => x.StartsWith("_content_variant_") && x.EndsWith("_")) .Select(x => x.TrimStart("_content_variant_").TrimEnd("_")) .Select(x => diff --git a/src/Umbraco.Web.BackOffice/Filters/CheckIfUserTicketDataIsStaleAttribute.cs b/src/Umbraco.Web.BackOffice/Filters/CheckIfUserTicketDataIsStaleAttribute.cs index 0c065c000f..6b25370a8f 100644 --- a/src/Umbraco.Web.BackOffice/Filters/CheckIfUserTicketDataIsStaleAttribute.cs +++ b/src/Umbraco.Web.BackOffice/Filters/CheckIfUserTicketDataIsStaleAttribute.cs @@ -166,7 +166,10 @@ namespace Umbraco.Cms.Web.BackOffice.Filters private async Task ReSync(IUser user, ActionExecutingContext actionContext) { BackOfficeIdentityUser? backOfficeIdentityUser = _umbracoMapper.Map(user); - await _backOfficeSignInManager.SignInAsync(backOfficeIdentityUser, isPersistent: true); + if (backOfficeIdentityUser is not null) + { + await _backOfficeSignInManager.SignInAsync(backOfficeIdentityUser, isPersistent: true); + } // flag that we've made changes _requestCache.Set(nameof(CheckIfUserTicketDataIsStaleFilter), true); diff --git a/src/Umbraco.Web.BackOffice/Mapping/ContentMapDefinition.cs b/src/Umbraco.Web.BackOffice/Mapping/ContentMapDefinition.cs index 6d954ad5af..91fda179e2 100644 --- a/src/Umbraco.Web.BackOffice/Mapping/ContentMapDefinition.cs +++ b/src/Umbraco.Web.BackOffice/Mapping/ContentMapDefinition.cs @@ -332,7 +332,7 @@ namespace Umbraco.Cms.Web.BackOffice.Mapping { // return false if this is the user's actual start node, the node will be rendered in the tree // regardless of if it's a list view or not - if (userStartNodes.Contains(source.Id)) + if (userStartNodes?.Contains(source.Id) ?? false) return false; } } diff --git a/src/Umbraco.Web.BackOffice/PropertyEditors/Validation/ValidationResultConverter.cs b/src/Umbraco.Web.BackOffice/PropertyEditors/Validation/ValidationResultConverter.cs index 38d62748a6..cb88f4537d 100644 --- a/src/Umbraco.Web.BackOffice/PropertyEditors/Validation/ValidationResultConverter.cs +++ b/src/Umbraco.Web.BackOffice/PropertyEditors/Validation/ValidationResultConverter.cs @@ -141,7 +141,7 @@ namespace Umbraco.Cms.Web.BackOffice.PropertyEditors.Validation var jo = new JObject(); if (!validationResult?.ErrorMessage.IsNullOrWhiteSpace() ?? false) { - var errObj = JToken.FromObject(validationResult.ErrorMessage!, camelCaseSerializer); + var errObj = JToken.FromObject(validationResult!.ErrorMessage!, camelCaseSerializer); jo.Add("errorMessage", errObj); } if (validationResult?.MemberNames.Any() ?? false) diff --git a/src/Umbraco.Web.Common/Extensions/ViewDataExtensions.cs b/src/Umbraco.Web.Common/Extensions/ViewDataExtensions.cs index dc4b8a4320..39ee98f09e 100644 --- a/src/Umbraco.Web.Common/Extensions/ViewDataExtensions.cs +++ b/src/Umbraco.Web.Common/Extensions/ViewDataExtensions.cs @@ -86,7 +86,7 @@ namespace Umbraco.Extensions return (string?)viewData[TokenInstallApiBaseUrl]; } - public static void SetInstallApiBaseUrl(this ViewDataDictionary viewData, string value) + public static void SetInstallApiBaseUrl(this ViewDataDictionary viewData, string? value) { viewData[TokenInstallApiBaseUrl] = value; } diff --git a/src/Umbraco.Web.Common/Security/IBackOfficeSignInManager.cs b/src/Umbraco.Web.Common/Security/IBackOfficeSignInManager.cs index 5e6922cb66..fcbadd191c 100644 --- a/src/Umbraco.Web.Common/Security/IBackOfficeSignInManager.cs +++ b/src/Umbraco.Web.Common/Security/IBackOfficeSignInManager.cs @@ -19,7 +19,7 @@ namespace Umbraco.Cms.Web.BackOffice.Security Task GetTwoFactorAuthenticationUserAsync(); Task PasswordSignInAsync(string userName, string password, bool isPersistent, bool lockoutOnFailure); Task SignOutAsync(); - Task SignInAsync(BackOfficeIdentityUser? user, bool isPersistent, string? authenticationMethod = null); + Task SignInAsync(BackOfficeIdentityUser user, bool isPersistent, string? authenticationMethod = null); Task CreateUserPrincipalAsync(BackOfficeIdentityUser user); Task TwoFactorSignInAsync(string? provider, string? code, bool isPersistent, bool rememberClient); Task UpdateExternalAuthenticationTokensAsync(ExternalLoginInfo externalLogin); diff --git a/tests/Umbraco.Tests.Benchmarks/CombineGuidBenchmarks.cs b/tests/Umbraco.Tests.Benchmarks/CombineGuidBenchmarks.cs index 6f7cea07ca..1a13545df8 100644 --- a/tests/Umbraco.Tests.Benchmarks/CombineGuidBenchmarks.cs +++ b/tests/Umbraco.Tests.Benchmarks/CombineGuidBenchmarks.cs @@ -13,7 +13,7 @@ namespace Umbraco.Tests.Benchmarks private static readonly Guid _b = Guid.NewGuid(); [Benchmark] - public byte[] CombineUtils() => GuidUtils.Combine(_a, _b).ToByteArray(); + public byte[] CombineUtils() => GuidUtils.Combine(_a, _b)?.ToByteArray(); [Benchmark] public byte[] CombineLoop() => Combine(_a, _b); diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/GuidUtilsTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/GuidUtilsTests.cs index da9865e72e..9b4bfd966b 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/GuidUtilsTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/GuidUtilsTests.cs @@ -15,7 +15,7 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core var a = Guid.NewGuid(); var b = Guid.NewGuid(); - Assert.AreEqual(GuidUtils.Combine(a, b).ToByteArray(), Combine(a, b)); + Assert.AreEqual(GuidUtils.Combine(a, b)?.ToByteArray(), Combine(a, b)); } [Test]