diff --git a/.github/ISSUE_TEMPLATE/01_bug_report.yml b/.github/ISSUE_TEMPLATE/01_bug_report.yml index 800ac53e68..a60d373b9c 100644 --- a/.github/ISSUE_TEMPLATE/01_bug_report.yml +++ b/.github/ISSUE_TEMPLATE/01_bug_report.yml @@ -6,7 +6,7 @@ body: - type: input id: "version" attributes: - label: "Which *exact* Umbraco version are you using? For example: 8.13.1 - don't just write v8" + label: "Which *exact* Umbraco version are you using? For example: 9.0.1 - don't just write v9" description: "Use the help icon in the Umbraco backoffice to find the version you're using" validations: required: true diff --git a/.gitignore b/.gitignore index 9cea2d4e10..c69474ac30 100644 --- a/.gitignore +++ b/.gitignore @@ -84,6 +84,7 @@ src/Umbraco.Web.UI/wwwroot/[Uu]mbraco/js/* src/Umbraco.Web.UI/wwwroot/[Uu]mbraco/lib/* src/Umbraco.Web.UI/wwwroot/[Uu]mbraco/views/* src/Umbraco.Web.UI/wwwroot/Media/* +src/Umbraco.Web.UI/Smidge/ # Tests cypress.env.json diff --git a/build/NuSpecs/buildTransitive/Umbraco.Cms.StaticAssets.targets b/build/NuSpecs/buildTransitive/Umbraco.Cms.StaticAssets.targets index 70081e0677..7f73c14650 100644 --- a/build/NuSpecs/buildTransitive/Umbraco.Cms.StaticAssets.targets +++ b/build/NuSpecs/buildTransitive/Umbraco.Cms.StaticAssets.targets @@ -90,4 +90,10 @@ + + + + + + diff --git a/src/JsonSchema/AppSettings.cs b/src/JsonSchema/AppSettings.cs index 4af6685d8a..8db2bbb8c7 100644 --- a/src/JsonSchema/AppSettings.cs +++ b/src/JsonSchema/AppSettings.cs @@ -82,6 +82,7 @@ namespace JsonSchema public BasicAuthSettings BasicAuth { get; set; } public PackageMigrationSettings PackageMigration { get; set; } + public LegacyPasswordMigrationSettings LegacyPasswordMigration { get; set; } } /// diff --git a/src/Umbraco.Core/Actions/ActionNotify.cs b/src/Umbraco.Core/Actions/ActionNotify.cs new file mode 100644 index 0000000000..a13b605277 --- /dev/null +++ b/src/Umbraco.Core/Actions/ActionNotify.cs @@ -0,0 +1,17 @@ +namespace Umbraco.Cms.Core.Actions +{ + public class ActionNotify : IAction + { + public char Letter => 'N'; + + public bool ShowInNotifier => false; + + public bool CanBePermissionAssigned => true; + + public string Icon => "megaphone"; + + public string Alias => "notify"; + + public string Category => Constants.Conventions.PermissionCategories.ContentCategory; + } +} diff --git a/src/Umbraco.Core/Configuration/EntryAssemblyMetadata.cs b/src/Umbraco.Core/Configuration/EntryAssemblyMetadata.cs new file mode 100644 index 0000000000..b6b9f067b9 --- /dev/null +++ b/src/Umbraco.Core/Configuration/EntryAssemblyMetadata.cs @@ -0,0 +1,24 @@ +using System.Reflection; + +namespace Umbraco.Cms.Core.Configuration +{ + internal class EntryAssemblyMetadata : IEntryAssemblyMetadata + { + public EntryAssemblyMetadata() + { + var entryAssembly = Assembly.GetEntryAssembly(); + + Name = entryAssembly + ?.GetName() + ?.Name ?? string.Empty; + + InformationalVersion = entryAssembly + ?.GetCustomAttribute() + ?.InformationalVersion ?? string.Empty; + } + + public string Name { get; } + + public string InformationalVersion { get; } + } +} diff --git a/src/Umbraco.Core/Configuration/IEntryAssemblyMetadata.cs b/src/Umbraco.Core/Configuration/IEntryAssemblyMetadata.cs new file mode 100644 index 0000000000..09ea5058df --- /dev/null +++ b/src/Umbraco.Core/Configuration/IEntryAssemblyMetadata.cs @@ -0,0 +1,18 @@ +namespace Umbraco.Cms.Core.Configuration +{ + /// + /// Provides metadata about the entry assembly. + /// + public interface IEntryAssemblyMetadata + { + /// + /// Gets the Name of entry assembly. + /// + public string Name { get; } + + /// + /// Gets the InformationalVersion string for entry assembly. + /// + public string InformationalVersion { get; } + } +} diff --git a/src/Umbraco.Core/Configuration/Models/GlobalSettings.cs b/src/Umbraco.Core/Configuration/Models/GlobalSettings.cs index c880830274..97fb91b0ec 100644 --- a/src/Umbraco.Core/Configuration/Models/GlobalSettings.cs +++ b/src/Umbraco.Core/Configuration/Models/GlobalSettings.cs @@ -130,6 +130,7 @@ namespace Umbraco.Cms.Core.Configuration.Models /// Gets or sets a value for the main dom lock. /// public string MainDomLock { get; set; } = string.Empty; + public string Id { get; set; } = string.Empty; /// @@ -159,6 +160,10 @@ namespace Umbraco.Cms.Core.Configuration.Models public bool IsSmtpServerConfigured => !string.IsNullOrWhiteSpace(Smtp?.Host); /// + /// Gets a value indicating whether there is a physical pickup directory configured. + /// + public bool IsPickupDirectoryLocationConfigured => !string.IsNullOrWhiteSpace(Smtp?.PickupDirectoryLocation); + /// Gets a value indicating whether TinyMCE scripting sanitization should be applied /// [DefaultValue(StaticSanitizeTinyMce)] @@ -174,4 +179,4 @@ namespace Umbraco.Cms.Core.Configuration.Models [DefaultValue(StaticSqlWriteLockTimeOut)] public TimeSpan SqlWriteLockTimeOut { get; } = TimeSpan.Parse(StaticSqlWriteLockTimeOut); } -} +} \ No newline at end of file diff --git a/src/Umbraco.Core/Configuration/Models/LegacyPasswordMigrationSettings.cs b/src/Umbraco.Core/Configuration/Models/LegacyPasswordMigrationSettings.cs new file mode 100644 index 0000000000..c3909ed619 --- /dev/null +++ b/src/Umbraco.Core/Configuration/Models/LegacyPasswordMigrationSettings.cs @@ -0,0 +1,30 @@ +// Copyright (c) Umbraco. +// See LICENSE for more details. + +using System.ComponentModel; + +namespace Umbraco.Cms.Core.Configuration.Models +{ + /// + /// Typed configuration options for legacy machine key settings used for migration of members from a v8 solution. + /// + [UmbracoOptions(Constants.Configuration.ConfigLegacyPasswordMigration)] + public class LegacyPasswordMigrationSettings + { + private const string StaticDecryptionKey = ""; + + /// + /// Gets the decryption algorithm. + /// + /// + /// Currently only AES is supported. This should include all machine keys generated by Umbraco. + /// + public string MachineKeyDecryption => "AES"; + + /// + /// Gets or sets the decryption hex-formatted string key found in legacy web.config machineKey configuration-element. + /// + [DefaultValue(StaticDecryptionKey)] + public string MachineKeyDecryptionKey { get; set; } = StaticDecryptionKey; + } +} diff --git a/src/Umbraco.Core/Constants-Configuration.cs b/src/Umbraco.Core/Constants-Configuration.cs index c36f5813ab..063d733821 100644 --- a/src/Umbraco.Core/Constants-Configuration.cs +++ b/src/Umbraco.Core/Constants-Configuration.cs @@ -26,6 +26,7 @@ public const string ConfigHostingDebug = ConfigHostingPrefix + "Debug"; public const string ConfigCustomErrorsMode = ConfigCustomErrorsPrefix + "Mode"; public const string ConfigActiveDirectory = ConfigPrefix + "ActiveDirectory"; + public const string ConfigLegacyPasswordMigration = ConfigPrefix + "LegacyPasswordMigration"; public const string ConfigContent = ConfigPrefix + "Content"; public const string ConfigCoreDebug = ConfigCorePrefix + "Debug"; public const string ConfigExceptionFilter = ConfigPrefix + "ExceptionFilter"; diff --git a/src/Umbraco.Core/Constants-Conventions.cs b/src/Umbraco.Core/Constants-Conventions.cs index 35032dff4c..16c63b4c02 100644 --- a/src/Umbraco.Core/Constants-Conventions.cs +++ b/src/Umbraco.Core/Constants-Conventions.cs @@ -235,7 +235,7 @@ namespace Umbraco.Cms.Core /// The standard properties group name for membership properties. /// public const string StandardPropertiesGroupName = "Membership"; - } + } /// /// Defines the alias identifiers for Umbraco member types. diff --git a/src/Umbraco.Core/Constants-HttpContextItemsKeys.cs b/src/Umbraco.Core/Constants-HttpContextItemsKeys.cs new file mode 100644 index 0000000000..7be1fbd140 --- /dev/null +++ b/src/Umbraco.Core/Constants-HttpContextItemsKeys.cs @@ -0,0 +1,19 @@ +namespace Umbraco.Cms.Core +{ + public static partial class Constants + { + public static class HttpContext + { + /// + /// Defines keys for items stored in HttpContext.Items + /// + public static class Items + { + /// + /// Key for current requests body deserialized as JObject. + /// + public const string RequestBodyAsJObject = "RequestBodyAsJObject"; + } + } + } +} diff --git a/src/Umbraco.Core/Constants-Security.cs b/src/Umbraco.Core/Constants-Security.cs index 0506a66ad2..b509c12ff5 100644 --- a/src/Umbraco.Core/Constants-Security.cs +++ b/src/Umbraco.Core/Constants-Security.cs @@ -66,6 +66,7 @@ namespace Umbraco.Cms.Core public const string AspNetCoreV2PasswordHashAlgorithmName = "PBKDF2.ASPNETCORE.V2"; public const string AspNetUmbraco8PasswordHashAlgorithmName = "HMACSHA256"; public const string AspNetUmbraco4PasswordHashAlgorithmName = "HMACSHA1"; + public const string UnknownPasswordConfigJson = "{\"hashAlgorithm\":\"Unknown\"}"; } } } diff --git a/src/Umbraco.Core/ContentApps/MemberEditorContentAppFactory.cs b/src/Umbraco.Core/ContentApps/MemberEditorContentAppFactory.cs new file mode 100644 index 0000000000..3763b06e0b --- /dev/null +++ b/src/Umbraco.Core/ContentApps/MemberEditorContentAppFactory.cs @@ -0,0 +1,34 @@ +using System.Collections.Generic; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.ContentEditing; +using Umbraco.Cms.Core.Models.Membership; + +namespace Umbraco.Cms.Core.ContentApps +{ + internal class MemberEditorContentAppFactory : IContentAppFactory + { + // see note on ContentApp + internal const int Weight = +50; + + private ContentApp _memberApp; + + public ContentApp GetContentAppFor(object source, IEnumerable userGroups) + { + switch (source) + { + case IMember _: + return _memberApp ??= new ContentApp + { + Alias = "umbMembership", + Name = "Member", + Icon = "icon-user", + View = "views/member/apps/membership/membership.html", + Weight = Weight + }; + + default: + return null; + } + } + } +} diff --git a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.CollectionBuilders.cs b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.CollectionBuilders.cs new file mode 100644 index 0000000000..1c5dddbc20 --- /dev/null +++ b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.CollectionBuilders.cs @@ -0,0 +1,116 @@ +using System; +using Umbraco.Cms.Core.Composing; +using Umbraco.Cms.Core.Dashboards; +using Umbraco.Cms.Core.Media; +using Umbraco.Cms.Core.Models.ContentEditing; +using Umbraco.Cms.Core.Routing; +using Umbraco.Cms.Core.Sections; + +namespace Umbraco.Cms.Core.DependencyInjection +{ + /// + /// Contains extensions methods for used for registering content apps. + /// + public static partial class UmbracoBuilderExtensions + { + /// + /// Register a component. + /// + /// The type of the component. + /// The builder. + public static IUmbracoBuilder AddComponent(this IUmbracoBuilder builder) + where T : class, IComponent + { + builder.Components().Append(); + return builder; + } + + /// + /// Register a content app. + /// + /// The type of the content app. + /// The builder. + public static IUmbracoBuilder AddContentApp(this IUmbracoBuilder builder) + where T : class, IContentAppFactory + { + builder.ContentApps().Append(); + return builder; + } + + /// + /// Register a content finder. + /// + /// The type of the content finder. + /// The builder. + public static IUmbracoBuilder AddContentFinder(this IUmbracoBuilder builder) + where T : class, IContentFinder + { + builder.ContentFinders().Append(); + return builder; + } + + /// + /// Register a dashboard. + /// + /// The type of the dashboard. + /// The builder. + public static IUmbracoBuilder AddDashboard(this IUmbracoBuilder builder) + where T : class, IDashboard + { + builder.Dashboards().Add(); + return builder; + } + + /// + /// Register a media url provider. + /// + /// The type of the media url provider. + /// The builder. + public static IUmbracoBuilder AddMediaUrlProvider(this IUmbracoBuilder builder) + where T : class, IMediaUrlProvider + { + builder.MediaUrlProviders().Append(); + return builder; + } + + /// + /// Register a embed provider. + /// + /// The type of the embed provider. + /// The builder. + public static IUmbracoBuilder AddEmbedProvider(this IUmbracoBuilder builder) + where T : class, IEmbedProvider + { + builder.EmbedProviders().Append(); + return builder; + } + + [Obsolete("Use AddEmbedProvider instead. This will be removed in Umbraco 10")] + public static IUmbracoBuilder AddOEmbedProvider(this IUmbracoBuilder builder) + where T : class, IEmbedProvider => AddEmbedProvider(builder); + + /// + /// Register a section. + /// + /// The type of the section. + /// The builder. + public static IUmbracoBuilder AddSection(this IUmbracoBuilder builder) + where T : class, ISection + { + builder.Sections().Append(); + return builder; + } + + /// + /// Register a url provider. + /// + /// The type of the url provider. + /// The Builder. + public static IUmbracoBuilder AddUrlProvider(this IUmbracoBuilder builder) + where T : class, IUrlProvider + { + builder.UrlProviders().Append(); + return builder; + } + } +} diff --git a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Collections.cs b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Collections.cs index 1d66e874be..811ee35c14 100644 --- a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Collections.cs +++ b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Collections.cs @@ -1,3 +1,4 @@ +using System; using Umbraco.Cms.Core.Actions; using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Composing; @@ -34,7 +35,7 @@ namespace Umbraco.Cms.Core.DependencyInjection { builder.CacheRefreshers().Add(() => builder.TypeLoader.GetCacheRefreshers()); builder.DataEditors().Add(() => builder.TypeLoader.GetDataEditors()); - builder.Actions().Add(() => builder.TypeLoader.GetActions()); + builder.Actions().Add(() => builder .TypeLoader.GetActions()); // register known content apps builder.ContentApps() @@ -44,7 +45,8 @@ namespace Umbraco.Cms.Core.DependencyInjection .Append() .Append() .Append() - .Append(); + .Append() + .Append(); // all built-in finders in the correct order, // devs can then modify this list on application startup @@ -102,7 +104,7 @@ namespace Umbraco.Cms.Core.DependencyInjection builder.ManifestFilters(); builder.MediaUrlGenerators(); // register OEmbed providers - no type scanning - all explicit opt-in of adding types, IEmbedProvider is not IDiscoverable - builder.OEmbedProviders() + builder.EmbedProviders() .Append() .Append() .Append() @@ -265,7 +267,15 @@ namespace Umbraco.Cms.Core.DependencyInjection /// Gets the backoffice OEmbed Providers collection builder. /// /// The builder. + [Obsolete("Use EmbedProviders() instead")] public static EmbedProvidersCollectionBuilder OEmbedProviders(this IUmbracoBuilder builder) + => EmbedProviders(builder); + + /// + /// Gets the backoffice Embed Providers collection builder. + /// + /// The builder. + public static EmbedProvidersCollectionBuilder EmbedProviders(this IUmbracoBuilder builder) => builder.WithCollectionBuilder(); /// diff --git a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Configuration.cs b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Configuration.cs index 9b31ed7056..6ef87464e8 100644 --- a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Configuration.cs +++ b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Configuration.cs @@ -73,6 +73,7 @@ namespace Umbraco.Cms.Core.DependencyInjection .AddUmbracoOptions() .AddUmbracoOptions() .AddUmbracoOptions() + .AddUmbracoOptions() .AddUmbracoOptions(); return builder; diff --git a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs index 5aa62eae19..1af25e16e8 100644 --- a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs +++ b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs @@ -36,6 +36,7 @@ using Umbraco.Cms.Core.Runtime; using Umbraco.Cms.Core.Security; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Sync; +using Umbraco.Cms.Core.Telemetry; using Umbraco.Cms.Core.Templates; using Umbraco.Cms.Core.Web; using Umbraco.Cms.Web.Common.DependencyInjection; @@ -163,6 +164,7 @@ namespace Umbraco.Cms.Core.DependencyInjection Services.AddUnique(factory => factory.GetRequiredService().RequestCache); Services.AddUnique(); Services.AddUnique(); + Services.AddUnique(); this.AddAllCoreCollectionBuilders(); this.AddNotificationHandler(); @@ -258,6 +260,9 @@ namespace Umbraco.Cms.Core.DependencyInjection // Register ValueEditorCache used for validation Services.AddSingleton(); + + // Register telemetry service used to gather data about installed packages + Services.AddUnique(); } } } diff --git a/src/Umbraco.Core/Manifest/PackageManifest.cs b/src/Umbraco.Core/Manifest/PackageManifest.cs index 753ec0613a..ae6ffdc8ba 100644 --- a/src/Umbraco.Core/Manifest/PackageManifest.cs +++ b/src/Umbraco.Core/Manifest/PackageManifest.cs @@ -48,6 +48,19 @@ namespace Umbraco.Cms.Core.Manifest /// [IgnoreDataMember] public string Source { get; set; } + + /// + /// Gets or sets the version of the package + /// + [DataMember(Name = "version")] + public string Version { get; set; } = string.Empty; + + /// + /// Gets or sets a value indicating whether telemetry is allowed + /// + [DataMember(Name = "allowPackageTelemetry")] + public bool AllowPackageTelemetry { get; set; } = true; + [DataMember(Name = "bundleOptions")] public BundleOptions BundleOptions { get; set; } diff --git a/src/Umbraco.Core/Media/EmbedProviders/DailyMotion.cs b/src/Umbraco.Core/Media/EmbedProviders/DailyMotion.cs index e5fc553ea8..b79e1a8de2 100644 --- a/src/Umbraco.Core/Media/EmbedProviders/DailyMotion.cs +++ b/src/Umbraco.Core/Media/EmbedProviders/DailyMotion.cs @@ -1,8 +1,9 @@ -using System.Collections.Generic; +using System.Collections.Generic; using Umbraco.Cms.Core.Serialization; namespace Umbraco.Cms.Core.Media.EmbedProviders { + // TODO (V10): change base class to OEmbedProviderBase public class DailyMotion : EmbedProviderBase { public override string ApiEndpoint => "https://www.dailymotion.com/services/oembed"; diff --git a/src/Umbraco.Core/Media/EmbedProviders/EmbedProviderBase.cs b/src/Umbraco.Core/Media/EmbedProviders/EmbedProviderBase.cs index bbc690ce97..6d745d3d49 100644 --- a/src/Umbraco.Core/Media/EmbedProviders/EmbedProviderBase.cs +++ b/src/Umbraco.Core/Media/EmbedProviders/EmbedProviderBase.cs @@ -1,85 +1,14 @@ -using System; -using System.Collections.Generic; -using System.Net; -using System.Net.Http; -using System.Text; -using System.Xml; +using System; using Umbraco.Cms.Core.Serialization; namespace Umbraco.Cms.Core.Media.EmbedProviders { - public abstract class EmbedProviderBase : IEmbedProvider + [Obsolete("Use OEmbedProviderBase instead")] + public abstract class EmbedProviderBase : OEmbedProviderBase { - private readonly IJsonSerializer _jsonSerializer; - protected EmbedProviderBase(IJsonSerializer jsonSerializer) + : base(jsonSerializer) { - _jsonSerializer = jsonSerializer; - } - - private static HttpClient _httpClient; - - public abstract string ApiEndpoint { get; } - - public abstract string[] UrlSchemeRegex { get; } - - public abstract Dictionary RequestParams { get; } - - public abstract string GetMarkup(string url, int maxWidth = 0, int maxHeight = 0); - - public virtual string GetEmbedProviderUrl(string url, int maxWidth, int maxHeight) - { - if (Uri.IsWellFormedUriString(url, UriKind.RelativeOrAbsolute) == false) - throw new ArgumentException("Not a valid URL.", nameof(url)); - - var fullUrl = new StringBuilder(); - - fullUrl.Append(ApiEndpoint); - fullUrl.Append("?url=" + WebUtility.UrlEncode(url)); - - foreach (var param in RequestParams) - fullUrl.Append($"&{param.Key}={param.Value}"); - - if (maxWidth > 0) - fullUrl.Append("&maxwidth=" + maxWidth); - - if (maxHeight > 0) - fullUrl.Append("&maxheight=" + maxHeight); - - return fullUrl.ToString(); - } - - public virtual string DownloadResponse(string url) - { - if (_httpClient == null) - _httpClient = new HttpClient(); - - using (var request = new HttpRequestMessage(HttpMethod.Get, url)) - { - var response = _httpClient.SendAsync(request).Result; - return response.Content.ReadAsStringAsync().Result; - } - } - - public virtual T GetJsonResponse(string url) where T : class - { - var response = DownloadResponse(url); - return _jsonSerializer.Deserialize(response); - } - - public virtual XmlDocument GetXmlResponse(string url) - { - var response = DownloadResponse(url); - var doc = new XmlDocument(); - doc.LoadXml(response); - - return doc; - } - - public virtual string GetXmlProperty(XmlDocument doc, string property) - { - var selectSingleNode = doc.SelectSingleNode(property); - return selectSingleNode != null ? selectSingleNode.InnerText : string.Empty; } } -}; +} diff --git a/src/Umbraco.Core/Media/EmbedProviders/Flickr.cs b/src/Umbraco.Core/Media/EmbedProviders/Flickr.cs index 48d4be06dc..2ea5fd8109 100644 --- a/src/Umbraco.Core/Media/EmbedProviders/Flickr.cs +++ b/src/Umbraco.Core/Media/EmbedProviders/Flickr.cs @@ -1,9 +1,10 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Net; using Umbraco.Cms.Core.Serialization; namespace Umbraco.Cms.Core.Media.EmbedProviders { + // TODO(V10) : change base class to OEmbedProviderBase public class Flickr : EmbedProviderBase { public override string ApiEndpoint => "http://www.flickr.com/services/oembed/"; diff --git a/src/Umbraco.Core/Media/EmbedProviders/GettyImages.cs b/src/Umbraco.Core/Media/EmbedProviders/GettyImages.cs index 3dbe44a9e2..0071ee935b 100644 --- a/src/Umbraco.Core/Media/EmbedProviders/GettyImages.cs +++ b/src/Umbraco.Core/Media/EmbedProviders/GettyImages.cs @@ -1,8 +1,9 @@ -using System.Collections.Generic; +using System.Collections.Generic; using Umbraco.Cms.Core.Serialization; namespace Umbraco.Cms.Core.Media.EmbedProviders { + // TODO(V10) : change base class to OEmbedProviderBase public class GettyImages : EmbedProviderBase { public override string ApiEndpoint => "http://embed.gettyimages.com/oembed"; diff --git a/src/Umbraco.Core/Media/EmbedProviders/Giphy.cs b/src/Umbraco.Core/Media/EmbedProviders/Giphy.cs index ae39b04123..8d99752525 100644 --- a/src/Umbraco.Core/Media/EmbedProviders/Giphy.cs +++ b/src/Umbraco.Core/Media/EmbedProviders/Giphy.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using Umbraco.Cms.Core.Serialization; namespace Umbraco.Cms.Core.Media.EmbedProviders @@ -6,6 +6,7 @@ namespace Umbraco.Cms.Core.Media.EmbedProviders /// /// Embed Provider for Giphy.com the popular online GIFs and animated sticker provider. /// + /// TODO(V10) : change base class to OEmbedProviderBase public class Giphy : EmbedProviderBase { public override string ApiEndpoint => "https://giphy.com/services/oembed?url="; diff --git a/src/Umbraco.Core/Media/EmbedProviders/Hulu.cs b/src/Umbraco.Core/Media/EmbedProviders/Hulu.cs index 305d69d497..c88a3d5553 100644 --- a/src/Umbraco.Core/Media/EmbedProviders/Hulu.cs +++ b/src/Umbraco.Core/Media/EmbedProviders/Hulu.cs @@ -1,8 +1,9 @@ -using System.Collections.Generic; +using System.Collections.Generic; using Umbraco.Cms.Core.Serialization; namespace Umbraco.Cms.Core.Media.EmbedProviders { + // TODO(V10) : change base class to OEmbedProviderBase public class Hulu : EmbedProviderBase { public override string ApiEndpoint => "http://www.hulu.com/api/oembed.json"; diff --git a/src/Umbraco.Core/Media/EmbedProviders/Issuu.cs b/src/Umbraco.Core/Media/EmbedProviders/Issuu.cs index 50ff03d880..89179d40af 100644 --- a/src/Umbraco.Core/Media/EmbedProviders/Issuu.cs +++ b/src/Umbraco.Core/Media/EmbedProviders/Issuu.cs @@ -1,8 +1,9 @@ -using System.Collections.Generic; +using System.Collections.Generic; using Umbraco.Cms.Core.Serialization; namespace Umbraco.Cms.Core.Media.EmbedProviders { + // TODO(V10) : change base class to OEmbedProviderBase public class Issuu : EmbedProviderBase { public override string ApiEndpoint => "https://issuu.com/oembed"; diff --git a/src/Umbraco.Core/Media/EmbedProviders/Kickstarter.cs b/src/Umbraco.Core/Media/EmbedProviders/Kickstarter.cs index b3527a82a1..1d2c03e897 100644 --- a/src/Umbraco.Core/Media/EmbedProviders/Kickstarter.cs +++ b/src/Umbraco.Core/Media/EmbedProviders/Kickstarter.cs @@ -1,8 +1,9 @@ -using System.Collections.Generic; +using System.Collections.Generic; using Umbraco.Cms.Core.Serialization; namespace Umbraco.Cms.Core.Media.EmbedProviders { + // TODO(V10) : change base class to OEmbedProviderBase public class Kickstarter : EmbedProviderBase { public override string ApiEndpoint => "http://www.kickstarter.com/services/oembed"; diff --git a/src/Umbraco.Core/Media/EmbedProviders/OEmbedProviderBase.cs b/src/Umbraco.Core/Media/EmbedProviders/OEmbedProviderBase.cs new file mode 100644 index 0000000000..e7507c3bf2 --- /dev/null +++ b/src/Umbraco.Core/Media/EmbedProviders/OEmbedProviderBase.cs @@ -0,0 +1,85 @@ +using System; +using System.Collections.Generic; +using System.Net; +using System.Net.Http; +using System.Text; +using System.Xml; +using Umbraco.Cms.Core.Serialization; + +namespace Umbraco.Cms.Core.Media.EmbedProviders +{ + public abstract class OEmbedProviderBase : IEmbedProvider + { + private readonly IJsonSerializer _jsonSerializer; + + protected OEmbedProviderBase(IJsonSerializer jsonSerializer) + { + _jsonSerializer = jsonSerializer; + } + + private static HttpClient _httpClient; + + public abstract string ApiEndpoint { get; } + + public abstract string[] UrlSchemeRegex { get; } + + public abstract Dictionary RequestParams { get; } + + public abstract string GetMarkup(string url, int maxWidth = 0, int maxHeight = 0); + + public virtual string GetEmbedProviderUrl(string url, int maxWidth, int maxHeight) + { + if (Uri.IsWellFormedUriString(url, UriKind.RelativeOrAbsolute) == false) + throw new ArgumentException("Not a valid URL.", nameof(url)); + + var fullUrl = new StringBuilder(); + + fullUrl.Append(ApiEndpoint); + fullUrl.Append("?url=" + WebUtility.UrlEncode(url)); + + foreach (var param in RequestParams) + fullUrl.Append($"&{param.Key}={param.Value}"); + + if (maxWidth > 0) + fullUrl.Append("&maxwidth=" + maxWidth); + + if (maxHeight > 0) + fullUrl.Append("&maxheight=" + maxHeight); + + return fullUrl.ToString(); + } + + public virtual string DownloadResponse(string url) + { + if (_httpClient == null) + _httpClient = new HttpClient(); + + using (var request = new HttpRequestMessage(HttpMethod.Get, url)) + { + var response = _httpClient.SendAsync(request).Result; + return response.Content.ReadAsStringAsync().Result; + } + } + + public virtual T GetJsonResponse(string url) where T : class + { + var response = DownloadResponse(url); + return _jsonSerializer.Deserialize(response); + } + + public virtual XmlDocument GetXmlResponse(string url) + { + var response = DownloadResponse(url); + var doc = new XmlDocument(); + doc.LoadXml(response); + + return doc; + } + + public virtual string GetXmlProperty(XmlDocument doc, string property) + { + var selectSingleNode = doc.SelectSingleNode(property); + return selectSingleNode != null ? selectSingleNode.InnerText : string.Empty; + } + } +}; diff --git a/src/Umbraco.Core/Media/EmbedProviders/OEmbedResponse.cs b/src/Umbraco.Core/Media/EmbedProviders/OEmbedResponse.cs index 33710d49d0..80ae4f7a14 100644 --- a/src/Umbraco.Core/Media/EmbedProviders/OEmbedResponse.cs +++ b/src/Umbraco.Core/Media/EmbedProviders/OEmbedResponse.cs @@ -1,4 +1,4 @@ -using System.Net; +using System.Net; using System.Runtime.Serialization; namespace Umbraco.Cms.Core.Media.EmbedProviders diff --git a/src/Umbraco.Core/Media/EmbedProviders/Slideshare.cs b/src/Umbraco.Core/Media/EmbedProviders/Slideshare.cs index 1517886458..42e500aa5c 100644 --- a/src/Umbraco.Core/Media/EmbedProviders/Slideshare.cs +++ b/src/Umbraco.Core/Media/EmbedProviders/Slideshare.cs @@ -1,8 +1,9 @@ -using System.Collections.Generic; +using System.Collections.Generic; using Umbraco.Cms.Core.Serialization; namespace Umbraco.Cms.Core.Media.EmbedProviders { + // TODO(V10) : change base class to OEmbedProviderBase public class Slideshare : EmbedProviderBase { public override string ApiEndpoint => "http://www.slideshare.net/api/oembed/2"; diff --git a/src/Umbraco.Core/Media/EmbedProviders/SoundCloud.cs b/src/Umbraco.Core/Media/EmbedProviders/SoundCloud.cs index 36426b8625..687da98697 100644 --- a/src/Umbraco.Core/Media/EmbedProviders/SoundCloud.cs +++ b/src/Umbraco.Core/Media/EmbedProviders/SoundCloud.cs @@ -1,8 +1,9 @@ -using System.Collections.Generic; +using System.Collections.Generic; using Umbraco.Cms.Core.Serialization; namespace Umbraco.Cms.Core.Media.EmbedProviders { + // TODO(V10) : change base class to OEmbedProviderBase public class Soundcloud : EmbedProviderBase { public override string ApiEndpoint => "https://soundcloud.com/oembed"; diff --git a/src/Umbraco.Core/Media/EmbedProviders/Ted.cs b/src/Umbraco.Core/Media/EmbedProviders/Ted.cs index a50681adf7..511cbf012d 100644 --- a/src/Umbraco.Core/Media/EmbedProviders/Ted.cs +++ b/src/Umbraco.Core/Media/EmbedProviders/Ted.cs @@ -1,8 +1,9 @@ -using System.Collections.Generic; +using System.Collections.Generic; using Umbraco.Cms.Core.Serialization; namespace Umbraco.Cms.Core.Media.EmbedProviders { + // TODO(V10) : change base class to OEmbedProviderBase public class Ted : EmbedProviderBase { public override string ApiEndpoint => "http://www.ted.com/talks/oembed.xml"; diff --git a/src/Umbraco.Core/Media/EmbedProviders/Twitter.cs b/src/Umbraco.Core/Media/EmbedProviders/Twitter.cs index 1504fb931c..0f90d2d54c 100644 --- a/src/Umbraco.Core/Media/EmbedProviders/Twitter.cs +++ b/src/Umbraco.Core/Media/EmbedProviders/Twitter.cs @@ -1,8 +1,9 @@ -using System.Collections.Generic; +using System.Collections.Generic; using Umbraco.Cms.Core.Serialization; namespace Umbraco.Cms.Core.Media.EmbedProviders { + // TODO(V10) : change base class to OEmbedProviderBase public class Twitter : EmbedProviderBase { public override string ApiEndpoint => "http://publish.twitter.com/oembed"; diff --git a/src/Umbraco.Core/Media/EmbedProviders/Vimeo.cs b/src/Umbraco.Core/Media/EmbedProviders/Vimeo.cs index e745ba50c0..db324bda12 100644 --- a/src/Umbraco.Core/Media/EmbedProviders/Vimeo.cs +++ b/src/Umbraco.Core/Media/EmbedProviders/Vimeo.cs @@ -1,8 +1,9 @@ -using System.Collections.Generic; +using System.Collections.Generic; using Umbraco.Cms.Core.Serialization; namespace Umbraco.Cms.Core.Media.EmbedProviders { + // TODO(V10) : change base class to OEmbedProviderBase public class Vimeo : EmbedProviderBase { public override string ApiEndpoint => "https://vimeo.com/api/oembed.xml"; diff --git a/src/Umbraco.Core/Media/EmbedProviders/Youtube.cs b/src/Umbraco.Core/Media/EmbedProviders/Youtube.cs index 9a8a28bf00..c85896db08 100644 --- a/src/Umbraco.Core/Media/EmbedProviders/Youtube.cs +++ b/src/Umbraco.Core/Media/EmbedProviders/Youtube.cs @@ -1,8 +1,9 @@ -using System.Collections.Generic; +using System.Collections.Generic; using Umbraco.Cms.Core.Serialization; namespace Umbraco.Cms.Core.Media.EmbedProviders { + // TODO(V10) : change base class to OEmbedProviderBase public class YouTube : EmbedProviderBase { public override string ApiEndpoint => "https://www.youtube.com/oembed"; diff --git a/src/Umbraco.Core/Models/ContentEditing/MemberDisplay.cs b/src/Umbraco.Core/Models/ContentEditing/MemberDisplay.cs index c422b226e3..809e47e073 100644 --- a/src/Umbraco.Core/Models/ContentEditing/MemberDisplay.cs +++ b/src/Umbraco.Core/Models/ContentEditing/MemberDisplay.cs @@ -37,5 +37,9 @@ namespace Umbraco.Cms.Core.Models.ContentEditing [DataMember(Name = "apps")] public IEnumerable ContentApps { get; set; } + + + [DataMember(Name = "membershipProperties")] + public IEnumerable MembershipProperties { get; set; } } } diff --git a/src/Umbraco.Core/Models/ContentEditing/UserGroupPermissionsSave.cs b/src/Umbraco.Core/Models/ContentEditing/UserGroupPermissionsSave.cs index 551cbbc0ee..e782d69635 100644 --- a/src/Umbraco.Core/Models/ContentEditing/UserGroupPermissionsSave.cs +++ b/src/Umbraco.Core/Models/ContentEditing/UserGroupPermissionsSave.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Linq; using System.Runtime.Serialization; @@ -28,5 +29,14 @@ namespace Umbraco.Cms.Core.Models.ContentEditing /// [DataMember(Name = "permissions")] public IDictionary> AssignedPermissions { get; set; } + + [Obsolete("This is not used and will be removed in Umbraco 10")] + public IEnumerable Validate(ValidationContext validationContext) + { + if (AssignedPermissions.SelectMany(x => x.Value).Any(x => x.IsNullOrWhiteSpace())) + { + yield return new ValidationResult("A permission value cannot be null or empty", new[] { "Permissions" }); + } + } } } diff --git a/src/Umbraco.Core/Models/IContentType.cs b/src/Umbraco.Core/Models/IContentType.cs index a01e612887..a689da4c05 100644 --- a/src/Umbraco.Core/Models/IContentType.cs +++ b/src/Umbraco.Core/Models/IContentType.cs @@ -4,12 +4,16 @@ using Umbraco.Cms.Core.Models.ContentEditing; namespace Umbraco.Cms.Core.Models { - [Obsolete("This will be merged into IContentType in Umbraco 10")] + /// + /// Defines a content type that contains a history cleanup policy. + /// + [Obsolete("This will be merged into IContentType in Umbraco 10.")] public interface IContentTypeWithHistoryCleanup : IContentType { /// - /// Gets or Sets the history cleanup configuration + /// Gets or sets the history cleanup configuration. /// + /// The history cleanup configuration. HistoryCleanup HistoryCleanup { get; set; } } diff --git a/src/Umbraco.Core/Models/Mapping/ContentTypeMapDefinition.cs b/src/Umbraco.Core/Models/Mapping/ContentTypeMapDefinition.cs index debaa976c5..3625e90a14 100644 --- a/src/Umbraco.Core/Models/Mapping/ContentTypeMapDefinition.cs +++ b/src/Umbraco.Core/Models/Mapping/ContentTypeMapDefinition.cs @@ -185,20 +185,15 @@ namespace Umbraco.Cms.Core.Models.Mapping { target.HistoryCleanup = new HistoryCleanupViewModel { - PreventCleanup = sourceWithHistoryCleanup.HistoryCleanup.PreventCleanup, - KeepAllVersionsNewerThanDays = - sourceWithHistoryCleanup.HistoryCleanup.KeepAllVersionsNewerThanDays, - KeepLatestVersionPerDayForDays = - sourceWithHistoryCleanup.HistoryCleanup.KeepLatestVersionPerDayForDays, - GlobalKeepAllVersionsNewerThanDays = - _contentSettings.ContentVersionCleanupPolicy.KeepAllVersionsNewerThanDays, - GlobalKeepLatestVersionPerDayForDays = - _contentSettings.ContentVersionCleanupPolicy.KeepLatestVersionPerDayForDays, + PreventCleanup = sourceWithHistoryCleanup.HistoryCleanup?.PreventCleanup ?? false, + KeepAllVersionsNewerThanDays = sourceWithHistoryCleanup.HistoryCleanup?.KeepAllVersionsNewerThanDays, + KeepLatestVersionPerDayForDays = sourceWithHistoryCleanup.HistoryCleanup?.KeepLatestVersionPerDayForDays, + GlobalKeepAllVersionsNewerThanDays = _contentSettings.ContentVersionCleanupPolicy.KeepAllVersionsNewerThanDays, + GlobalKeepLatestVersionPerDayForDays = _contentSettings.ContentVersionCleanupPolicy.KeepLatestVersionPerDayForDays, GlobalEnableCleanup = _contentSettings.ContentVersionCleanupPolicy.EnableCleanup }; } - target.AllowCultureVariant = source.VariesByCulture(); target.AllowSegmentVariant = source.VariesBySegment(); target.ContentApps = _commonMapper.GetContentApps(source); diff --git a/src/Umbraco.Core/Models/Mapping/MemberTabsAndPropertiesMapper.cs b/src/Umbraco.Core/Models/Mapping/MemberTabsAndPropertiesMapper.cs index 8de419bd0e..d61e32d88a 100644 --- a/src/Umbraco.Core/Models/Mapping/MemberTabsAndPropertiesMapper.cs +++ b/src/Umbraco.Core/Models/Mapping/MemberTabsAndPropertiesMapper.cs @@ -75,84 +75,14 @@ namespace Umbraco.Cms.Core.Models.Mapping isLockedOutProperty.Value = _localizedTextService.Localize("general", "no"); } - if (_backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser != null - && _backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser.AllowedSections.Any(x => x.Equals(Constants.Applications.Settings))) - { - var memberTypeLink = $"#/member/memberTypes/edit/{source.ContentTypeId}"; - - // Replace the doctype property - var docTypeProperty = resolved.SelectMany(x => x.Properties) - .First(x => x.Alias == $"{Constants.PropertyEditors.InternalGenericPropertiesPrefix}doctype"); - docTypeProperty.Value = new List - { - new - { - linkText = source.ContentType.Name, - url = memberTypeLink, - target = "_self", - icon = Constants.Icons.ContentType - } - }; - docTypeProperty.View = "urllist"; - } - return resolved; } + [Obsolete("Use MapMembershipProperties. Will be removed in Umbraco 10.")] protected override IEnumerable GetCustomGenericProperties(IContentBase content) { var member = (IMember)content; - - var genericProperties = new List - { - new ContentPropertyDisplay - { - Alias = $"{Constants.PropertyEditors.InternalGenericPropertiesPrefix}id", - Label = _localizedTextService.Localize("general","id"), - Value = new List {member.Id.ToString(), member.Key.ToString()}, - View = "idwithguid" - }, - new ContentPropertyDisplay - { - Alias = $"{Constants.PropertyEditors.InternalGenericPropertiesPrefix}doctype", - Label = _localizedTextService.Localize("content","membertype"), - Value = _localizedTextService.UmbracoDictionaryTranslate(CultureDictionary, member.ContentType.Name), - View = _propertyEditorCollection[Constants.PropertyEditors.Aliases.Label].GetValueEditor().View - }, - GetLoginProperty(member, _localizedTextService), - new ContentPropertyDisplay - { - Alias = $"{Constants.PropertyEditors.InternalGenericPropertiesPrefix}email", - Label = _localizedTextService.Localize("general","email"), - Value = member.Email, - View = "email", - Validation = {Mandatory = true} - }, - new ContentPropertyDisplay - { - Alias = $"{Constants.PropertyEditors.InternalGenericPropertiesPrefix}password", - Label = _localizedTextService.Localize(null,"password"), - Value = new Dictionary - { - // TODO: why ignoreCase, what are we doing here?! - {"newPassword", member.GetAdditionalDataValueIgnoreCase("NewPassword", null)}, - }, - // TODO: Hard coding this because the changepassword doesn't necessarily need to be a resolvable (real) property editor - View = "changepassword", - // initialize the dictionary with the configuration from the default membership provider - Config = GetPasswordConfig(member) - }, - new ContentPropertyDisplay - { - Alias = $"{Constants.PropertyEditors.InternalGenericPropertiesPrefix}membergroup", - Label = _localizedTextService.Localize("content","membergroup"), - Value = GetMemberGroupValue(member.Username), - View = "membergroups", - Config = new Dictionary {{"IsRequired", true}} - } - }; - - return genericProperties; + return MapMembershipProperties(member, null); } private Dictionary GetPasswordConfig(IMember member) @@ -256,5 +186,59 @@ namespace Umbraco.Cms.Core.Models.Mapping return result; } + + public IEnumerable MapMembershipProperties(IMember member, MapperContext context) + { + var properties = new List + { + new ContentPropertyDisplay + { + Alias = $"{Constants.PropertyEditors.InternalGenericPropertiesPrefix}id", + Label = _localizedTextService.Localize("general","id"), + Value = new List {member.Id.ToString(), member.Key.ToString()}, + View = "idwithguid" + }, + new ContentPropertyDisplay + { + Alias = $"{Constants.PropertyEditors.InternalGenericPropertiesPrefix}doctype", + Label = _localizedTextService.Localize("content","membertype"), + Value = _localizedTextService.UmbracoDictionaryTranslate(CultureDictionary, member.ContentType.Name), + View = _propertyEditorCollection[Constants.PropertyEditors.Aliases.Label].GetValueEditor().View + }, + GetLoginProperty(member, _localizedTextService), + new ContentPropertyDisplay + { + Alias = $"{Constants.PropertyEditors.InternalGenericPropertiesPrefix}email", + Label = _localizedTextService.Localize("general","email"), + Value = member.Email, + View = "email", + Validation = {Mandatory = true} + }, + new ContentPropertyDisplay + { + Alias = $"{Constants.PropertyEditors.InternalGenericPropertiesPrefix}password", + Label = _localizedTextService.Localize(null,"password"), + Value = new Dictionary + { + // TODO: why ignoreCase, what are we doing here?! + {"newPassword", member.GetAdditionalDataValueIgnoreCase("NewPassword", null)}, + }, + // TODO: Hard coding this because the changepassword doesn't necessarily need to be a resolvable (real) property editor + View = "changepassword", + // Initialize the dictionary with the configuration from the default membership provider + Config = GetPasswordConfig(member) + }, + new ContentPropertyDisplay + { + Alias = $"{Constants.PropertyEditors.InternalGenericPropertiesPrefix}membergroup", + Label = _localizedTextService.Localize("content","membergroup"), + Value = GetMemberGroupValue(member.Username), + View = "membergroups", + Config = new Dictionary {{"IsRequired", true}} + } + }; + + return properties; + } } } diff --git a/src/Umbraco.Core/Models/Mapping/TabsAndPropertiesMapper.cs b/src/Umbraco.Core/Models/Mapping/TabsAndPropertiesMapper.cs index e7e4f5466d..fbab50afe3 100644 --- a/src/Umbraco.Core/Models/Mapping/TabsAndPropertiesMapper.cs +++ b/src/Umbraco.Core/Models/Mapping/TabsAndPropertiesMapper.cs @@ -52,47 +52,25 @@ namespace Umbraco.Cms.Core.Models.Mapping var noGroupProperties = content.GetNonGroupedProperties() .Where(x => IgnoreProperties.Contains(x.Alias) == false) // skip ignored .ToList(); - var genericproperties = MapProperties(content, noGroupProperties, context); + var genericProperties = MapProperties(content, noGroupProperties, context); - tabs.Add(new Tab - { - Id = 0, - Label = LocalizedTextService.Localize("general", "properties"), - Alias = "Generic properties", - Properties = genericproperties, - Type = PropertyGroupType.Group.ToString() - }); - var genericProps = tabs.Single(x => x.Id == 0); - - //store the current props to append to the newly inserted ones - var currProps = genericProps.Properties.ToArray(); - - var contentProps = new List(); var customProperties = GetCustomGenericProperties(content); if (customProperties != null) { - //add the custom ones - contentProps.AddRange(customProperties); + genericProperties.AddRange(customProperties); } - //now add the user props - contentProps.AddRange(currProps); - - //re-assign - genericProps.Properties = contentProps; - - //Show or hide properties tab based on whether it has or not any properties - if (genericProps.Properties.Any() == false) + if (genericProperties.Count > 0) { - //loop through the tabs, remove the one with the id of zero and exit the loop - for (var i = 0; i < tabs.Count; i++) + tabs.Add(new Tab { - if (tabs[i].Id != 0) continue; - tabs.RemoveAt(i); - break; - } + Id = 0, + Label = LocalizedTextService.Localize("general", "properties"), + Alias = "Generic properties", + Properties = genericProperties + }); } } diff --git a/src/Umbraco.Core/Security/LegacyPasswordSecurity.cs b/src/Umbraco.Core/Security/LegacyPasswordSecurity.cs index bf56c3161d..8f6813e7ba 100644 --- a/src/Umbraco.Core/Security/LegacyPasswordSecurity.cs +++ b/src/Umbraco.Core/Security/LegacyPasswordSecurity.cs @@ -50,9 +50,33 @@ namespace Umbraco.Cms.Core.Security if (dbPassword.StartsWith(Constants.Security.EmptyPasswordPrefix)) return false; - var storedHashedPass = ParseStoredHashPassword(algorithm, dbPassword, out var salt); - var hashed = HashPassword(algorithm, password, salt); - return storedHashedPass == hashed; + try + { + var storedHashedPass = ParseStoredHashPassword(algorithm, dbPassword, out var salt); + var hashed = HashPassword(algorithm, password, salt); + return storedHashedPass == hashed; + } + catch (ArgumentOutOfRangeException) + { + //This can happen if the length of the password is wrong and a salt cannot be extracted. + return false; + } + + } + + /// + /// Verify a legacy hashed password (HMACSHA1) + /// + public bool VerifyLegacyHashedPassword(string password, string dbPassword) + { + var hashAlgorith = new HMACSHA1 + { + //the legacy salt was actually the password :( + Key = Encoding.Unicode.GetBytes(password) + }; + var hashed = Convert.ToBase64String(hashAlgorith.ComputeHash(Encoding.Unicode.GetBytes(password))); + + return dbPassword == hashed; } /// diff --git a/src/Umbraco.Core/Telemetry/ITelemetryService.cs b/src/Umbraco.Core/Telemetry/ITelemetryService.cs new file mode 100644 index 0000000000..60070481f3 --- /dev/null +++ b/src/Umbraco.Core/Telemetry/ITelemetryService.cs @@ -0,0 +1,15 @@ +using Umbraco.Cms.Core.Telemetry.Models; + +namespace Umbraco.Cms.Core.Telemetry +{ + /// + /// Service which gathers the data for telemetry reporting + /// + public interface ITelemetryService + { + /// + /// Try and get the + /// + bool TryGetTelemetryReportData(out TelemetryReportData telemetryReportData); + } +} diff --git a/src/Umbraco.Core/Telemetry/Models/PackageTelemetry.cs b/src/Umbraco.Core/Telemetry/Models/PackageTelemetry.cs new file mode 100644 index 0000000000..8b7aa4bc0c --- /dev/null +++ b/src/Umbraco.Core/Telemetry/Models/PackageTelemetry.cs @@ -0,0 +1,26 @@ +using System.Runtime.Serialization; + +namespace Umbraco.Cms.Core.Telemetry.Models +{ + /// + /// Serializable class containing information about an installed package. + /// + [DataContract(Name = "packageTelemetry")] + public class PackageTelemetry + { + /// + /// Gets or sets the name of the installed package. + /// + [DataMember(Name = "name")] + public string Name { get; set; } + + /// + /// Gets or sets the version of the installed package. + /// + /// + /// This may be an empty string if no version is specified, or if package telemetry has been restricted. + /// + [DataMember(Name = "version")] + public string Version { get; set; } + } +} diff --git a/src/Umbraco.Core/Telemetry/Models/TelemetryReportData.cs b/src/Umbraco.Core/Telemetry/Models/TelemetryReportData.cs new file mode 100644 index 0000000000..d19e24695b --- /dev/null +++ b/src/Umbraco.Core/Telemetry/Models/TelemetryReportData.cs @@ -0,0 +1,34 @@ +using System; +using System.Collections.Generic; +using System.Runtime.Serialization; + +namespace Umbraco.Cms.Core.Telemetry.Models +{ + /// + /// Serializable class containing telemetry information. + /// + [DataContract] + public class TelemetryReportData + { + /// + /// Gets or sets a random GUID to prevent an instance posting multiple times pr. day. + /// + [DataMember(Name = "id")] + public Guid Id { get; set; } + + /// + /// Gets or sets the Umbraco CMS version. + /// + [DataMember(Name = "version")] + public string Version { get; set; } + + /// + /// Gets or sets an enumerable containing information about packages. + /// + /// + /// Contains only the name and version of the packages, unless no version is specified. + /// + [DataMember(Name = "packages")] + public IEnumerable Packages { get; set; } + } +} diff --git a/src/Umbraco.Core/Telemetry/TelemetryService.cs b/src/Umbraco.Core/Telemetry/TelemetryService.cs new file mode 100644 index 0000000000..63e4e1ff49 --- /dev/null +++ b/src/Umbraco.Core/Telemetry/TelemetryService.cs @@ -0,0 +1,86 @@ +using System; +using System.Collections.Generic; +using Microsoft.Extensions.Options; +using Umbraco.Cms.Core.Configuration; +using Umbraco.Cms.Core.Configuration.Models; +using Umbraco.Cms.Core.Manifest; +using Umbraco.Cms.Core.Telemetry.Models; +using Umbraco.Extensions; + +namespace Umbraco.Cms.Core.Telemetry +{ + /// + internal class TelemetryService : ITelemetryService + { + private readonly IOptionsMonitor _globalSettings; + private readonly IManifestParser _manifestParser; + private readonly IUmbracoVersion _umbracoVersion; + + /// + /// Initializes a new instance of the class. + /// + public TelemetryService( + IOptionsMonitor globalSettings, + IManifestParser manifestParser, + IUmbracoVersion umbracoVersion) + { + _manifestParser = manifestParser; + _umbracoVersion = umbracoVersion; + _globalSettings = globalSettings; + } + + /// + public bool TryGetTelemetryReportData(out TelemetryReportData telemetryReportData) + { + if (TryGetTelemetryId(out Guid telemetryId) is false) + { + telemetryReportData = null; + return false; + } + + telemetryReportData = new TelemetryReportData + { + Id = telemetryId, + Version = _umbracoVersion.SemanticVersion.ToSemanticStringWithoutBuild(), + Packages = GetPackageTelemetry() + }; + return true; + } + + private bool TryGetTelemetryId(out Guid telemetryId) + { + // Parse telemetry string as a GUID & verify its a GUID and not some random string + // since users may have messed with or decided to empty the app setting or put in something random + if (Guid.TryParse(_globalSettings.CurrentValue.Id, out var parsedTelemetryId) is false) + { + telemetryId = Guid.Empty; + return false; + } + + telemetryId = parsedTelemetryId; + return true; + } + + private IEnumerable GetPackageTelemetry() + { + List packages = new (); + IEnumerable manifests = _manifestParser.GetManifests(); + + foreach (PackageManifest manifest in manifests) + { + if (manifest.AllowPackageTelemetry is false) + { + continue; + } + + packages.Add(new PackageTelemetry + { + Name = manifest.PackageName, + Version = manifest.Version ?? string.Empty + }); + } + + return packages; + } + } +} diff --git a/src/Umbraco.Core/WebAssets/IRuntimeMinifier.cs b/src/Umbraco.Core/WebAssets/IRuntimeMinifier.cs index 7ca1cd883b..a3b88633b6 100644 --- a/src/Umbraco.Core/WebAssets/IRuntimeMinifier.cs +++ b/src/Umbraco.Core/WebAssets/IRuntimeMinifier.cs @@ -88,7 +88,18 @@ namespace Umbraco.Cms.Core.WebAssets /// /// Ensures that all runtime minifications are refreshed on next request. E.g. Clearing cache. /// + /// + /// + /// No longer necessary, invalidation occurs automatically if any of the following occur. + /// + /// + /// Your sites assembly information version changes. + /// Umbraco.Cms.Core assembly information version changes. + /// RuntimeMinificationSettings Version string changes. + /// + /// for further details. + /// + [Obsolete("Invalidation is handled automatically. Scheduled for removal V11.")] void Reset(); - } } diff --git a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs index 173c80ea7b..6781623fe7 100644 --- a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs +++ b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs @@ -268,16 +268,17 @@ namespace Umbraco.Cms.Infrastructure.DependencyInjection public static IUmbracoBuilder AddLogViewer(this IUmbracoBuilder builder) { builder.Services.AddSingleton(); + builder.Services.AddSingleton(); builder.SetLogViewer(); builder.Services.AddSingleton(factory => new SerilogJsonLogViewer(factory.GetRequiredService>(), factory.GetRequiredService(), factory.GetRequiredService(), + factory.GetRequiredService(), Log.Logger)); return builder; } - public static IUmbracoBuilder AddCoreNotifications(this IUmbracoBuilder builder) { // add handlers for sending user notifications (i.e. emails) diff --git a/src/Umbraco.Infrastructure/HostedServices/ReportSiteTask.cs b/src/Umbraco.Infrastructure/HostedServices/ReportSiteTask.cs index 4f02a89439..7591290bf4 100644 --- a/src/Umbraco.Infrastructure/HostedServices/ReportSiteTask.cs +++ b/src/Umbraco.Infrastructure/HostedServices/ReportSiteTask.cs @@ -1,35 +1,42 @@ using System; using System.Net.Http; -using System.Runtime.Serialization; using System.Text; using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Newtonsoft.Json; using Umbraco.Cms.Core.Configuration; using Umbraco.Cms.Core.Configuration.Models; -using Umbraco.Extensions; +using Umbraco.Cms.Core.Telemetry; +using Umbraco.Cms.Core.Telemetry.Models; +using Umbraco.Cms.Web.Common.DependencyInjection; namespace Umbraco.Cms.Infrastructure.HostedServices { public class ReportSiteTask : RecurringHostedServiceBase { private readonly ILogger _logger; - private readonly IUmbracoVersion _umbracoVersion; - private GlobalSettings _globalSettings; + private readonly ITelemetryService _telemetryService; private static HttpClient s_httpClient; public ReportSiteTask( ILogger logger, - IUmbracoVersion umbracoVersion, - IOptionsMonitor globalSettings) + ITelemetryService telemetryService) : base(TimeSpan.FromDays(1), TimeSpan.FromMinutes(1)) { _logger = logger; - _umbracoVersion = umbracoVersion; - _globalSettings = globalSettings.CurrentValue; + _telemetryService = telemetryService; s_httpClient = new HttpClient(); - globalSettings.OnChange(x => _globalSettings = x); + } + + [Obsolete("Use the constructor that takes ITelemetryService instead, scheduled for removal in V11")] + public ReportSiteTask( + ILogger logger, + IUmbracoVersion umbracoVersion, + IOptions globalSettings) + : this(logger, StaticServiceProvider.Instance.GetRequiredService()) + { } /// @@ -38,14 +45,8 @@ namespace Umbraco.Cms.Infrastructure.HostedServices /// public override async Task PerformExecuteAsync(object state) { - // Try & get a value stored in umbracoSettings.config on the backoffice XML element ID attribute - var backofficeIdentifierRaw = _globalSettings.Id; - - // Parse as a GUID & verify its a GUID and not some random string - // In case of users may have messed or decided to empty the file contents or put in something random - if (Guid.TryParse(backofficeIdentifierRaw, out var telemetrySiteIdentifier) == false) + if (_telemetryService.TryGetTelemetryReportData(out TelemetryReportData telemetryReportData) is false) { - // Some users may have decided to mess with the XML attribute and put in something else _logger.LogWarning("No telemetry marker found"); return; @@ -53,7 +54,6 @@ namespace Umbraco.Cms.Infrastructure.HostedServices try { - if (s_httpClient.BaseAddress is null) { // Send data to LIVE telemetry @@ -65,9 +65,6 @@ namespace Umbraco.Cms.Infrastructure.HostedServices #if DEBUG // Send data to DEBUG telemetry service s_httpClient.BaseAddress = new Uri("https://telemetry.rainbowsrock.net/"); - - - #endif } @@ -76,8 +73,7 @@ namespace Umbraco.Cms.Infrastructure.HostedServices using (var request = new HttpRequestMessage(HttpMethod.Post, "installs/")) { - var postData = new TelemetryReportData { Id = telemetrySiteIdentifier, Version = _umbracoVersion.SemanticVersion.ToSemanticStringWithoutBuild() }; - request.Content = new StringContent(JsonConvert.SerializeObject(postData), Encoding.UTF8, "application/json"); //CONTENT-TYPE header + request.Content = new StringContent(JsonConvert.SerializeObject(telemetryReportData), Encoding.UTF8, "application/json"); //CONTENT-TYPE header // Make a HTTP Post to telemetry service // https://telemetry.umbraco.com/installs/ @@ -95,16 +91,5 @@ namespace Umbraco.Cms.Infrastructure.HostedServices _logger.LogDebug("There was a problem sending a request to the Umbraco telemetry service"); } } - [DataContract] - private class TelemetryReportData - { - [DataMember(Name = "id")] - public Guid Id { get; set; } - - [DataMember(Name = "version")] - public string Version { get; set; } - } - - } } diff --git a/src/Umbraco.Infrastructure/Logging/Serilog/LoggerConfigExtensions.cs b/src/Umbraco.Infrastructure/Logging/Serilog/LoggerConfigExtensions.cs index 2fcb78f7a6..c4e8484307 100644 --- a/src/Umbraco.Infrastructure/Logging/Serilog/LoggerConfigExtensions.cs +++ b/src/Umbraco.Infrastructure/Logging/Serilog/LoggerConfigExtensions.cs @@ -24,14 +24,26 @@ namespace Umbraco.Extensions /// Such as adding ProcessID, Thread, AppDomain etc /// It is highly recommended that you keep/use this default in your own logging config customizations /// - /// A Serilog LoggerConfiguration - /// - /// public static LoggerConfiguration MinimalConfiguration( this LoggerConfiguration logConfig, IHostingEnvironment hostingEnvironment, ILoggingConfiguration loggingConfiguration, IConfiguration configuration) + { + return MinimalConfiguration(logConfig, hostingEnvironment, loggingConfiguration, configuration, out _); + } + + /// + /// This configures Serilog with some defaults + /// Such as adding ProcessID, Thread, AppDomain etc + /// It is highly recommended that you keep/use this default in your own logging config customizations + /// + public static LoggerConfiguration MinimalConfiguration( + this LoggerConfiguration logConfig, + IHostingEnvironment hostingEnvironment, + ILoggingConfiguration loggingConfiguration, + IConfiguration configuration, + out UmbracoFileConfiguration umbFileConfiguration) { global::Serilog.Debugging.SelfLog.Enable(msg => System.Diagnostics.Debug.WriteLine(msg)); @@ -54,6 +66,8 @@ namespace Umbraco.Extensions //This is not optimal, but seems to be the only way if we do not make an Serilog.Sink.UmbracoFile sink all the way. var umbracoFileConfiguration = new UmbracoFileConfiguration(configuration); + umbFileConfiguration = umbracoFileConfiguration; + logConfig.WriteTo.UmbracoFile( path : umbracoFileConfiguration.GetPath(loggingConfiguration.LogDirectory), fileSizeLimitBytes: umbracoFileConfiguration.FileSizeLimitBytes, diff --git a/src/Umbraco.Infrastructure/Logging/Serilog/SerilogLogger.cs b/src/Umbraco.Infrastructure/Logging/Serilog/SerilogLogger.cs index 054070240e..151f3e760c 100644 --- a/src/Umbraco.Infrastructure/Logging/Serilog/SerilogLogger.cs +++ b/src/Umbraco.Infrastructure/Logging/Serilog/SerilogLogger.cs @@ -3,6 +3,7 @@ using Microsoft.Extensions.Configuration; using Serilog; using Serilog.Events; using Umbraco.Cms.Core.Hosting; +using Umbraco.Cms.Infrastructure.Logging.Serilog; using Umbraco.Extensions; namespace Umbraco.Cms.Core.Logging.Serilog @@ -20,6 +21,14 @@ namespace Umbraco.Cms.Core.Logging.Serilog SerilogLog = logConfig.CreateLogger(); } + public static SerilogLogger CreateWithDefaultConfiguration( + IHostingEnvironment hostingEnvironment, + ILoggingConfiguration loggingConfiguration, + IConfiguration configuration) + { + return CreateWithDefaultConfiguration(hostingEnvironment, loggingConfiguration, configuration, out _); + } + /// /// Creates a logger with some pre-defined configuration and remainder from config file /// @@ -27,13 +36,14 @@ namespace Umbraco.Cms.Core.Logging.Serilog public static SerilogLogger CreateWithDefaultConfiguration( IHostingEnvironment hostingEnvironment, ILoggingConfiguration loggingConfiguration, - IConfiguration configuration) + IConfiguration configuration, + out UmbracoFileConfiguration umbracoFileConfig) { - var loggerConfig = new LoggerConfiguration() - .MinimalConfiguration(hostingEnvironment, loggingConfiguration, configuration) + var serilogConfig = new LoggerConfiguration() + .MinimalConfiguration(hostingEnvironment, loggingConfiguration, configuration, out umbracoFileConfig) .ReadFrom.Configuration(configuration); - return new SerilogLogger(loggerConfig); + return new SerilogLogger(serilogConfig); } /// diff --git a/src/Umbraco.Infrastructure/Logging/Viewer/ILogLevelLoader.cs b/src/Umbraco.Infrastructure/Logging/Viewer/ILogLevelLoader.cs new file mode 100644 index 0000000000..705e283ed9 --- /dev/null +++ b/src/Umbraco.Infrastructure/Logging/Viewer/ILogLevelLoader.cs @@ -0,0 +1,18 @@ +using System.Collections.ObjectModel; +using Serilog.Events; + +namespace Umbraco.Cms.Core.Logging.Viewer +{ + public interface ILogLevelLoader + { + /// + /// Get the Serilog level values of the global minimum and the UmbracoFile one from the config file. + /// + ReadOnlyDictionary GetLogLevelsFromSinks(); + + /// + /// Get the Serilog minimum-level value from the config file. + /// + LogEventLevel GetGlobalMinLogLevel(); + } +} diff --git a/src/Umbraco.Infrastructure/Logging/Viewer/ILogViewer.cs b/src/Umbraco.Infrastructure/Logging/Viewer/ILogViewer.cs index 61ca492ba2..4fed8ca9ab 100644 --- a/src/Umbraco.Infrastructure/Logging/Viewer/ILogViewer.cs +++ b/src/Umbraco.Infrastructure/Logging/Viewer/ILogViewer.cs @@ -1,4 +1,7 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using Serilog.Events; using Umbraco.Cms.Core.Models; namespace Umbraco.Cms.Core.Logging.Viewer @@ -44,6 +47,7 @@ namespace Umbraco.Cms.Core.Logging.Viewer /// Gets the current Serilog minimum log level /// /// + [Obsolete("Please use GetLogLevels() instead. Scheduled for removal in V11.")] string GetLogLevel(); /// diff --git a/src/Umbraco.Infrastructure/Logging/Viewer/LogLevelLoader.cs b/src/Umbraco.Infrastructure/Logging/Viewer/LogLevelLoader.cs new file mode 100644 index 0000000000..b090ddb77c --- /dev/null +++ b/src/Umbraco.Infrastructure/Logging/Viewer/LogLevelLoader.cs @@ -0,0 +1,41 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using System.Text; +using Serilog; +using Serilog.Events; +using Umbraco.Cms.Infrastructure.Logging.Serilog; + +namespace Umbraco.Cms.Core.Logging.Viewer +{ + public class LogLevelLoader : ILogLevelLoader + { + private readonly UmbracoFileConfiguration _umbracoFileConfig; + + public LogLevelLoader(UmbracoFileConfiguration umbracoFileConfig) => _umbracoFileConfig = umbracoFileConfig; + + /// + /// Get the Serilog level values of the global minimum and the UmbracoFile one from the config file. + /// + public ReadOnlyDictionary GetLogLevelsFromSinks() + { + var configuredLogLevels = new Dictionary + { + { "Global", GetGlobalMinLogLevel() }, + { "UmbracoFile", _umbracoFileConfig.RestrictedToMinimumLevel } + }; + + return new ReadOnlyDictionary(configuredLogLevels); + } + + /// + /// Get the Serilog minimum-level value from the config file. + /// + public LogEventLevel GetGlobalMinLogLevel() + { + var logLevel = Enum.GetValues(typeof(LogEventLevel)).Cast().Where(Log.IsEnabled).DefaultIfEmpty(LogEventLevel.Information)?.Min() ?? null; + return (LogEventLevel)logLevel; + } + } +} diff --git a/src/Umbraco.Infrastructure/Logging/Viewer/SerilogJsonLogViewer.cs b/src/Umbraco.Infrastructure/Logging/Viewer/SerilogJsonLogViewer.cs index 7599ab0a16..d52d455fbc 100644 --- a/src/Umbraco.Infrastructure/Logging/Viewer/SerilogJsonLogViewer.cs +++ b/src/Umbraco.Infrastructure/Logging/Viewer/SerilogJsonLogViewer.cs @@ -6,7 +6,6 @@ using Microsoft.Extensions.Logging; using Newtonsoft.Json; using Serilog.Events; using Serilog.Formatting.Compact.Reader; -using Umbraco.Cms.Core.Logging; namespace Umbraco.Cms.Core.Logging.Viewer { @@ -19,8 +18,9 @@ namespace Umbraco.Cms.Core.Logging.Viewer ILogger logger, ILogViewerConfig logViewerConfig, ILoggingConfiguration loggingConfiguration, + ILogLevelLoader logLevelLoader, global::Serilog.ILogger serilogLog) - : base(logViewerConfig, serilogLog) + : base(logViewerConfig, logLevelLoader, serilogLog) { _logger = logger; _logsPath = loggingConfiguration.LogDirectory; diff --git a/src/Umbraco.Infrastructure/Logging/Viewer/SerilogLogViewerSourceBase.cs b/src/Umbraco.Infrastructure/Logging/Viewer/SerilogLogViewerSourceBase.cs index ce897de0cd..eafe4d1787 100644 --- a/src/Umbraco.Infrastructure/Logging/Viewer/SerilogLogViewerSourceBase.cs +++ b/src/Umbraco.Infrastructure/Logging/Viewer/SerilogLogViewerSourceBase.cs @@ -1,8 +1,11 @@ using System; using System.Collections.Generic; +using System.Collections.ObjectModel; using System.Linq; +using Microsoft.Extensions.DependencyInjection; using Serilog.Events; using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Web.Common.DependencyInjection; using Umbraco.Extensions; namespace Umbraco.Cms.Core.Logging.Viewer @@ -10,11 +13,21 @@ namespace Umbraco.Cms.Core.Logging.Viewer public abstract class SerilogLogViewerSourceBase : ILogViewer { private readonly ILogViewerConfig _logViewerConfig; + private readonly ILogLevelLoader _logLevelLoader; private readonly global::Serilog.ILogger _serilogLog; + [Obsolete("Please use ctor with all params instead. Scheduled for removal in V11.")] protected SerilogLogViewerSourceBase(ILogViewerConfig logViewerConfig, global::Serilog.ILogger serilogLog) { _logViewerConfig = logViewerConfig; + _logLevelLoader = StaticServiceProvider.Instance.GetRequiredService(); + _serilogLog = serilogLog; + } + + protected SerilogLogViewerSourceBase(ILogViewerConfig logViewerConfig, ILogLevelLoader logLevelLoader, global::Serilog.ILogger serilogLog) + { + _logViewerConfig = logViewerConfig; + _logLevelLoader = logLevelLoader; _serilogLog = serilogLog; } @@ -43,14 +56,22 @@ namespace Umbraco.Cms.Core.Logging.Viewer return errorCounter.Count; } + /// + /// Get the Serilog minimum-level and UmbracoFile-level values from the config file. + /// + public ReadOnlyDictionary GetLogLevels() + { + return _logLevelLoader.GetLogLevelsFromSinks(); + } + /// /// Get the Serilog minimum-level value from the config file. /// - /// + [Obsolete("Please use LogLevelLoader.GetGlobalMinLogLevel() instead. Scheduled for removal in V11.")] public string GetLogLevel() { var logLevel = Enum.GetValues(typeof(LogEventLevel)).Cast().Where(_serilogLog.IsEnabled).DefaultIfEmpty(LogEventLevel.Information)?.Min() ?? null; - return logLevel?.ToString() ?? ""; + return logLevel?.ToString() ?? string.Empty; } public LogLevelCounts GetLogLevelCounts(LogTimePeriod logTimePeriod) @@ -129,7 +150,5 @@ namespace Umbraco.Cms.Core.Logging.Viewer Items = logMessages }; } - - } } diff --git a/src/Umbraco.Infrastructure/Mail/EmailSender.cs b/src/Umbraco.Infrastructure/Mail/EmailSender.cs index 31b3d67bd0..22d8bec680 100644 --- a/src/Umbraco.Infrastructure/Mail/EmailSender.cs +++ b/src/Umbraco.Infrastructure/Mail/EmailSender.cs @@ -76,15 +76,13 @@ namespace Umbraco.Cms.Infrastructure.Mail } } - var isPickupDirectoryConfigured = !string.IsNullOrWhiteSpace(_globalSettings.Smtp?.PickupDirectoryLocation); - - if (_globalSettings.IsSmtpServerConfigured == false && !isPickupDirectoryConfigured) + if (!_globalSettings.IsSmtpServerConfigured && !_globalSettings.IsPickupDirectoryLocationConfigured) { _logger.LogDebug("Could not send email for {Subject}. It was not handled by a notification handler and there is no SMTP configured.", message.Subject); return; } - if (isPickupDirectoryConfigured && !string.IsNullOrWhiteSpace(_globalSettings.Smtp?.From)) + if (_globalSettings.IsPickupDirectoryLocationConfigured && !string.IsNullOrWhiteSpace(_globalSettings.Smtp?.From)) { // The following code snippet is the recommended way to handle PickupDirectoryLocation. // See more https://github.com/jstedfast/MailKit/blob/master/FAQ.md#q-how-can-i-send-email-to-a-specifiedpickupdirectory @@ -132,7 +130,7 @@ namespace Umbraco.Cms.Infrastructure.Mail _globalSettings.Smtp.Port, (MailKit.Security.SecureSocketOptions)(int)_globalSettings.Smtp.SecureSocketOptions); - if (!(_globalSettings.Smtp.Username is null && _globalSettings.Smtp.Password is null)) + if (!string.IsNullOrWhiteSpace(_globalSettings.Smtp.Username) && !string.IsNullOrWhiteSpace(_globalSettings.Smtp.Password)) { await client.AuthenticateAsync(_globalSettings.Smtp.Username, _globalSettings.Smtp.Password); } @@ -155,7 +153,10 @@ namespace Umbraco.Cms.Infrastructure.Mail /// /// /// We assume this is possible if either an event handler is registered or an smtp server is configured + /// or a pickup directory location is configured /// - public bool CanSendRequiredEmail() => _globalSettings.IsSmtpServerConfigured || _notificationHandlerRegistered; + public bool CanSendRequiredEmail() => _globalSettings.IsSmtpServerConfigured + || _globalSettings.IsPickupDirectoryLocationConfigured + || _notificationHandlerRegistered; } -} +} \ No newline at end of file diff --git a/src/Umbraco.Infrastructure/Media/ImageSharpImageUrlGenerator.cs b/src/Umbraco.Infrastructure/Media/ImageSharpImageUrlGenerator.cs index ecde6790d2..2b68e4dde0 100644 --- a/src/Umbraco.Infrastructure/Media/ImageSharpImageUrlGenerator.cs +++ b/src/Umbraco.Infrastructure/Media/ImageSharpImageUrlGenerator.cs @@ -56,16 +56,16 @@ namespace Umbraco.Cms.Infrastructure.Media void AddQueryString(string key, params IConvertible[] values) => AppendQueryString(key + '=' + string.Join(",", values.Select(x => x.ToString(CultureInfo.InvariantCulture)))); - if (options.FocalPoint != null) - { - AddQueryString("rxy", options.FocalPoint.Left, options.FocalPoint.Top); - } - if (options.Crop != null) { AddQueryString("cc", options.Crop.Left, options.Crop.Top, options.Crop.Right, options.Crop.Bottom); } + if (options.FocalPoint != null) + { + AddQueryString("rxy", options.FocalPoint.Left, options.FocalPoint.Top); + } + if (options.ImageCropMode.HasValue) { AddQueryString("rmode", options.ImageCropMode.Value.ToString().ToLowerInvariant()); diff --git a/src/Umbraco.Infrastructure/Migrations/Install/DatabaseDataCreator.cs b/src/Umbraco.Infrastructure/Migrations/Install/DatabaseDataCreator.cs index 8c99fd6630..4f2ef1f2e9 100644 --- a/src/Umbraco.Infrastructure/Migrations/Install/DatabaseDataCreator.cs +++ b/src/Umbraco.Infrastructure/Migrations/Install/DatabaseDataCreator.cs @@ -199,9 +199,9 @@ namespace Umbraco.Cms.Infrastructure.Migrations.Install private void CreateUserGroupData() { - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.UserGroup, "id", false, new UserGroupDto { Id = 1, StartMediaId = -1, StartContentId = -1, Alias = Cms.Core.Constants.Security.AdminGroupAlias, Name = "Administrators", DefaultPermissions = "CADMOSKTPIURZ:5F7ï", CreateDate = DateTime.Now, UpdateDate = DateTime.Now, Icon = "icon-medal" }); - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.UserGroup, "id", false, new UserGroupDto { Id = 2, StartMediaId = -1, StartContentId = -1, Alias = Cms.Core.Constants.Security.WriterGroupAlias, Name = "Writers", DefaultPermissions = "CAH:F", CreateDate = DateTime.Now, UpdateDate = DateTime.Now, Icon = "icon-edit" }); - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.UserGroup, "id", false, new UserGroupDto { Id = 3, StartMediaId = -1, StartContentId = -1, Alias = Cms.Core.Constants.Security.EditorGroupAlias, Name = "Editors", DefaultPermissions = "CADMOSKTPUZ:5Fï", CreateDate = DateTime.Now, UpdateDate = DateTime.Now, Icon = "icon-tools" }); + _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.UserGroup, "id", false, new UserGroupDto { Id = 1, StartMediaId = -1, StartContentId = -1, Alias = Cms.Core.Constants.Security.AdminGroupAlias, Name = "Administrators", DefaultPermissions = "CADMOSKTPIURZ:5F7ïN", CreateDate = DateTime.Now, UpdateDate = DateTime.Now, Icon = "icon-medal" }); + _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.UserGroup, "id", false, new UserGroupDto { Id = 2, StartMediaId = -1, StartContentId = -1, Alias = Cms.Core.Constants.Security.WriterGroupAlias, Name = "Writers", DefaultPermissions = "CAH:FN", CreateDate = DateTime.Now, UpdateDate = DateTime.Now, Icon = "icon-edit" }); + _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.UserGroup, "id", false, new UserGroupDto { Id = 3, StartMediaId = -1, StartContentId = -1, Alias = Cms.Core.Constants.Security.EditorGroupAlias, Name = "Editors", DefaultPermissions = "CADMOSKTPUZ:5FïN", CreateDate = DateTime.Now, UpdateDate = DateTime.Now, Icon = "icon-tools" }); _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.UserGroup, "id", false, new UserGroupDto { Id = 4, StartMediaId = -1, StartContentId = -1, Alias = Cms.Core.Constants.Security.TranslatorGroupAlias, Name = "Translators", DefaultPermissions = "AF", CreateDate = DateTime.Now, UpdateDate = DateTime.Now, Icon = "icon-globe" }); _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.UserGroup, "id", false, new UserGroupDto { Id = 5, StartMediaId = -1, StartContentId = -1, Alias = Cms.Core.Constants.Security.SensitiveDataGroupAlias, Name = "Sensitive data", DefaultPermissions = "", CreateDate = DateTime.Now, UpdateDate = DateTime.Now, Icon = "icon-lock" }); } @@ -241,7 +241,7 @@ namespace Umbraco.Cms.Infrastructure.Migrations.Install _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.PropertyTypeGroup, "id", false, new PropertyTypeGroupDto { Id = 54, UniqueId = new Guid(Cms.Core.Constants.PropertyTypeGroups.Article), ContentTypeNodeId = 1036, Text = "Article", Alias = "article", SortOrder = 1 }); _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.PropertyTypeGroup, "id", false, new PropertyTypeGroupDto { Id = 55, UniqueId = new Guid(Cms.Core.Constants.PropertyTypeGroups.VectorGraphics), ContentTypeNodeId = 1037, Text = "Vector Graphics", Alias = "vectorGraphics", SortOrder = 1 }); //membership property group - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.PropertyTypeGroup, "id", false, new PropertyTypeGroupDto { Id = 11, UniqueId = new Guid(Cms.Core.Constants.PropertyTypeGroups.Membership), ContentTypeNodeId = 1044, Text = "Membership", Alias = "membership", SortOrder = 1 }); + _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.PropertyTypeGroup, "id", false, new PropertyTypeGroupDto { Id = 11, UniqueId = new Guid(Cms.Core.Constants.PropertyTypeGroups.Membership), ContentTypeNodeId = 1044, Text = Cms.Core.Constants.Conventions.Member.StandardPropertiesGroupName, Alias = Cms.Core.Constants.Conventions.Member.StandardPropertiesGroupAlias, SortOrder = 1 }); } private void CreatePropertyTypeData() diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs index 836981e73b..257fee9967 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs @@ -267,6 +267,7 @@ namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade // TO 9.2.0 To("{0571C395-8F0B-44E9-8E3F-47BDD08D817B}"); + } } } diff --git a/src/Umbraco.Infrastructure/Packaging/PackageDataInstallation.cs b/src/Umbraco.Infrastructure/Packaging/PackageDataInstallation.cs index 5c9942f945..defea0ea51 100644 --- a/src/Umbraco.Infrastructure/Packaging/PackageDataInstallation.cs +++ b/src/Umbraco.Infrastructure/Packaging/PackageDataInstallation.cs @@ -814,9 +814,47 @@ namespace Umbraco.Cms.Infrastructure.Packaging UpdateContentTypesPropertyGroups(contentType, documentType.Element("Tabs")); UpdateContentTypesProperties(contentType, documentType.Element("GenericProperties")); + if (contentType is IContentTypeWithHistoryCleanup withCleanup) + { + UpdateHistoryCleanupPolicy(withCleanup, documentType.Element("HistoryCleanupPolicy")); + } + return contentType; } + private void UpdateHistoryCleanupPolicy(IContentTypeWithHistoryCleanup withCleanup, XElement element) + { + if (element == null) + { + return; + } + + withCleanup.HistoryCleanup ??= new Core.Models.ContentEditing.HistoryCleanup(); + + if (bool.TryParse(element.Attribute("preventCleanup")?.Value, out var preventCleanup)) + { + withCleanup.HistoryCleanup.PreventCleanup = preventCleanup; + } + + if (int.TryParse(element.Attribute("keepAllVersionsNewerThanDays")?.Value, out var keepAll)) + { + withCleanup.HistoryCleanup.KeepAllVersionsNewerThanDays = keepAll; + } + else + { + withCleanup.HistoryCleanup.KeepAllVersionsNewerThanDays = null; + } + + if (int.TryParse(element.Attribute("keepLatestVersionPerDayForDays")?.Value, out var keepLatest)) + { + withCleanup.HistoryCleanup.KeepLatestVersionPerDayForDays = keepLatest; + } + else + { + withCleanup.HistoryCleanup.KeepLatestVersionPerDayForDays = null; + } + } + private void UpdateContentTypesAllowedTemplates(IContentType contentType, XElement allowedTemplatesElement, XElement defaultTemplateElement) { if (allowedTemplatesElement != null && allowedTemplatesElement.Elements("Template").Any()) diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentTypeCommonRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentTypeCommonRepository.cs index 2a14a745c9..04ca55b499 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentTypeCommonRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentTypeCommonRepository.cs @@ -334,13 +334,10 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement // ensure builtin properties if (contentType is IMemberType memberType) { - // ensure that the group exists (ok if it already exists) - memberType.AddPropertyGroup(Constants.Conventions.Member.StandardPropertiesGroupAlias, - Constants.Conventions.Member.StandardPropertiesGroupName); - // ensure that property types exist (ok if they already exist) foreach ((var alias, PropertyType propertyType) in builtinProperties) { + var added = memberType.AddPropertyType(propertyType, Constants.Conventions.Member.StandardPropertiesGroupAlias, Constants.Conventions.Member.StandardPropertiesGroupName); diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentTypeRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentTypeRepository.cs index 6ab97c971f..9b0fe45c79 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentTypeRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentTypeRepository.cs @@ -297,15 +297,17 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement private void PersistHistoryCleanup(IContentType entity) { + // historyCleanup property is not mandatory for api endpoint, handle the case where it's not present. + // DocumentTypeSave doesn't handle this for us like ContentType constructors do. if (entity is IContentTypeWithHistoryCleanup entityWithHistoryCleanup) { ContentVersionCleanupPolicyDto dto = new ContentVersionCleanupPolicyDto() { ContentTypeId = entity.Id, Updated = DateTime.Now, - PreventCleanup = entityWithHistoryCleanup.HistoryCleanup.PreventCleanup, - KeepAllVersionsNewerThanDays = entityWithHistoryCleanup.HistoryCleanup.KeepAllVersionsNewerThanDays, - KeepLatestVersionPerDayForDays = entityWithHistoryCleanup.HistoryCleanup.KeepLatestVersionPerDayForDays + PreventCleanup = entityWithHistoryCleanup.HistoryCleanup?.PreventCleanup ?? false, + KeepAllVersionsNewerThanDays = entityWithHistoryCleanup.HistoryCleanup?.KeepAllVersionsNewerThanDays, + KeepLatestVersionPerDayForDays = entityWithHistoryCleanup.HistoryCleanup?.KeepLatestVersionPerDayForDays }; Database.InsertOrUpdate(dto); } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MemberRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MemberRepository.cs index cf382db164..a1b15f407d 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MemberRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MemberRepository.cs @@ -775,6 +775,9 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement { memberDto.PasswordConfig = DefaultPasswordConfigJson; changedCols.Add("passwordConfig"); + }else if (memberDto.PasswordConfig == Constants.Security.UnknownPasswordConfigJson) + { + changedCols.Add("passwordConfig"); } // do NOT update the password if it has not changed or if it is null or empty diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MemberTypeRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MemberTypeRepository.cs index e355964de6..ca3edea81c 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MemberTypeRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MemberTypeRepository.cs @@ -146,7 +146,6 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement } //By Convention we add 9 standard PropertyTypes to an Umbraco MemberType - entity.AddPropertyGroup(Cms.Core.Constants.Conventions.Member.StandardPropertiesGroupAlias, Cms.Core.Constants.Conventions.Member.StandardPropertiesGroupName); var standardPropertyTypes = ConventionsHelper.GetStandardPropertyTypeStubs(_shortStringHelper); foreach (var standardPropertyType in standardPropertyTypes) { diff --git a/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorPropertyEditor.cs b/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorPropertyEditor.cs index 93e7d5be50..b4b46f2f7c 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorPropertyEditor.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorPropertyEditor.cs @@ -128,52 +128,58 @@ namespace Umbraco.Cms.Core.PropertyEditors if (blockEditorData == null) return string.Empty; - foreach (var row in blockEditorData.BlockValue.ContentData) + void MapBlockItemData(List items) { - foreach (var prop in row.PropertyValues) + foreach (var row in items) { - // create a temp property with the value - // - force it to be culture invariant as the block editor can't handle culture variant element properties - prop.Value.PropertyType.Variations = ContentVariation.Nothing; - var tempProp = new Property(prop.Value.PropertyType); - tempProp.SetValue(prop.Value.Value); - - var propEditor = _propertyEditors[prop.Value.PropertyType.PropertyEditorAlias]; - if (propEditor == null) + foreach (var prop in row.PropertyValues) { - // NOTE: This logic was borrowed from Nested Content and I'm unsure why it exists. - // if the property editor doesn't exist I think everything will break anyways? + // create a temp property with the value + // - force it to be culture invariant as the block editor can't handle culture variant element properties + prop.Value.PropertyType.Variations = ContentVariation.Nothing; + var tempProp = new Property(prop.Value.PropertyType); + tempProp.SetValue(prop.Value.Value); + + var propEditor = _propertyEditors[prop.Value.PropertyType.PropertyEditorAlias]; + if (propEditor == null) + { + // NOTE: This logic was borrowed from Nested Content and I'm unsure why it exists. + // if the property editor doesn't exist I think everything will break anyways? + // update the raw value since this is what will get serialized out + row.RawPropertyValues[prop.Key] = tempProp.GetValue()?.ToString(); + continue; + } + + var dataType = _dataTypeService.GetDataType(prop.Value.PropertyType.DataTypeId); + if (dataType == null) + { + // deal with weird situations by ignoring them (no comment) + row.PropertyValues.Remove(prop.Key); + _logger.LogWarning( + "ToEditor removed property value {PropertyKey} in row {RowId} for property type {PropertyTypeAlias}", + prop.Key, row.Key, property.PropertyType.Alias); + continue; + } + + if (!valEditors.TryGetValue(dataType.Id, out var valEditor)) + { + var tempConfig = dataType.Configuration; + valEditor = propEditor.GetValueEditor(tempConfig); + + valEditors.Add(dataType.Id, valEditor); + } + + var convValue = valEditor.ToEditor(tempProp); + // update the raw value since this is what will get serialized out - row.RawPropertyValues[prop.Key] = tempProp.GetValue()?.ToString(); - continue; + row.RawPropertyValues[prop.Key] = convValue; } - - var dataType = _dataTypeService.GetDataType(prop.Value.PropertyType.DataTypeId); - if (dataType == null) - { - // deal with weird situations by ignoring them (no comment) - row.PropertyValues.Remove(prop.Key); - _logger.LogWarning( - "ToEditor removed property value {PropertyKey} in row {RowId} for property type {PropertyTypeAlias}", - prop.Key, row.Key, property.PropertyType.Alias); - continue; - } - - if (!valEditors.TryGetValue(dataType.Id, out var valEditor)) - { - var tempConfig = dataType.Configuration; - valEditor = propEditor.GetValueEditor(tempConfig); - - valEditors.Add(dataType.Id, valEditor); - } - - var convValue = valEditor.ToEditor(tempProp); - - // update the raw value since this is what will get serialized out - row.RawPropertyValues[prop.Key] = convValue; } } + MapBlockItemData(blockEditorData.BlockValue.ContentData); + MapBlockItemData(blockEditorData.BlockValue.SettingsData); + // return json convertable object return blockEditorData.BlockValue; } @@ -203,28 +209,34 @@ namespace Umbraco.Cms.Core.PropertyEditors if (blockEditorData == null || blockEditorData.BlockValue.ContentData.Count == 0) return string.Empty; - foreach (var row in blockEditorData.BlockValue.ContentData) + void MapBlockItemData(List items) { - foreach (var prop in row.PropertyValues) + foreach (var row in items) { - // Fetch the property types prevalue - var propConfiguration = _dataTypeService.GetDataType(prop.Value.PropertyType.DataTypeId).Configuration; + foreach (var prop in row.PropertyValues) + { + // Fetch the property types prevalue + var propConfiguration = _dataTypeService.GetDataType(prop.Value.PropertyType.DataTypeId).Configuration; - // Lookup the property editor - var propEditor = _propertyEditors[prop.Value.PropertyType.PropertyEditorAlias]; - if (propEditor == null) continue; + // Lookup the property editor + var propEditor = _propertyEditors[prop.Value.PropertyType.PropertyEditorAlias]; + if (propEditor == null) continue; - // Create a fake content property data object - var contentPropData = new ContentPropertyData(prop.Value.Value, propConfiguration); + // Create a fake content property data object + var contentPropData = new ContentPropertyData(prop.Value.Value, propConfiguration); - // Get the property editor to do it's conversion - var newValue = propEditor.GetValueEditor().FromEditor(contentPropData, prop.Value.Value); + // Get the property editor to do it's conversion + var newValue = propEditor.GetValueEditor().FromEditor(contentPropData, prop.Value.Value); - // update the raw value since this is what will get serialized out - row.RawPropertyValues[prop.Key] = newValue; + // update the raw value since this is what will get serialized out + row.RawPropertyValues[prop.Key] = newValue; + } } } + MapBlockItemData(blockEditorData.BlockValue.ContentData); + MapBlockItemData(blockEditorData.BlockValue.SettingsData); + // return json return JsonConvert.SerializeObject(blockEditorData.BlockValue); } diff --git a/src/Umbraco.Infrastructure/Security/MemberPasswordHasher.cs b/src/Umbraco.Infrastructure/Security/MemberPasswordHasher.cs index 2d2cad1624..e470bf0a6c 100644 --- a/src/Umbraco.Infrastructure/Security/MemberPasswordHasher.cs +++ b/src/Umbraco.Infrastructure/Security/MemberPasswordHasher.cs @@ -1,9 +1,15 @@ using System; +using System.IO; +using System.Linq; +using System.Security.Cryptography; +using System.Text; using Microsoft.AspNetCore.Identity; -using Umbraco.Cms.Core; -using Umbraco.Cms.Core.Models.Membership; -using Umbraco.Cms.Core.Security; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Serialization; +using Umbraco.Cms.Web.Common.DependencyInjection; using Umbraco.Extensions; namespace Umbraco.Cms.Core.Security @@ -16,9 +22,27 @@ namespace Umbraco.Cms.Core.Security /// public class MemberPasswordHasher : UmbracoPasswordHasher { + private readonly IOptions _legacyMachineKeySettings; + private readonly ILogger _logger; + + [Obsolete("Use ctor with all params")] public MemberPasswordHasher(LegacyPasswordSecurity legacyPasswordHasher, IJsonSerializer jsonSerializer) + : this(legacyPasswordHasher, + jsonSerializer, + StaticServiceProvider.Instance.GetRequiredService>(), + StaticServiceProvider.Instance.GetRequiredService>()) + { + } + + public MemberPasswordHasher( + LegacyPasswordSecurity legacyPasswordHasher, + IJsonSerializer jsonSerializer, + IOptions legacyMachineKeySettings, + ILogger logger) : base(legacyPasswordHasher, jsonSerializer) { + _legacyMachineKeySettings = legacyMachineKeySettings; + _logger = logger; } /// @@ -36,10 +60,21 @@ namespace Umbraco.Cms.Core.Security throw new ArgumentNullException(nameof(user)); } + var isPasswordAlgorithmKnown = user.PasswordConfig.IsNullOrWhiteSpace() == false && + user.PasswordConfig != Constants.Security.UnknownPasswordConfigJson; // if there's password config use the base implementation - if (!user.PasswordConfig.IsNullOrWhiteSpace()) + if (isPasswordAlgorithmKnown) { - return base.VerifyHashedPassword(user, hashedPassword, providedPassword); + var result = base.VerifyHashedPassword(user, hashedPassword, providedPassword); + if (result != PasswordVerificationResult.Failed) + { + return result; + } + } + // We need to check for clear text passwords from members as the first thing. This was possible in v8 :( + else if (IsSuccessfulLegacyPassword(hashedPassword, providedPassword)) + { + return PasswordVerificationResult.SuccessRehashNeeded; } // Else we need to detect what the password is. This will be the case @@ -66,7 +101,16 @@ namespace Umbraco.Cms.Core.Security return base.VerifyHashedPassword(user, hashedPassword, providedPassword); } - throw new InvalidOperationException("unable to determine member password hashing algorith"); + if (isPasswordAlgorithmKnown) + { + _logger.LogError("Unable to determine member password hashing algorithm"); + } + else + { + _logger.LogDebug("Unable to determine member password hashing algorithm, but this can happen when member enters a wrong password, before it has be rehashed"); + } + + return PasswordVerificationResult.Failed; } var isValid = LegacyPasswordSecurity.VerifyPassword( @@ -76,5 +120,65 @@ namespace Umbraco.Cms.Core.Security return isValid ? PasswordVerificationResult.SuccessRehashNeeded : PasswordVerificationResult.Failed; } + + private bool IsSuccessfulLegacyPassword(string hashedPassword, string providedPassword) + { + if (!string.IsNullOrEmpty(_legacyMachineKeySettings.Value.MachineKeyDecryptionKey)) + { + try + { + var decryptedPassword = DecryptLegacyPassword(hashedPassword, _legacyMachineKeySettings.Value.MachineKeyDecryption, _legacyMachineKeySettings.Value.MachineKeyDecryptionKey); + return decryptedPassword == providedPassword; + } + catch (Exception ex) + { + _logger.LogError(ex, "Could not decrypt password even that a DecryptionKey is provided. This means the DecryptionKey is wrong."); + return false; + } + } + + var result = LegacyPasswordSecurity.VerifyPassword(Constants.Security.AspNetUmbraco8PasswordHashAlgorithmName, providedPassword, hashedPassword); + return result || LegacyPasswordSecurity.VerifyPassword(Constants.Security.AspNetUmbraco4PasswordHashAlgorithmName, providedPassword, hashedPassword); + } + + private static string DecryptLegacyPassword(string encryptedPassword, string algorithmName, string decryptionKey) + { + SymmetricAlgorithm algorithm; + switch (algorithmName) + { + case "AES": + algorithm = new AesCryptoServiceProvider() + { + Key = StringToByteArray(decryptionKey), + IV = new byte[16] + }; + break; + default: + throw new NotSupportedException($"The algorithm ({algorithmName}) is not supported"); + } + + using (algorithm) + { + return DecryptLegacyPassword(encryptedPassword, algorithm); + } + } + + private static string DecryptLegacyPassword(string encryptedPassword, SymmetricAlgorithm algorithm) + { + using var memoryStream = new MemoryStream(); + ICryptoTransform cryptoTransform = algorithm.CreateDecryptor(); + var cryptoStream = new CryptoStream((Stream)memoryStream, cryptoTransform, CryptoStreamMode.Write); + var buf = Convert.FromBase64String(encryptedPassword); + cryptoStream.Write(buf, 0, 32); + cryptoStream.FlushFinalBlock(); + + return Encoding.Unicode.GetString(memoryStream.ToArray()); + } + + private static byte[] StringToByteArray(string hex) => + Enumerable.Range(0, hex.Length) + .Where(x => x % 2 == 0) + .Select(x => Convert.ToByte(hex.Substring(x, 2), 16)) + .ToArray(); } } diff --git a/src/Umbraco.Infrastructure/Security/MemberUserStore.cs b/src/Umbraco.Infrastructure/Security/MemberUserStore.cs index 086ff6a66c..d757cfb088 100644 --- a/src/Umbraco.Infrastructure/Security/MemberUserStore.cs +++ b/src/Umbraco.Infrastructure/Security/MemberUserStore.cs @@ -592,6 +592,12 @@ namespace Umbraco.Cms.Core.Security member.PasswordConfiguration = identityUser.PasswordConfig; } + if (member.PasswordConfiguration != identityUser.PasswordConfig) + { + changeType = MemberDataChangeType.FullSave; + member.PasswordConfiguration = identityUser.PasswordConfig; + } + if (member.SecurityStamp != identityUser.SecurityStamp) { changeType = MemberDataChangeType.FullSave; diff --git a/src/Umbraco.Infrastructure/Security/UmbracoPasswordHasher.cs b/src/Umbraco.Infrastructure/Security/UmbracoPasswordHasher.cs index 73d6d2b025..da08bc8713 100644 --- a/src/Umbraco.Infrastructure/Security/UmbracoPasswordHasher.cs +++ b/src/Umbraco.Infrastructure/Security/UmbracoPasswordHasher.cs @@ -53,6 +53,13 @@ namespace Umbraco.Cms.Core.Security if (LegacyPasswordSecurity.SupportHashAlgorithm(deserialized.HashAlgorithm)) { var result = LegacyPasswordSecurity.VerifyPassword(deserialized.HashAlgorithm, providedPassword, hashedPassword); + + //We need to special handle this case, apparently v8 still saves the user algorithm as {"hashAlgorithm":"HMACSHA256"}, when using legacy encoding and hasinging. + if (result == false) + { + result = LegacyPasswordSecurity.VerifyLegacyHashedPassword(providedPassword, hashedPassword); + } + return result ? PasswordVerificationResult.SuccessRehashNeeded : PasswordVerificationResult.Failed; diff --git a/src/Umbraco.Infrastructure/Security/UmbracoUserManager.cs b/src/Umbraco.Infrastructure/Security/UmbracoUserManager.cs index 733761c7ab..0fee166013 100644 --- a/src/Umbraco.Infrastructure/Security/UmbracoUserManager.cs +++ b/src/Umbraco.Infrastructure/Security/UmbracoUserManager.cs @@ -205,6 +205,8 @@ namespace Umbraco.Cms.Core.Security await lockoutStore.ResetAccessFailedCountAsync(user, CancellationToken.None); + //Ensure the password config is null, so it is set to the default in repository + user.PasswordConfig = null; return await UpdateAsync(user); } @@ -234,6 +236,11 @@ namespace Umbraco.Cms.Core.Security // here we are persisting the value for the back office } + if (string.IsNullOrEmpty(user.PasswordConfig)) + { + //We cant pass null as that would be interpreted as the default algoritm, but due to the failing attempt we dont know. + user.PasswordConfig = Constants.Security.UnknownPasswordConfigJson; + } IdentityResult result = await UpdateAsync(user); return result; } diff --git a/src/Umbraco.Infrastructure/Services/Implement/EntityXmlSerializer.cs b/src/Umbraco.Infrastructure/Services/Implement/EntityXmlSerializer.cs index d2ebb72bca..26060bf988 100644 --- a/src/Umbraco.Infrastructure/Services/Implement/EntityXmlSerializer.cs +++ b/src/Umbraco.Infrastructure/Services/Implement/EntityXmlSerializer.cs @@ -5,6 +5,7 @@ using System.Linq; using System.Net; using System.Xml.Linq; using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.ContentEditing; using Umbraco.Cms.Core.PropertyEditors; using Umbraco.Cms.Core.Serialization; using Umbraco.Cms.Core.Strings; @@ -494,6 +495,11 @@ namespace Umbraco.Cms.Core.Services.Implement genericProperties, tabs); + if (contentType is IContentTypeWithHistoryCleanup withCleanup && withCleanup.HistoryCleanup is not null) + { + xml.Add(SerializeCleanupPolicy(withCleanup.HistoryCleanup)); + } + var folderNames = string.Empty; var folderKeys = string.Empty; //don't add folders if this is a child doc type @@ -564,6 +570,29 @@ namespace Umbraco.Cms.Core.Services.Implement propertyType.ValidationRegExpMessage != null ? new XElement("ValidationRegExpMessage", propertyType.ValidationRegExpMessage) : null, propertyType.Description != null ? new XElement("Description", new XCData(propertyType.Description)) : null); + private XElement SerializeCleanupPolicy(HistoryCleanup cleanupPolicy) + { + if (cleanupPolicy == null) + { + throw new ArgumentNullException(nameof(cleanupPolicy)); + } + + var element = new XElement("HistoryCleanupPolicy", + new XAttribute("preventCleanup", cleanupPolicy.PreventCleanup)); + + if (cleanupPolicy.KeepAllVersionsNewerThanDays.HasValue) + { + element.Add(new XAttribute("keepAllVersionsNewerThanDays", cleanupPolicy.KeepAllVersionsNewerThanDays)); + } + + if (cleanupPolicy.KeepLatestVersionPerDayForDays.HasValue) + { + element.Add(new XAttribute("keepLatestVersionPerDayForDays", cleanupPolicy.KeepLatestVersionPerDayForDays)); + } + + return element; + } + // exports an IContentBase (IContent, IMedia or IMember) as an XElement. private XElement SerializeContentBase(IContentBase contentBase, string urlValue, string nodeName, bool published) { diff --git a/src/Umbraco.Web.BackOffice/Controllers/EntityController.cs b/src/Umbraco.Web.BackOffice/Controllers/EntityController.cs index c3f36e92cb..408ae224ab 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/EntityController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/EntityController.cs @@ -54,6 +54,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers [ParameterSwapControllerActionSelector(nameof(GetById), "id", typeof(int), typeof(Guid), typeof(Udi))] [ParameterSwapControllerActionSelector(nameof(GetByIds), "ids", typeof(int[]), typeof(Guid[]), typeof(Udi[]))] [ParameterSwapControllerActionSelector(nameof(GetUrl), "id", typeof(int), typeof(Udi))] + [ParameterSwapControllerActionSelector(nameof(GetUrlsByIds), "ids", typeof(int[]), typeof(Guid[]), typeof(Udi[]))] public class EntityController : UmbracoAuthorizedJsonController { private static readonly string[] _postFilterSplitStrings = { "=", "==", "!=", "<>", ">", "<", ">=", "<=" }; @@ -318,6 +319,145 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers return GetUrl(intId.Result, entityType, culture); } + /// + /// Get entity URLs by IDs + /// + /// + /// A list of IDs to lookup items by + /// + /// The entity type to look for. + /// The culture to fetch the URL for. + /// Dictionary mapping Udi -> Url + /// + /// We allow for POST because there could be quite a lot of Ids. + /// + [HttpGet] + [HttpPost] + public IDictionary GetUrlsByIds([FromJsonPath] int[] ids, [FromQuery] UmbracoEntityTypes type, [FromQuery] string culture = null) + { + if (ids == null || !ids.Any()) + { + return new Dictionary(); + } + + string MediaOrDocumentUrl(int id) + { + switch (type) + { + case UmbracoEntityTypes.Document: + return _publishedUrlProvider.GetUrl(id, culture: culture ?? ClientCulture()); + + case UmbracoEntityTypes.Media: + { + 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); + } + + default: + return null; + } + } + + return ids + .Distinct() + .Select(id => new { + Id = id, + Url = MediaOrDocumentUrl(id) + }).ToDictionary(x => x.Id, x => x.Url); + } + + /// + /// Get entity URLs by IDs + /// + /// + /// A list of IDs to lookup items by + /// + /// The entity type to look for. + /// The culture to fetch the URL for. + /// Dictionary mapping Udi -> Url + /// + /// We allow for POST because there could be quite a lot of Ids. + /// + [HttpGet] + [HttpPost] + public IDictionary GetUrlsByIds([FromJsonPath] Guid[] ids, [FromQuery] UmbracoEntityTypes type, [FromQuery] string culture = null) + { + if (ids == null || !ids.Any()) + { + return new Dictionary(); + } + + string MediaOrDocumentUrl(Guid id) + { + return type switch + { + UmbracoEntityTypes.Document => _publishedUrlProvider.GetUrl(id, culture: culture ?? ClientCulture()), + + // NOTE: If culture is passed here we get an empty string rather than a media item URL. + UmbracoEntityTypes.Media => _publishedUrlProvider.GetMediaUrl(id, culture: null), + + _ => null + }; + } + + return ids + .Distinct() + .Select(id => new { + Id = id, + Url = MediaOrDocumentUrl(id) + }).ToDictionary(x => x.Id, x => x.Url); + } + + /// + /// Get entity URLs by IDs + /// + /// + /// A list of IDs to lookup items by + /// + /// The entity type to look for. + /// The culture to fetch the URL for. + /// Dictionary mapping Udi -> Url + /// + /// We allow for POST because there could be quite a lot of Ids. + /// + [HttpGet] + [HttpPost] + public IDictionary GetUrlsByIds([FromJsonPath] Udi[] ids, [FromQuery] UmbracoEntityTypes type, [FromQuery] string culture = null) + { + if (ids == null || !ids.Any()) + { + 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) + { + if (id is not GuidUdi guidUdi) + { + return null; + } + + return type switch + { + UmbracoEntityTypes.Document => _publishedUrlProvider.GetUrl(guidUdi.Guid, culture: culture ?? ClientCulture()), + + // NOTE: If culture is passed here we get an empty string rather than a media item URL. + UmbracoEntityTypes.Media => _publishedUrlProvider.GetMediaUrl(guidUdi.Guid, culture: null), + + _ => null + }; + } + + return ids + .Distinct() + .Select(id => new { + Id = id, + Url = MediaOrDocumentUrl(id) + }).ToDictionary(x => x.Id, x => x.Url); + } + /// /// Get entity URLs by UDIs /// @@ -331,33 +471,31 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers /// [HttpGet] [HttpPost] + [Obsolete("Use GetUrlsByIds instead.")] public IDictionary GetUrlsByUdis([FromJsonPath] Udi[] udis, string culture = null) { - if (udis == null || udis.Length == 0) + if (udis == null || !udis.Any()) { return new Dictionary(); } - // TODO: PMJ 2021-09-27 - Should GetUrl(Udi) exist as an extension method on UrlProvider/IUrlProvider (in v9) - string MediaOrDocumentUrl(Udi udi) - { - if (udi is not GuidUdi guidUdi) - { - return null; - } + var udiEntityType = udis.First().EntityType; + UmbracoEntityTypes entityType; - return guidUdi.EntityType switch - { - Constants.UdiEntityType.Document => _publishedUrlProvider.GetUrl(guidUdi.Guid, - culture: culture ?? ClientCulture()), - // NOTE: If culture is passed here we get an empty string rather than a media item URL WAT - Constants.UdiEntityType.Media => _publishedUrlProvider.GetMediaUrl(guidUdi.Guid, culture: null), - _ => null - }; + switch (udiEntityType) + { + case Constants.UdiEntityType.Document: + entityType = UmbracoEntityTypes.Document; + break; + case Constants.UdiEntityType.Media: + entityType = UmbracoEntityTypes.Media; + break; + default: + entityType = (UmbracoEntityTypes)(-1); + break; } - return udis - .Select(udi => new { Udi = udi, Url = MediaOrDocumentUrl(udi) }).ToDictionary(x => x.Udi, x => x.Url); + return GetUrlsByIds(udis, entityType, culture); } /// diff --git a/src/Umbraco.Web.BackOffice/Controllers/LogViewerController.cs b/src/Umbraco.Web.BackOffice/Controllers/LogViewerController.cs index 9fcf407581..03cc427ec1 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/LogViewerController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/LogViewerController.cs @@ -1,12 +1,16 @@ using System; using System.Collections.Generic; +using System.Collections.ObjectModel; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.DependencyInjection; +using Serilog.Events; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Logging.Viewer; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Web.Common.Attributes; using Umbraco.Cms.Web.Common.Authorization; +using Umbraco.Cms.Web.Common.DependencyInjection; using Constants = Umbraco.Cms.Core.Constants; namespace Umbraco.Cms.Web.BackOffice.Controllers @@ -20,10 +24,19 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers public class LogViewerController : BackOfficeNotificationsController { private readonly ILogViewer _logViewer; + private readonly ILogLevelLoader _logLevelLoader; + [Obsolete] public LogViewerController(ILogViewer logViewer) + : this(logViewer, StaticServiceProvider.Instance.GetRequiredService()) + { + + } + + public LogViewerController(ILogViewer logViewer, ILogLevelLoader logLevelLoader) { _logViewer = logViewer ?? throw new ArgumentNullException(nameof(logViewer)); + _logLevelLoader = logLevelLoader ?? throw new ArgumentNullException(nameof(logLevelLoader)); } private bool CanViewLogs(LogTimePeriod logTimePeriod) @@ -135,6 +148,13 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers return _logViewer.DeleteSavedSearch(item.Name, item.Query); } + [HttpGet] + public ReadOnlyDictionary GetLogLevels() + { + return _logLevelLoader.GetLogLevelsFromSinks(); + } + + [Obsolete("Please use GetLogLevels() instead. Scheduled for removal in V11.")] [HttpGet] public string GetLogLevel() { diff --git a/src/Umbraco.Web.BackOffice/Controllers/ParameterSwapControllerActionSelectorAttribute.cs b/src/Umbraco.Web.BackOffice/Controllers/ParameterSwapControllerActionSelectorAttribute.cs index ac0a7fea84..3fafd2c1e4 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/ParameterSwapControllerActionSelectorAttribute.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/ParameterSwapControllerActionSelectorAttribute.cs @@ -1,22 +1,48 @@ -using System; +using System; using System.Linq; using System.Net.Http; using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc.ActionConstraints; using Microsoft.AspNetCore.Mvc.Controllers; using Newtonsoft.Json; using Newtonsoft.Json.Linq; +using Umbraco.Cms.Core; using Umbraco.Extensions; namespace Umbraco.Cms.Web.BackOffice.Controllers { + /// + /// + /// This attribute is odd because it applies at class level where some methods may use it whilst others don't. + /// + /// + /// + /// What we should probably have (if we really even need something like this at all) is an attribute for method level. + /// + /// + /// + /// + /// [HasParameterFromUriOrBodyOfType("ids", typeof(Guid[]))] + /// public IActionResult GetByIds([FromJsonPath] Guid[] ids) { } + /// + /// [HasParameterFromUriOrBodyOfType("ids", typeof(int[]))] + /// public IActionResult GetByIds([FromJsonPath] int[] ids) { } + /// + /// + /// + /// + /// + /// That way we wouldn't need confusing things like Accept returning true when action name doesn't even match attribute metadata. + /// + /// [AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = true)] internal class ParameterSwapControllerActionSelectorAttribute : Attribute, IActionConstraint { + private readonly string _actionName; private readonly string _parameterName; private readonly Type[] _supportedTypes; - private string _requestBody; public ParameterSwapControllerActionSelectorAttribute(string actionName, string parameterName, params Type[] supportedTypes) { @@ -33,10 +59,12 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers { if (!IsValidCandidate(context.CurrentCandidate)) { + // See remarks on class, required because we apply at class level + // and some controllers have some actions with parameter swaps and others without. return true; } - var chosenCandidate = SelectAction(context); + ActionSelectorCandidate? chosenCandidate = SelectAction(context); var found = context.CurrentCandidate.Equals(chosenCandidate); return found; @@ -49,23 +77,45 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers return candidate; } + HttpContext httpContext = context.RouteContext.HttpContext; + // if it's a post we can try to read from the body and bind from the json value - if (context.RouteContext.HttpContext.Request.Method == HttpMethod.Post.ToString()) + if (context.RouteContext.HttpContext.Request.Method.Equals(HttpMethod.Post.Method)) { - // We need to use the asynchronous method here if synchronous IO is not allowed (it may or may not be, depending - // on configuration in UmbracoBackOfficeServiceCollectionExtensions.AddUmbraco()). - // We can't use async/await due to the need to override IsValidForRequest, which doesn't have an async override, so going with - // this, which seems to be the least worst option for "sync to async" (https://stackoverflow.com/a/32429753/489433). - var strJson = _requestBody ??= Task.Run(() => context.RouteContext.HttpContext.Request.GetRawBodyStringAsync()).GetAwaiter().GetResult(); + JObject postBodyJson; - var json = JsonConvert.DeserializeObject(strJson); + if (httpContext.Items.TryGetValue(Constants.HttpContext.Items.RequestBodyAsJObject, out var value) && value is JObject cached) + { + postBodyJson = cached; + } + else + { + // We need to use the asynchronous method here if synchronous IO is not allowed (it may or may not be, depending + // on configuration in UmbracoBackOfficeServiceCollectionExtensions.AddUmbraco()). + // We can't use async/await due to the need to override IsValidForRequest, which doesn't have an async override, so going with + // this, which seems to be the least worst option for "sync to async" (https://stackoverflow.com/a/32429753/489433). + // + // To expand on the above, if KestrelServerOptions/IISServerOptions is AllowSynchronousIO=false + // And you attempt to read stream sync an InvalidOperationException is thrown with message + // "Synchronous operations are disallowed. Call ReadAsync or set AllowSynchronousIO to true instead." + var rawBody = Task.Run(() => httpContext.Request.GetRawBodyStringAsync()).GetAwaiter().GetResult(); + try + { + postBodyJson = JsonConvert.DeserializeObject(rawBody); + httpContext.Items[Constants.HttpContext.Items.RequestBodyAsJObject] = postBodyJson; + } + catch (JsonException) + { + postBodyJson = null; + } + } - if (json == null) + if (postBodyJson == null) { return null; } - var requestParam = json[_parameterName]; + var requestParam = postBodyJson[_parameterName]; if (requestParam != null) { @@ -85,11 +135,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers } } } - catch (JsonReaderException) - { - // can't convert - } - catch (JsonSerializationException) + catch (JsonException) { // can't convert } diff --git a/src/Umbraco.Web.BackOffice/EmbeddedResources/Tours/getting-started.json b/src/Umbraco.Web.BackOffice/EmbeddedResources/Tours/getting-started.json index 10817d7ef0..837682c896 100644 --- a/src/Umbraco.Web.BackOffice/EmbeddedResources/Tours/getting-started.json +++ b/src/Umbraco.Web.BackOffice/EmbeddedResources/Tours/getting-started.json @@ -50,6 +50,14 @@ "content": "Each area in Umbraco is called a Section. Right now you are in the Content section, when you want to go to another section simply click on the appropriate name in the main menu and you'll be there in no time.", "backdropOpacity": 0.6 }, + { + "element": "[data-element='section-content']", + "skipStepIfVisible": "[data-element='dashboard']", + "title": "Content section", + "content": "Try clicking Content to enter the content section.", + "event": "click", + "backdropOpacity": 0.6 + }, { "element": "#tree", "elementPreventClick": true, diff --git a/src/Umbraco.Web.BackOffice/Install/InstallController.cs b/src/Umbraco.Web.BackOffice/Install/InstallController.cs index ca29a508c4..17aba2b27e 100644 --- a/src/Umbraco.Web.BackOffice/Install/InstallController.cs +++ b/src/Umbraco.Web.BackOffice/Install/InstallController.cs @@ -13,6 +13,7 @@ using Umbraco.Cms.Core.Security; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.WebAssets; using Umbraco.Cms.Infrastructure.Install; +using Umbraco.Cms.Web.Common.Attributes; using Umbraco.Cms.Web.Common.Filters; using Umbraco.Extensions; @@ -71,9 +72,6 @@ namespace Umbraco.Cms.Web.BackOffice.Install // TODO: Update for package migrations if (_runtime.Level == RuntimeLevel.Upgrade) { - // Update ClientDependency version and delete its temp directories to make sure we get fresh caches - _runtimeMinifier.Reset(); - var authResult = await this.AuthenticateBackOfficeAsync(); if (!authResult.Succeeded) @@ -101,6 +99,7 @@ namespace Umbraco.Cms.Web.BackOffice.Install /// /// [HttpGet] + [IgnoreFromNotFoundSelectorPolicy] public ActionResult Redirect() { var uri = HttpContext.Request.GetEncodedUrl(); diff --git a/src/Umbraco.Web.BackOffice/Mapping/MemberMapDefinition.cs b/src/Umbraco.Web.BackOffice/Mapping/MemberMapDefinition.cs index a5813a2e8e..f0647b9efb 100644 --- a/src/Umbraco.Web.BackOffice/Mapping/MemberMapDefinition.cs +++ b/src/Umbraco.Web.BackOffice/Mapping/MemberMapDefinition.cs @@ -47,7 +47,6 @@ namespace Umbraco.Cms.Web.BackOffice.Mapping target.ContentTypeAlias = source.ContentType.Alias; target.ContentTypeName = source.ContentType.Name; target.CreateDate = source.CreateDate; - target.Email = source.Email; target.Icon = source.ContentType.Icon; target.Id = source.Id; target.Key = source.Key; @@ -61,7 +60,11 @@ namespace Umbraco.Cms.Web.BackOffice.Mapping target.TreeNodeUrl = _commonTreeNodeMapper.GetTreeNodeUrl(source); target.Udi = Udi.Create(Constants.UdiEntityType.Member, source.Key); target.UpdateDate = source.UpdateDate; + + //Membership target.Username = source.Username; + target.Email = source.Email; + target.MembershipProperties = _tabsAndPropertiesMapper.MapMembershipProperties(source, context); } // Umbraco.Code.MapAll -Trashed -Edited -Updater -Alias -VariesByCulture diff --git a/src/Umbraco.Web.BackOffice/ModelBinders/FromJsonPathAttribute.cs b/src/Umbraco.Web.BackOffice/ModelBinders/FromJsonPathAttribute.cs index a27243714f..f53fbd0b43 100644 --- a/src/Umbraco.Web.BackOffice/ModelBinders/FromJsonPathAttribute.cs +++ b/src/Umbraco.Web.BackOffice/ModelBinders/FromJsonPathAttribute.cs @@ -5,6 +5,7 @@ using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http; using Newtonsoft.Json; using Newtonsoft.Json.Linq; +using Umbraco.Cms.Core; using Umbraco.Extensions; namespace Umbraco.Cms.Web.BackOffice.ModelBinders @@ -36,6 +37,10 @@ namespace Umbraco.Cms.Web.BackOffice.ModelBinders return; } + if (TryModelBindFromHttpContextItems(bindingContext)) + { + return; + } var strJson = await bindingContext.HttpContext.Request.GetRawBodyStringAsync(); @@ -60,6 +65,30 @@ namespace Umbraco.Cms.Web.BackOffice.ModelBinders bindingContext.Result = ModelBindingResult.Success(model); } + public static bool TryModelBindFromHttpContextItems(ModelBindingContext bindingContext) + { + const string key = Constants.HttpContext.Items.RequestBodyAsJObject; + + if (!bindingContext.HttpContext.Items.TryGetValue(key, out var cached)) + { + return false; + } + + if (cached is not JObject json) + { + return false; + } + + JToken match = json.SelectToken(bindingContext.FieldName); + + // ReSharper disable once InvertIf + if (match != null) + { + bindingContext.Result = ModelBindingResult.Success(match.ToObject(bindingContext.ModelType)); + } + + return true; + } } } } diff --git a/src/Umbraco.Web.BackOffice/Trees/ContentTreeController.cs b/src/Umbraco.Web.BackOffice/Trees/ContentTreeController.cs index 74c456026d..3dd303d8f7 100644 --- a/src/Umbraco.Web.BackOffice/Trees/ContentTreeController.cs +++ b/src/Umbraco.Web.BackOffice/Trees/ContentTreeController.cs @@ -282,12 +282,7 @@ namespace Umbraco.Cms.Web.BackOffice.Trees if (_emailSender.CanSendRequiredEmail()) { - menu.Items.Add(new MenuItem("notify", LocalizedTextService) - { - Icon = "megaphone", - SeparatorBefore = true, - OpensDialog = true - }); + AddActionNode(item, menu, true, opensDialog: true); } if((item is DocumentEntitySlim documentEntity && documentEntity.IsContainer) == false) diff --git a/src/Umbraco.Web.Common/Attributes/IgnoreFromNotFoundSelectorPolicyAttribute.cs b/src/Umbraco.Web.Common/Attributes/IgnoreFromNotFoundSelectorPolicyAttribute.cs new file mode 100644 index 0000000000..8fad2b6c6b --- /dev/null +++ b/src/Umbraco.Web.Common/Attributes/IgnoreFromNotFoundSelectorPolicyAttribute.cs @@ -0,0 +1,13 @@ +using System; + +namespace Umbraco.Cms.Web.Common.Attributes +{ + /// + /// When applied to an api controller it will be routed to the /Umbraco/BackOffice prefix route so we can determine if it + /// is a back office route or not. + /// + [AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = true)] + public sealed class IgnoreFromNotFoundSelectorPolicyAttribute : Attribute + { + } +} diff --git a/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilder.ImageSharp.cs b/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilder.ImageSharp.cs index 6755159fc1..4d621d348c 100644 --- a/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilder.ImageSharp.cs +++ b/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilder.ImageSharp.cs @@ -1,8 +1,10 @@ using System.Collections.Generic; using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; +using Microsoft.Net.Http.Headers; using SixLabors.ImageSharp.Web.Caching; using SixLabors.ImageSharp.Web.Commands; using SixLabors.ImageSharp.Web.DependencyInjection; @@ -27,7 +29,7 @@ namespace Umbraco.Extensions builder.Services.AddImageSharp(options => { - // The configuration is set using ImageSharpConfigurationOptions + // options.Configuration is set using ImageSharpConfigurationOptions below options.BrowserMaxAge = imagingSettings.Cache.BrowserMaxAge; options.CacheMaxAge = imagingSettings.Cache.CacheMaxAge; options.CachedNameLength = imagingSettings.Cache.CachedNameLength; @@ -48,20 +50,29 @@ namespace Umbraco.Extensions context.Commands.Remove(ResizeWebProcessor.Height); } + return Task.CompletedTask; + }; + options.OnBeforeSaveAsync = _ => Task.CompletedTask; + options.OnProcessedAsync = _ => Task.CompletedTask; + options.OnPrepareResponseAsync = context => + { + // Change Cache-Control header when cache buster value is present + if (context.Request.Query.ContainsKey("rnd")) + { + var headers = context.Response.GetTypedHeaders(); + + var cacheControl = headers.CacheControl; + cacheControl.MustRevalidate = false; + cacheControl.Extensions.Add(new NameValueHeaderValue("immutable")); + + headers.CacheControl = cacheControl; + } + return Task.CompletedTask; }; }) .Configure(options => options.CacheFolder = builder.BuilderHostingEnvironment.MapPathContentRoot(imagingSettings.Cache.CacheFolder)) - // We need to add CropWebProcessor before ResizeWebProcessor (until https://github.com/SixLabors/ImageSharp.Web/issues/182 is fixed) - .RemoveProcessor() - .RemoveProcessor() - .RemoveProcessor() - .RemoveProcessor() - .AddProcessor() - .AddProcessor() - .AddProcessor() - .AddProcessor() - .AddProcessor(); + .AddProcessor(); builder.Services.AddTransient, ImageSharpConfigurationOptions>(); diff --git a/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilderExtensions.cs b/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilderExtensions.cs index fda4d7ca32..97a9f3e087 100644 --- a/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilderExtensions.cs +++ b/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilderExtensions.cs @@ -18,6 +18,7 @@ using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Serilog; using Smidge; +using Smidge.Cache; using Smidge.FileProcessors; using Smidge.InMemory; using Smidge.Nuglify; @@ -27,7 +28,6 @@ using Umbraco.Cms.Core.Composing; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.Diagnostics; -using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Hosting; using Umbraco.Cms.Core.Logging; using Umbraco.Cms.Core.Macros; @@ -35,6 +35,7 @@ using Umbraco.Cms.Core.Net; using Umbraco.Cms.Core.Notifications; using Umbraco.Cms.Core.Security; using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Telemetry; using Umbraco.Cms.Core.Templates; using Umbraco.Cms.Core.Web; using Umbraco.Cms.Core.WebAssets; @@ -181,7 +182,10 @@ namespace Umbraco.Extensions builder.Services.AddHostedService(); builder.Services.AddHostedService(); builder.Services.AddHostedService(); - builder.Services.AddHostedService(); + builder.Services.AddHostedService(provider => + new ReportSiteTask( + provider.GetRequiredService>(), + provider.GetRequiredService())); return builder; } @@ -274,7 +278,10 @@ namespace Umbraco.Extensions new[] { "/App_Plugins/**/*.js", "/App_Plugins/**/*.css" })); }); + builder.Services.AddUnique(); builder.Services.AddSmidge(builder.Config.GetSection(Constants.Configuration.ConfigRuntimeMinification)); + // Replace the Smidge request helper, in order to discourage the use of brotli since it's super slow + builder.Services.AddUnique(); builder.Services.AddSmidgeNuglify(); builder.Services.AddSmidgeInMemory(false); // it will be enabled based on config/cachebuster diff --git a/src/Umbraco.Web.Common/Extensions/ServiceCollectionExtensions.cs b/src/Umbraco.Web.Common/Extensions/ServiceCollectionExtensions.cs index df7ffd4328..4441ae6e42 100644 --- a/src/Umbraco.Web.Common/Extensions/ServiceCollectionExtensions.cs +++ b/src/Umbraco.Web.Common/Extensions/ServiceCollectionExtensions.cs @@ -1,3 +1,4 @@ +using System; using System.IO; using System.Reflection; using Microsoft.AspNetCore.Hosting; @@ -29,7 +30,8 @@ namespace Umbraco.Extensions IConfiguration configuration) { // Create a serilog logger - var logger = SerilogLogger.CreateWithDefaultConfiguration(hostingEnvironment, loggingConfiguration, configuration); + var logger = SerilogLogger.CreateWithDefaultConfiguration(hostingEnvironment, loggingConfiguration, configuration, out var umbracoFileConfig); + services.AddSingleton(umbracoFileConfig); // This is nessasary to pick up all the loggins to MS ILogger. Log.Logger = logger.SerilogLog; diff --git a/src/Umbraco.Web.Common/RuntimeMinification/SmidgeRequestHelper.cs b/src/Umbraco.Web.Common/RuntimeMinification/SmidgeRequestHelper.cs new file mode 100644 index 0000000000..4313e0e359 --- /dev/null +++ b/src/Umbraco.Web.Common/RuntimeMinification/SmidgeRequestHelper.cs @@ -0,0 +1,78 @@ +using System.Collections.Generic; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Primitives; +using Microsoft.Net.Http.Headers; +using Smidge; +using Smidge.Models; + +namespace Umbraco.Cms.Web.Common.RuntimeMinification +{ + public class SmidgeRequestHelper : IRequestHelper + { + private RequestHelper _wrappedRequestHelper; + + public SmidgeRequestHelper(IWebsiteInfo siteInfo) + { + _wrappedRequestHelper = new RequestHelper(siteInfo); + } + + /// + public string Content(string path) => _wrappedRequestHelper.Content(path); + + /// + public string Content(IWebFile file) => _wrappedRequestHelper.Content(file); + + /// + public bool IsExternalRequestPath(string path) => _wrappedRequestHelper.IsExternalRequestPath(path); + + /// + /// Overrides the default order of compression from Smidge, since Brotli is super slow (~10 seconds for backoffice.js) + /// + /// + /// + public CompressionType GetClientCompression(IDictionary headers) + { + var type = CompressionType.None; + + if (headers is not IHeaderDictionary headerDictionary) + { + headerDictionary = new HeaderDictionary(headers.Count); + foreach ((var key, StringValues stringValues) in headers) + { + headerDictionary[key] = stringValues; + } + } + + var acceptEncoding = headerDictionary.GetCommaSeparatedValues(HeaderNames.AcceptEncoding); + if (acceptEncoding.Length > 0) + { + // Prefer in order: GZip, Deflate, Brotli. + for (var i = 0; i < acceptEncoding.Length; i++) + { + var encoding = acceptEncoding[i].Trim(); + + CompressionType parsed = CompressionType.Parse(encoding); + + // Not pack200-gzip. + if (parsed == CompressionType.GZip) + { + return CompressionType.GZip; + } + + if (parsed == CompressionType.Deflate) + { + type = CompressionType.Deflate; + } + + // Brotli is typically last in the accept encoding header. + if (type != CompressionType.Deflate && parsed == CompressionType.Brotli) + { + type = CompressionType.Brotli; + } + } + } + + return type; + } + } +} diff --git a/src/Umbraco.Web.Common/RuntimeMinification/SmidgeRuntimeMinifier.cs b/src/Umbraco.Web.Common/RuntimeMinification/SmidgeRuntimeMinifier.cs index 96188ba08c..6e24c06b8e 100644 --- a/src/Umbraco.Web.Common/RuntimeMinification/SmidgeRuntimeMinifier.cs +++ b/src/Umbraco.Web.Common/RuntimeMinification/SmidgeRuntimeMinifier.cs @@ -24,7 +24,6 @@ namespace Umbraco.Cms.Web.Common.RuntimeMinification private readonly IHostingEnvironment _hostingEnvironment; private readonly IConfigManipulator _configManipulator; private readonly CacheBusterResolver _cacheBusterResolver; - private readonly RuntimeMinificationSettings _runtimeMinificationSettings; private readonly IBundleManager _bundles; private readonly SmidgeHelperAccessor _smidge; @@ -53,7 +52,6 @@ namespace Umbraco.Cms.Web.Common.RuntimeMinification _hostingEnvironment = hostingEnvironment; _configManipulator = configManipulator; _cacheBusterResolver = cacheBusterResolver; - _runtimeMinificationSettings = runtimeMinificationSettings.Value; _jsMinPipeline = new Lazy(() => _bundles.PipelineFactory.Create(typeof(JsMinifier))); _cssMinPipeline = new Lazy(() => _bundles.PipelineFactory.Create(typeof(NuglifyCss))); @@ -76,10 +74,10 @@ namespace Umbraco.Cms.Web.Common.RuntimeMinification return defaultCss; }); - Type cacheBusterType = _runtimeMinificationSettings.CacheBuster switch + Type cacheBusterType = runtimeMinificationSettings.Value.CacheBuster switch { RuntimeMinificationCacheBuster.AppDomain => typeof(AppDomainLifetimeCacheBuster), - RuntimeMinificationCacheBuster.Version => typeof(ConfigCacheBuster), + RuntimeMinificationCacheBuster.Version => typeof(UmbracoSmidgeConfigCacheBuster), RuntimeMinificationCacheBuster.Timestamp => typeof(TimestampCacheBuster), _ => throw new NotImplementedException() }; @@ -169,18 +167,12 @@ namespace Umbraco.Cms.Web.Common.RuntimeMinification } } - /// - /// - /// Smidge uses the version number as cache buster (configurable). - /// We therefore can reset, by updating the version number in config - /// + [Obsolete("Invalidation is handled automatically. Scheduled for removal V11.")] public void Reset() { var version = DateTime.UtcNow.Ticks.ToString(); _configManipulator.SaveConfigValue(Cms.Core.Constants.Configuration.ConfigRuntimeMinificationVersion, version.ToString()); } - - } } diff --git a/src/Umbraco.Web.Common/RuntimeMinification/UmbracoSmidgeConfigCacheBuster.cs b/src/Umbraco.Web.Common/RuntimeMinification/UmbracoSmidgeConfigCacheBuster.cs new file mode 100644 index 0000000000..c323204148 --- /dev/null +++ b/src/Umbraco.Web.Common/RuntimeMinification/UmbracoSmidgeConfigCacheBuster.cs @@ -0,0 +1,77 @@ +using System; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Smidge; +using Smidge.Cache; +using Umbraco.Cms.Core.Configuration; +using Umbraco.Cms.Core.Configuration.Models; +using Umbraco.Extensions; + +namespace Umbraco.Cms.Web.Common.RuntimeMinification +{ + /// + /// Constructs a cache buster string with sensible defaults. + /// + /// + /// + /// Had planned on handling all of this in SmidgeRuntimeMinifier, but that only handles some urls. + /// + /// + /// A lot of the work is delegated e.g. to + /// which doesn't care about the cache buster string in our classes only the value returned by the resolved ICacheBuster. + /// + /// + /// Then I thought fine I'll just use a IConfigureOptions to tweak upstream , but that only cares about the + /// class we instantiate and pass through in + /// + /// + /// So here we are, create our own to ensure we cache bust in a reasonable fashion. + /// + ///

+ /// + /// Note that this class makes some other bits of code pretty redundant e.g. will + /// concatenate version with CacheBuster value and hash again, but there's no real harm so can think about that later. + /// + ///
+ internal class UmbracoSmidgeConfigCacheBuster : ICacheBuster + { + private readonly IOptions _runtimeMinificationSettings; + private readonly IUmbracoVersion _umbracoVersion; + private readonly IEntryAssemblyMetadata _entryAssemblyMetadata; + + private string _cacheBusterValue; + + public UmbracoSmidgeConfigCacheBuster( + IOptions runtimeMinificationSettings, + IUmbracoVersion umbracoVersion, + IEntryAssemblyMetadata entryAssemblyMetadata) + { + _runtimeMinificationSettings = runtimeMinificationSettings ?? throw new ArgumentNullException(nameof(runtimeMinificationSettings)); + _umbracoVersion = umbracoVersion ?? throw new ArgumentNullException(nameof(umbracoVersion)); + _entryAssemblyMetadata = entryAssemblyMetadata ?? throw new ArgumentNullException(nameof(entryAssemblyMetadata)); + } + + private string CacheBusterValue + { + get + { + if (_cacheBusterValue != null) + { + return _cacheBusterValue; + } + + // Assembly Name adds a bit of uniqueness across sites when version missing from config. + // Adds a bit of security through obscurity that was asked for in standup. + var prefix = _runtimeMinificationSettings.Value.Version ?? _entryAssemblyMetadata.Name ?? string.Empty; + var umbracoVersion = _umbracoVersion.SemanticVersion.ToString(); + var downstreamVersion = _entryAssemblyMetadata.InformationalVersion; + + _cacheBusterValue = $"{prefix}_{umbracoVersion}_{downstreamVersion}".GenerateHash(); + + return _cacheBusterValue; + } + } + + public string GetValue() => CacheBusterValue; + } +} diff --git a/src/Umbraco.Web.Common/Umbraco.Web.Common.csproj b/src/Umbraco.Web.Common/Umbraco.Web.Common.csproj index aa7314b388..0c01f07da8 100644 --- a/src/Umbraco.Web.Common/Umbraco.Web.Common.csproj +++ b/src/Umbraco.Web.Common/Umbraco.Web.Common.csproj @@ -35,8 +35,8 @@ - - + + all diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/content/edit.controller.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/content/edit.controller.js index 50a32e0b05..ae9f0121ca 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/content/edit.controller.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/content/edit.controller.js @@ -602,8 +602,6 @@ $scope.page.buttonGroupState = 'error'; handleHttpException(err); }); - - }, close: function () { overlayService.close(); @@ -641,13 +639,14 @@ overlayService.close(); return $q.when(data); }, - function (err) { - clearDirtyState($scope.content.variants); - model.submitButtonState = "error"; - //re-map the dialog model since we've re-bound the properties - dialog.variants = $scope.content.variants; - handleHttpException(err); - }); + function (err) { + clearDirtyState($scope.content.variants); + model.submitButtonState = "error"; + //re-map the dialog model since we've re-bound the properties + dialog.variants = $scope.content.variants; + + handleHttpException(err); + }); }, close: function () { overlayService.close(); @@ -699,14 +698,18 @@ clearNotifications($scope.content); overlayService.close(); return $q.when(data); - }, - function (err) { - clearDirtyState($scope.content.variants); - model.submitButtonState = "error"; - //re-map the dialog model since we've re-bound the properties - dialog.variants = $scope.content.variants; - handleHttpException(err); - }); + }, function (err) { + clearDirtyState($scope.content.variants); + model.submitButtonState = "error"; + //re-map the dialog model since we've re-bound the properties + dialog.variants = $scope.content.variants; + + //ensure error messages are displayed + formHelper.showNotifications(err.data); + clearNotifications($scope.content); + + handleHttpException(err); + }); }, close: function () { overlayService.close(); @@ -760,20 +763,24 @@ clearNotifications($scope.content); overlayService.close(); return $q.when(data); - }, - function (err) { - clearDirtyState($scope.content.variants); - //model.submitButtonState = "error"; - // Because this is the "save"-action, then we actually save though there was a validation error, therefor we will show success and display the validation errors politely. - if(err && err.data && err.data.ModelState && Object.keys(err.data.ModelState).length > 0) { - model.submitButtonState = "success"; - } else { - model.submitButtonState = "error"; - } + }, function (err) { + clearDirtyState($scope.content.variants); + //model.submitButtonState = "error"; + // Because this is the "save"-action, then we actually save though there was a validation error, therefor we will show success and display the validation errors politely. + if(err && err.data && err.data.ModelState && Object.keys(err.data.ModelState).length > 0) { + model.submitButtonState = "success"; + } else { + model.submitButtonState = "error"; //re-map the dialog model since we've re-bound the properties dialog.variants = $scope.content.variants; + + //ensure error messages are displayed + formHelper.showNotifications(err.data); + clearNotifications($scope.content); + handleHttpException(err); - }); + } + }) }, close: function (oldModel) { overlayService.close(); diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/umbeditorheader.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/umbeditorheader.directive.js index 1b63dde26c..75df00c596 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/umbeditorheader.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/umbeditorheader.directive.js @@ -192,6 +192,7 @@ Use this directive to construct a header inside the main editor window. @param {array=} tabs Array of tabs. See example above. @param {array=} navigation Array of sub views. See example above. @param {boolean=} nameLocked Set to true to lock the name. +@param {number=} nameMaxLength Maximum length of the name. @param {object=} menu Add a context menu to the editor. @param {string=} icon Show and edit the content icon. Opens an overlay to change the icon. @param {boolean=} hideIcon Set to true to hide icon. @@ -210,11 +211,11 @@ Use this directive to construct a header inside the main editor window. function EditorHeaderDirective(editorService, localizationService, editorState, $rootScope) { - function link(scope, $injector) { + function link(scope) { scope.vm = {}; scope.vm.dropdownOpen = false; - scope.vm.currentVariant = ""; + scope.vm.currentVariant = ""; scope.loading = true; scope.accessibility = {}; scope.accessibility.a11yMessage = ""; @@ -222,6 +223,12 @@ Use this directive to construct a header inside the main editor window. scope.accessibility.a11yMessageVisible = false; scope.accessibility.a11yNameVisible = false; + // trim the name if required + scope.nameMaxLength = scope.nameMaxLength || 255; + if (scope.name && scope.name.length > scope.nameMaxLength) { + scope.name = scope.name.substring(0, scope.nameMaxLength - 1) + '…'; + } + // need to call localizationService service outside of routine to set a11y due to promise requirements if (editorState.current) { //to do make work for user create/edit @@ -376,6 +383,7 @@ Use this directive to construct a header inside the main editor window. name: "=", nameLocked: "=", nameRequired: "=?", + nameMaxLength: "=?", menu: "=", hideActionsMenu: " elm.classList.contains('umb-button-ellipsis') || elm.classList.contains('umb-sub-views-nav-item__action') || elm.classList.contains('umb-tab-button')); + + if(avoidStartElm === 0) { + focusableElements[1].focus(); + } + else { + firstFocusableElement.focus(); + } } else { defaultFocusedElement.focus(); @@ -175,34 +183,27 @@ } function onInit(targetElm) { - $timeout(() => { + // Fetch the DOM nodes we need + getDomNodes(); - // Fetch the DOM nodes we need - getDomNodes(); + cleanupEventHandlers(); - cleanupEventHandlers(); + getFocusableElements(targetElm); - getFocusableElements(targetElm); + if(focusableElements.length > 0) { - if(focusableElements.length > 0) { + observeDomChanges(); - observeDomChanges(); + setElementFocus(); - setElementFocus(); - - // Handle keydown - target.addEventListener('keydown', handleKeydown); - } - - }); + // Handle keydown + target.addEventListener('keydown', handleKeydown); + } + }, 500); } - scope.$on('$includeContentLoaded', () => { - angularHelper.safeApply(scope, () => { - onInit(); - }); - }); + onInit(); // If more than one editor is still open then re-initialize otherwise remove the event listener scope.$on('$destroy', function () { @@ -243,6 +244,3 @@ angular.module('umbraco.directives').directive('umbFocusLock', FocusLock); })(); - - -// TODO: Ensure the domObserver is NOT started when there is only one infinite overlay and it's being destroyed! diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/localization/localize.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/localization/localize.directive.js index 2262da4c16..6fea8d8c84 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/localization/localize.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/localization/localize.directive.js @@ -1,85 +1,101 @@ angular.module('umbraco.directives') - /** - * @ngdoc directive - * @name umbraco.directives.directive:localize - * @restrict EA - * @function - * @description - *
- * Component
- * Localize a specific token to put into the HTML as an item. - *
- *
- * Attribute
- * Add an HTML attribute to an element containing the HTML attribute name you wish to localize, - * using the format of '@section_key' or 'section_key'. - *
- * ##Usage - *
-    * 
-    * Close
-    * Fallback value
-    *
-    * 
-    * 
-    * 
-    * 
- *
- **/ - .directive('localize', function ($log, localizationService) { - return { - restrict: 'E', - scope: { - key: '@', - tokens: '=', - watchTokens: '@' - }, - replace: true, - link: function (scope, element, attrs) { - var key = scope.key; +/** +* @ngdoc directive +* @name umbraco.directives.directive:localize +* @restrict EA +* @function +* @description +*
+* Used to localize text in HTMl-elements or attributes using translation keys. Translations are stored in umbraco/config/lang/ or the /lang-folder of a package i App_Plugins. +*
+*
+* Component/Element
+ * Localize a specific token to put into the HTML as an item +*
+*
+* Attribute
+ * Add a HTML attribute to an element containing the HTML attribute name you wish to localise + * Using the format of '@section_key' or 'section_key' +*
+* ##Basic Usage +*
+* 
+* Close
+* Fallback value
+*
+* 
+* 
+* 
+* 
+*
+* ##Use with tokens +* Also supports tokens inside the translation key, example of a translation +*
+*   You have %0% characters left of %1%
+* 
+* Can be used like this: +*
+*   You have %0% characters left of %1%
+*   
+* 
+* Where the "tokens"-attribute is an array of tokens for the translations, "watch-tokens" will make the component watch the expression passed. +**/ +.directive('localize', function ($log, localizationService) { + return { + restrict: 'E', + scope: { + key: '@', + tokens: '=', + watchTokens: '@' + }, + replace: true, + link: function (scope, element, attrs) { + var key = scope.key; scope.text = ''; - // A render function to be able to update tokens as values update - function render() { - element.html(localizationService.tokenReplace(scope.text, scope.tokens || null)); - } - - localizationService.localize(key).then(function (value) { - scope.text = value; - render(); - }); - - if (scope.watchTokens === 'true') { - scope.$watch("tokens", render, true); - } + // A render function to be able to update tokens as values update. + function render() { + element.html(localizationService.tokenReplace(scope.text, scope.tokens || null)); } - }; - }) - .directive('localize', function ($log, localizationService) { - return { - restrict: 'A', - link: function (scope, element, attrs) { - // Support one or more attribute properties to update - var keys = attrs.localize.split(','); - Utilities.forEach(keys, (value, key) => { - var attr = element.attr(value); - if (attr) { + // As per component definition in ngdoc above, the initial inner html of the element is to be used as fallback value + var fallbackValue = element.html(); + localizationService.localize(key, null, fallbackValue).then(function (value) { + scope.text = value; + render(); + }); + + if (scope.watchTokens === 'true') { + scope.$watch("tokens", render, true); + } + } + }; +}) +.directive('localize', function ($log, localizationService) { + return { + restrict: 'A', + link: function (scope, element, attrs) { + //Support one or more attribute properties to update + var keys = attrs.localize.split(','); + + Utilities.forEach(keys, (value, key) => { + var attr = element.attr(value); + if (attr) { // Localizing is done async, so make sure the key isn't visible element.removeAttr(value); - - if (attr[0] === '@') { - // If the translation key starts with @ then remove it - attr = attr.substring(1); - } - var t = localizationService.tokenize(attr, scope); - - localizationService.localize(t.key, t.tokens).then(function (val) { - element.attr(value, val); - }); + if (attr[0] === '@') { + //If the translation key starts with @ then remove it + attr = attr.substring(1); } - }); - } - }; - }); + + var t = localizationService.tokenize(attr, scope); + + localizationService.localize(t.key, t.tokens).then(function (val) { + element.attr(value, val); + }); + } + }); + } + }; +}); diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/property/umbproperty.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/property/umbproperty.directive.js index f4cfacbf70..25e55455db 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/property/umbproperty.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/property/umbproperty.directive.js @@ -75,7 +75,21 @@ // inheritance is (i.e.infinite editing) var found = angularHelper.traverseScopeChain($scope, s => s && s.vm && s.vm.constructor.name === "UmbPropertyController"); vm.parentUmbProperty = found ? found.vm : null; + } + + if (vm.property.description) { + // split by lines containing only '--' + var descriptionParts = vm.property.description.split(/^--$/gim); + if (descriptionParts.length > 1) { + // if more than one part, we have an extended description, + // combine to one extended description, and remove leading linebreak + vm.property.extendedDescription = descriptionParts.splice(1).join("--").substring(1); + vm.property.extendedDescriptionVisible = false; + + // set propertydescription to first part, and remove trailing linebreak + vm.property.description = descriptionParts[0].slice(0, -1); } + } } } diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbgroupsbuilder.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbgroupsbuilder.directive.js index 44d45263da..79da9e3ac6 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbgroupsbuilder.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbgroupsbuilder.directive.js @@ -582,7 +582,7 @@ }; scope.canRemoveTab = (tab) => { - return tab.inherited !== true; + return scope.canRemoveGroup(tab) && _.every(scope.model.groups.filter(group => group.parentAlias === tab.alias), group => scope.canRemoveGroup(group)); }; scope.setTabOverflowState = (overflowLeft, overflowRight) => { diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/upload/umbfiledropzone.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/upload/umbfiledropzone.directive.js index 51fc6284b8..98f02b7e06 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/upload/umbfiledropzone.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/upload/umbfiledropzone.directive.js @@ -31,7 +31,7 @@ angular.module("umbraco.directives") propertyAlias: '@', accept: '@', maxFileSize: '@', - + compact: '@', hideDropzone: '@', acceptedMediatypes: '=', @@ -42,9 +42,10 @@ angular.module("umbraco.directives") }, link: function(scope, element, attrs) { scope.queue = []; - scope.done = []; - scope.rejected = []; + scope.totalQueued = 0; scope.currentFile = undefined; + scope.processed = []; + scope.totalMessages = 0; function _filterFile(file) { var ignoreFileNames = ['Thumbs.db']; @@ -65,51 +66,50 @@ angular.module("umbraco.directives") function _filesQueued(files, event) { //Push into the queue Utilities.forEach(files, file => { - if (_filterFile(file) === true) { - - if (file.$error) { - scope.rejected.push(file); - } else { - scope.queue.push(file); - } + if (_filterFile(file) === true) { + file.messages = []; + scope.queue.push(file); } }); - //when queue is done, kick the uploader - if (!scope.working) { - // Upload not allowed - if (!scope.acceptedMediatypes || !scope.acceptedMediatypes.length) { - files.map(file => { - file.uploadStatus = "error"; - file.serverErrorMessage = "File type is not allowed here"; - scope.rejected.push(file); - }); - scope.queue = []; - } - // If we have Accepted Media Types, we will ask to choose Media Type, if Choose Media Type returns false, it only had one choice and therefor no reason to - if (scope.acceptedMediatypes && _requestChooseMediaTypeDialog() === false) { - scope.contentTypeAlias = "umbracoAutoSelect"; - - _processQueueItem(); - } + // Upload not allowed + if (!scope.acceptedMediatypes || !scope.acceptedMediatypes.length) { + files.map(file => { + file.messages.push({message: "File type is not allowed here", type: "Error"}); + }); } + + // If we have Accepted Media Types, we will ask to choose Media Type, if Choose Media Type returns false, it only had one choice and therefor no reason to + if (scope.acceptedMediatypes && _requestChooseMediaTypeDialog() === false) { + scope.contentTypeAlias = "umbracoAutoSelect"; + } + + // Add the processed length, as we might be uploading in stages + scope.totalQueued = scope.queue.length + scope.processed.length; + + _processQueueItems(); } - function _processQueueItem() { - if (scope.queue.length > 0) { + function _processQueueItems() { + // if we have processed all files, either by successful + // upload, or attending to all messages, we deem the + // action complete, else continue processing files + scope.totalMessages = scope.processed.filter(e => e.messages.length > 0).length; + if (scope.totalQueued === scope.processed.length) { + if (scope.totalMessages === 0) { + if (scope.filesUploaded) { + //queue is empty, trigger the done action + scope.filesUploaded(scope.done); + } + //auto-clear the done queue after 3 secs + var currentLength = scope.processed.length; + $timeout(function() { + scope.processed.splice(0, currentLength); + }, 3000); + } + } else { scope.currentFile = scope.queue.shift(); _upload(scope.currentFile); - } else if (scope.done.length > 0) { - if (scope.filesUploaded) { - //queue is empty, trigger the done action - scope.filesUploaded(scope.done); - } - - //auto-clear the done queue after 3 secs - var currentLength = scope.done.length; - $timeout(function() { - scope.done.splice(0, currentLength); - }, 3000); } } @@ -134,55 +134,36 @@ angular.module("umbraco.directives") var progressPercentage = parseInt(100.0 * evt.loaded / evt.total, 10); // set percentage property on file file.uploadProgress = progressPercentage; - // set uploading status on file - file.uploadStatus = "uploading"; } }) - .success(function(data, status, headers, config) { - if (data.notifications && data.notifications.length > 0) { - // set error status on file - file.uploadStatus = "error"; - // Throw message back to user with the cause of the error - file.serverErrorMessage = data.notifications[0].message; - // Put the file in the rejected pool - scope.rejected.push(file); - } else { - // set done status on file - file.uploadStatus = "done"; - file.uploadProgress = 100; - // set date/time for when done - used for sorting - file.doneDate = new Date(); - // Put the file in the done pool - scope.done.push(file); - } + .success(function (data, status, headers, config) { + // Set server messages + file.messages = data.notifications; + scope.processed.push(file); + //after processing, test if everything is done scope.currentFile = undefined; - //after processing, test if everthing is done - _processQueueItem(); + _processQueueItems(); }) .error(function(evt, status, headers, config) { - // set status done - file.uploadStatus = "error"; //if the service returns a detailed error if (evt.InnerException) { - file.serverErrorMessage = evt.InnerException.ExceptionMessage; + file.messages.push({ message: evt.InnerException.ExceptionMessage, type: "Error" }); //Check if its the common "too large file" exception if (evt.InnerException.StackTrace && evt.InnerException.StackTrace.indexOf("ValidateRequestEntityLength") > 0) { - file.serverErrorMessage = "File too large to upload"; + file.messages.push({ message: "File too large to upload", type: "Error" }); } } else if (evt.Message) { - file.serverErrorMessage = evt.Message; - } else if (evt && typeof evt === 'string') { - file.serverErrorMessage = evt; + file.messages.push({message: evt.Message, type: "Error"}); + } else if (evt && typeof evt === "string") { + file.messages.push({message: evt, type: "Error"}); } // If file not found, server will return a 404 and display this message if (status === 404) { - file.serverErrorMessage = "File not found"; + file.messages.push({message: "File not found", type: "Error"}); } - //after processing, test if everthing is done - scope.rejected.push(file); scope.currentFile = undefined; - _processQueueItem(); + _processQueueItems(); }); } @@ -224,16 +205,14 @@ angular.module("umbraco.directives") availableItems: filteredMediaTypes, submit: function (model) { scope.contentTypeAlias = model.selectedItem.alias; - _processQueueItem(); + _processQueueItems(); overlayService.close(); }, close: function () { scope.queue.map(function (file) { - file.uploadStatus = "error"; - file.serverErrorMessage = "No files uploaded, no mediatype selected"; - scope.rejected.push(file); + file.messages.push({message:"No files uploaded, no mediatype selected", type: "Error"}); }); scope.queue = []; @@ -245,14 +224,26 @@ angular.module("umbraco.directives") overlayService.open(dialog); }); - return true;// yes, we did open the choose-media dialog, therefor we return true. + return true; // yes, we did open the choose-media dialog, therefore we return true. + } + + scope.dismissMessages = function (file) { + file.messages = []; + _processQueueItems(); + } + + scope.dismissAllMessages = function () { + Utilities.forEach(scope.processed, file => { + file.messages = []; + }); + _processQueueItems(); } scope.handleFiles = function(files, event, invalidFiles) { const allFiles = [...files, ...invalidFiles]; // add unique key for each files to use in ng-repeats - allFiles.forEach(file => { + Utilities.forEach(allFiles, file => { file.key = String.CreateGuid(); }); diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/util/getDomElement.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/util/getDomElement.directive.js index 2a0c9c3aec..28a32665d7 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/util/getDomElement.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/util/getDomElement.directive.js @@ -1,17 +1,17 @@ angular.module("umbraco.directives").directive("retriveDomElement", function () { - var directiveDefinitionObject = { + var directiveDefinitionObject = { - restrict: "A", - selector: '[retriveDomElement]', - scope: { - "retriveDomElement": "&" - }, - link: { - post: function(scope, iElement, iAttrs, controller) { - scope.retriveDomElement({element:iElement, attributes: iAttrs}); + restrict: "A", + selector: '[retriveDomElement]', + scope: { + "retriveDomElement": "&" + }, + link: { + post: function (scope, iElement, iAttrs, controller) { + scope.retriveDomElement({ element: iElement, attributes: iAttrs }); + } } - } - }; + }; - return directiveDefinitionObject; + return directiveDefinitionObject; }); diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/util/retrieveDomElement.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/util/retrieveDomElement.directive.js new file mode 100644 index 0000000000..74b2639340 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/common/directives/util/retrieveDomElement.directive.js @@ -0,0 +1,17 @@ +angular.module("umbraco.directives").directive("retrieveDomElement", function () { + var directiveDefinitionObject = { + + restrict: "A", + selector: '[retrieveDomElement]', + scope: { + "retrieveDomElement": "&" + }, + link: { + post: function(scope, iElement, iAttrs, controller) { + scope.retrieveDomElement({element:iElement, attributes: iAttrs}); + } + } + }; + + return directiveDefinitionObject; +}); diff --git a/src/Umbraco.Web.UI.Client/src/common/filters/simpleMarkdown.filter.js.js b/src/Umbraco.Web.UI.Client/src/common/filters/simpleMarkdown.filter.js.js new file mode 100644 index 0000000000..d33b96916a --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/common/filters/simpleMarkdown.filter.js.js @@ -0,0 +1,20 @@ +/** +* @ngdoc filter +* @name umbraco.filters.simpleMarkdown +* @description +* Used when rendering a string as Markdown as HTML (i.e. with ng-bind-html). Allows use of **bold**, *italics*, ![images](url) and [links](url) +**/ +angular.module("umbraco.filters").filter('simpleMarkdown', function () { + return function (text) { + if (!text) { + return ''; + } + + return text + .replace(/\*\*(.*)\*\*/gim, '$1') + .replace(/\*(.*)\*/gim, '$1') + .replace(/!\[(.*?)\]\((.*?)\)/gim, "$1") + .replace(/\[(.*?)\]\((.*?)\)/gim, "$1") + .replace(/\n/g, '
').trim(); + }; +}); diff --git a/src/Umbraco.Web.UI.Client/src/common/mocks/resources/entity.mocks.js b/src/Umbraco.Web.UI.Client/src/common/mocks/resources/entity.mocks.js index 05594115e1..08c28fcbd1 100644 --- a/src/Umbraco.Web.UI.Client/src/common/mocks/resources/entity.mocks.js +++ b/src/Umbraco.Web.UI.Client/src/common/mocks/resources/entity.mocks.js @@ -34,7 +34,7 @@ angular.module('umbraco.mocks'). return [200, nodes, null]; } - function returnUrlsbyUdis(status, data, headers) { + function returnUrlsByIds(status, data, headers) { if (!mocksUtils.checkAuth()) { return [401, null, null]; @@ -83,8 +83,8 @@ angular.module('umbraco.mocks'). .respond(returnEntitybyIdsPost); $httpBackend - .whenPOST(mocksUtils.urlRegex('/umbraco/UmbracoApi/Entity/GetUrlsByUdis')) - .respond(returnUrlsbyUdis); + .whenPOST(mocksUtils.urlRegex('/umbraco/UmbracoApi/Entity/GetUrlsByIds')) + .respond(returnUrlsByIds); $httpBackend .whenGET(mocksUtils.urlRegex('/umbraco/UmbracoApi/Entity/GetAncestors')) diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/auth.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/auth.resource.js index 78fefc8db5..e09718176c 100644 --- a/src/Umbraco.Web.UI.Client/src/common/resources/auth.resource.js +++ b/src/Umbraco.Web.UI.Client/src/common/resources/auth.resource.js @@ -37,7 +37,7 @@ function authResource($q, $http, umbRequestHelper, angularHelper) { umbRequestHelper.getApiUrl( "authenticationApiBaseUrl", "Get2FAProviders")), - 'Could not retrive two factor provider info'); + 'Could not retrieve two factor provider info'); }, /** diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/contenttype.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/contenttype.resource.js index 6398cc55e7..ada64bd3f6 100644 --- a/src/Umbraco.Web.UI.Client/src/common/resources/contenttype.resource.js +++ b/src/Umbraco.Web.UI.Client/src/common/resources/contenttype.resource.js @@ -129,7 +129,7 @@ function contentTypeResource($q, $http, umbRequestHelper, umbDataFormatter, loca * }); * * - * @param {Int} contentTypeId id of the content item to retrive allowed child types for + * @param {Int} contentTypeId id of the content item to retrieve allowed child types for * @returns {Promise} resourcePromise object. * */ diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/datatype.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/datatype.resource.js index 60a5d235fe..f90f86364b 100644 --- a/src/Umbraco.Web.UI.Client/src/common/resources/datatype.resource.js +++ b/src/Umbraco.Web.UI.Client/src/common/resources/datatype.resource.js @@ -23,7 +23,7 @@ function dataTypeResource($q, $http, umbDataFormatter, umbRequestHelper) { * }); * * - * @param {String} editorAlias string alias of editor type to retrive prevalues configuration for + * @param {String} editorAlias string alias of editor type to retrieve prevalues configuration for * @param {Int} id id of datatype to retrieve prevalues for * @returns {Promise} resourcePromise object. * diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/entity.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/entity.resource.js index c6dc313bc7..d94bb4e6be 100644 --- a/src/Umbraco.Web.UI.Client/src/common/resources/entity.resource.js +++ b/src/Umbraco.Web.UI.Client/src/common/resources/entity.resource.js @@ -127,6 +127,24 @@ function entityResource($q, $http, umbRequestHelper) { 'Failed to retrieve url for id:' + id); }, + getUrlsByIds: function(ids, type, culture) { + var query = `type=${type}&culture=${culture || ""}`; + + return umbRequestHelper.resourcePromise( + $http.post( + umbRequestHelper.getApiUrl( + "entityApiBaseUrl", + "GetUrlsByIds", + query), + { + ids: ids + }), + 'Failed to retrieve url map for ids ' + ids); + }, + + /** + * @deprecated use getUrlsByIds instead. + */ getUrlsByUdis: function(udis, culture) { var query = "culture=" + (culture || ""); diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/log.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/log.resource.js index 9ae5c378ce..4e98b1d8cc 100644 --- a/src/Umbraco.Web.UI.Client/src/common/resources/log.resource.js +++ b/src/Umbraco.Web.UI.Client/src/common/resources/log.resource.js @@ -1,7 +1,7 @@ /** * @ngdoc service * @name umbraco.resources.logResource - * @description Retrives log history from umbraco + * @description Retrieves log history from umbraco * * **/ diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/logviewer.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/logviewer.resource.js index 46ef8d2919..b71c272d35 100644 --- a/src/Umbraco.Web.UI.Client/src/common/resources/logviewer.resource.js +++ b/src/Umbraco.Web.UI.Client/src/common/resources/logviewer.resource.js @@ -1,7 +1,7 @@ /** * @ngdoc service * @name umbraco.resources.logViewerResource - * @description Retrives Umbraco log items (by default from JSON files on disk) + * @description Retrieves Umbraco log items (by default from JSON files on disk) * * **/ @@ -25,7 +25,10 @@ function logViewerResource($q, $http, umbRequestHelper) { getNumberOfErrors: (startDate, endDate) => request('GET', 'GetNumberOfErrors', '?startDate=' + startDate + '&endDate=' + endDate, 'Failed to retrieve number of errors in logs'), - + + getLogLevels: () => + request('GET', 'GetLogLevels', null, 'Failed to retrieve log levels'), + getLogLevel: () => request('GET', 'GetLogLevel', null, 'Failed to retrieve log level'), diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/mediatype.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/mediatype.resource.js index e3fab86067..62fe2b7367 100644 --- a/src/Umbraco.Web.UI.Client/src/common/resources/mediatype.resource.js +++ b/src/Umbraco.Web.UI.Client/src/common/resources/mediatype.resource.js @@ -85,7 +85,7 @@ function mediaTypeResource($q, $http, umbRequestHelper, umbDataFormatter, locali * $scope.type = type; * }); * - * @param {Int} mediaId id of the media item to retrive allowed child types for + * @param {Int} mediaId id of the media item to retrieve allowed child types for * @returns {Promise} resourcePromise object. * */ diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/redirecturls.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/redirecturls.resource.js index fb145418ed..c91903eb0f 100644 --- a/src/Umbraco.Web.UI.Client/src/common/resources/redirecturls.resource.js +++ b/src/Umbraco.Web.UI.Client/src/common/resources/redirecturls.resource.js @@ -27,7 +27,7 @@ * }); * * @param {String} searchTerm Searh term - * @param {Int} pageIndex index of the page to retrive items from + * @param {Int} pageIndex index of the page to retrieve items from * @param {Int} pageSize The number of items on a page */ function searchRedirectUrls(searchTerm, pageIndex, pageSize) { diff --git a/src/Umbraco.Web.UI.Client/src/common/services/blockeditormodelobject.service.js b/src/Umbraco.Web.UI.Client/src/common/services/blockeditormodelobject.service.js index 4f5f47fb81..22baed8472 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/blockeditormodelobject.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/blockeditormodelobject.service.js @@ -442,7 +442,7 @@ * @ngdoc method * @name getAvailableAliasesForBlockContent * @methodOf umbraco.services.blockEditorModelObject - * @description Retrive a list of aliases that are available for content of blocks in this property editor, does not contain aliases of block settings. + * @description Retrieve a list of aliases that are available for content of blocks in this property editor, does not contain aliases of block settings. * @return {Array} array of strings representing alias. */ getAvailableAliasesForBlockContent: function () { @@ -460,7 +460,7 @@ * @ngdoc method * @name getAvailableBlocksForBlockPicker * @methodOf umbraco.services.blockEditorModelObject - * @description Retrive a list of available blocks, the list containing object with the confirugation model(blockConfigModel) and the element type model(elementTypeModel). + * @description Retrieve a list of available blocks, the list containing object with the confirugation model(blockConfigModel) and the element type model(elementTypeModel). * The purpose of this data is to provide it for the Block Picker. * @return {Array} array of objects representing available blocks, each object containing properties blockConfigModel and elementTypeModel. */ diff --git a/src/Umbraco.Web.UI.Client/src/common/services/clipboard.service.js b/src/Umbraco.Web.UI.Client/src/common/services/clipboard.service.js index 901e5fa93c..abf173b129 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/clipboard.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/clipboard.service.js @@ -6,8 +6,8 @@ * @requires eventsService * * @description - * Service to handle clipboard in general across the application. Responsible for handling the data both storing and retrive. - * The service has a set way for defining a data-set by a entryType and alias, which later will be used to retrive the posible entries for a paste scenario. + * Service to handle clipboard in general across the application. Responsible for handling the data both storing and retrieve. + * The service has a set way for defining a data-set by a entryType and alias, which later will be used to retrieve the posible entries for a paste scenario. * */ function clipboardService($window, notificationsService, eventsService, localStorageService, iconHelper) { @@ -77,7 +77,7 @@ function clipboardService($window, notificationsService, eventsService, localSto var STORAGE_KEY = "umbClipboardService"; - var retriveStorage = function() { + var retrieveStorage = function() { if (localStorageService.isSupported === false) { return null; } @@ -103,7 +103,7 @@ function clipboardService($window, notificationsService, eventsService, localSto try { // Check that we can parse the JSON: - var storageJSON = JSON.parse(storageString); + var _ = JSON.parse(storageString); // Store the string: localStorageService.set(STORAGE_KEY, storageString); @@ -114,11 +114,8 @@ function clipboardService($window, notificationsService, eventsService, localSto } catch(e) { return false; } - - return false; } - function resolvePropertyForStorage(prop, type) { type = type || "raw"; @@ -159,9 +156,6 @@ function clipboardService($window, notificationsService, eventsService, localSto ); } - - - function resolvePropertyForPaste(prop, type) { type = type || "raw"; @@ -173,8 +167,6 @@ function clipboardService($window, notificationsService, eventsService, localSto } } - - var service = {}; /** @@ -300,7 +292,7 @@ function clipboardService($window, notificationsService, eventsService, localSto */ service.copy = function(type, alias, data, displayLabel, displayIcon, uniqueKey, firstLevelClearupMethod) { - var storage = retriveStorage(); + var storage = retrieveStorage(); displayLabel = displayLabel || data.name; displayIcon = displayIcon || iconHelper.convertFromLegacyIcon(data.icon); @@ -347,7 +339,7 @@ function clipboardService($window, notificationsService, eventsService, localSto type = "elementType"; } - var storage = retriveStorage(); + var storage = retrieveStorage(); // Clean up each entry var copiedDatas = datas.map(data => prepareEntryForStorage(type, data, firstLevelClearupMethod)); @@ -383,6 +375,7 @@ function clipboardService($window, notificationsService, eventsService, localSto return localStorageService.isSupported; }; + /** * @ngdoc method * @name umbraco.services.supportsCopy#hasEntriesOfType @@ -396,16 +389,17 @@ function clipboardService($window, notificationsService, eventsService, localSto */ service.hasEntriesOfType = function(type, aliases) { - if(service.retriveEntriesOfType(type, aliases).length > 0) { + if(service.retrieveEntriesOfType(type, aliases).length > 0) { return true; } return false; }; + /** * @ngdoc method - * @name umbraco.services.supportsCopy#retriveEntriesOfType + * @name umbraco.services.supportsCopy#retrieveEntriesOfType * @methodOf umbraco.services.clipboardService * * @param {string} type A string defining the type of data to recive. @@ -414,9 +408,9 @@ function clipboardService($window, notificationsService, eventsService, localSto * @description * Returns an array of entries matching the given type and one of the provided aliases. */ - service.retriveEntriesOfType = function(type, allowedAliases) { + service.retrieveEntriesOfType = function(type, allowedAliases) { - var storage = retriveStorage(); + var storage = retrieveStorage(); // Find entries that are fulfilling the criteria for this nodeType and nodeTypesAliases. var filteretEntries = storage.entries.filter( @@ -428,9 +422,19 @@ function clipboardService($window, notificationsService, eventsService, localSto return filteretEntries; }; + + /** + * @obsolete Use the typo-free version instead. + */ + service.retriveEntriesOfType = (type, allowedAliases) => { + console.warn('clipboardService.retriveEntriesOfType is obsolete, use clipboardService.retrieveEntriesOfType instead'); + return service.retrieveEntriesOfType(type, allowedAliases); + } + + /** * @ngdoc method - * @name umbraco.services.supportsCopy#retriveEntriesOfType + * @name umbraco.services.supportsCopy#retrieveDataOfType * @methodOf umbraco.services.clipboardService * * @param {string} type A string defining the type of data to recive. @@ -439,13 +443,23 @@ function clipboardService($window, notificationsService, eventsService, localSto * @description * Returns an array of data of entries matching the given type and one of the provided aliases. */ - service.retriveDataOfType = function(type, aliases) { - return service.retriveEntriesOfType(type, aliases).map((x) => x.data); + service.retrieveDataOfType = function(type, aliases) { + return service.retrieveEntriesOfType(type, aliases).map((x) => x.data); }; + + /** + * @obsolete Use the typo-free version instead. + */ + service.retriveDataOfType = (type, aliases) => { + console.warn('clipboardService.retriveDataOfType is obsolete, use clipboardService.retrieveDataOfType instead'); + return service.retrieveDataOfType(type, aliases); + } + + /** * @ngdoc method - * @name umbraco.services.supportsCopy#retriveEntriesOfType + * @name umbraco.services.supportsCopy#clearEntriesOfType * @methodOf umbraco.services.clipboardService * * @param {string} type A string defining the type of data to remove. @@ -456,7 +470,7 @@ function clipboardService($window, notificationsService, eventsService, localSto */ service.clearEntriesOfType = function(type, allowedAliases) { - var storage = retriveStorage(); + var storage = retrieveStorage(); // Find entries that are NOT fulfilling the criteria for this nodeType and nodeTypesAliases. var filteretEntries = storage.entries.filter( @@ -470,7 +484,6 @@ function clipboardService($window, notificationsService, eventsService, localSto saveStorage(storage); }; - var emitClipboardStorageUpdate = _.debounce(function(e) { eventsService.emit("clipboardService.storageUpdate"); }, 1000); @@ -478,10 +491,7 @@ function clipboardService($window, notificationsService, eventsService, localSto // Fires if LocalStorage was changed from another tab than this one. $window.addEventListener("storage", emitClipboardStorageUpdate); - - return service; } - angular.module("umbraco.services").factory("clipboardService", clipboardService); diff --git a/src/Umbraco.Web.UI.Client/src/common/services/contenteditinghelper.service.js b/src/Umbraco.Web.UI.Client/src/common/services/contenteditinghelper.service.js index 0d8d5c782a..f4c6063a9a 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/contenteditinghelper.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/contenteditinghelper.service.js @@ -624,24 +624,29 @@ function contentEditingHelper(fileManager, $q, $location, $routeParams, editorSt var origProp = allOrigProps[k]; var alias = origProp.alias; var newProp = getNewProp(alias, allNewProps); - if (newProp && !_.isEqual(origProp.value, newProp.value)) { + if (newProp) { + // Always update readonly state + origProp.readonly = newProp.readonly; - //they have changed so set the origContent prop to the new one - var origVal = origProp.value; + // Check whether the value has changed and update accordingly + if (!_.isEqual(origProp.value, newProp.value)) { - origProp.value = newProp.value; + //they have changed so set the origContent prop to the new one + var origVal = origProp.value; - //instead of having a property editor $watch their expression to check if it has - // been updated, instead we'll check for the existence of a special method on their model - // and just call it. - if (Utilities.isFunction(origProp.onValueChanged)) { - //send the newVal + oldVal - origProp.onValueChanged(origProp.value, origVal); + origProp.value = newProp.value; + + //instead of having a property editor $watch their expression to check if it has + // been updated, instead we'll check for the existence of a special method on their model + // and just call it. + if (Utilities.isFunction(origProp.onValueChanged)) { + //send the newVal + oldVal + origProp.onValueChanged(origProp.value, origVal); + } + + changed.push(origProp); } - - changed.push(origProp); } - } } diff --git a/src/Umbraco.Web.UI.Client/src/common/services/tree.service.js b/src/Umbraco.Web.UI.Client/src/common/services/tree.service.js index 9970995a28..ba9ebc1b00 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/tree.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/tree.service.js @@ -390,7 +390,7 @@ function treeService($q, treeResource, iconHelper, notificationsService, eventsS * * @description * Gets a child node with a given ID, from a specific treeNode - * @param {object} treeNode to retrive child node from + * @param {object} treeNode to retrieve child node from * @param {int} id id of child node */ getChildNode: function (treeNode, id) { @@ -411,7 +411,7 @@ function treeService($q, treeResource, iconHelper, notificationsService, eventsS * * @description * Gets a descendant node by id - * @param {object} treeNode to retrive descendant node from + * @param {object} treeNode to retrieve descendant node from * @param {int} id id of descendant node * @param {string} treeAlias - optional tree alias, if fetching descendant node from a child of a listview document */ @@ -494,7 +494,7 @@ function treeService($q, treeResource, iconHelper, notificationsService, eventsS * * @description * Gets the root node of the current tree type for a given tree node - * @param {object} treeNode to retrive tree root node from + * @param {object} treeNode to retrieve tree root node from */ getTreeRoot: function (treeNode) { if (!treeNode) { @@ -531,7 +531,7 @@ function treeService($q, treeResource, iconHelper, notificationsService, eventsS * * @description * Gets the node's tree alias, this is done by looking up the meta-data of the current node's root node - * @param {object} treeNode to retrive tree alias from + * @param {object} treeNode to retrieve tree alias from */ getTreeAlias: function (treeNode) { var root = this.getTreeRoot(treeNode); diff --git a/src/Umbraco.Web.UI.Client/src/common/services/umbdataformatter.service.js b/src/Umbraco.Web.UI.Client/src/common/services/umbdataformatter.service.js index 4db5af883e..7e4d7eaa4a 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/umbdataformatter.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/umbdataformatter.service.js @@ -269,41 +269,62 @@ /** formats the display model used to display the member to the model used to save the member */ formatMemberPostData: function (displayModel, action) { - //this is basically the same as for media but we need to explicitly add the username,email, password to the save model + //this is basically the same as for media but we need to explicitly add the username, email, password to the save model var saveModel = this.formatMediaPostData(displayModel, action); saveModel.key = displayModel.key; - var genericTab = _.find(displayModel.tabs, function (item) { - return item.id === 0; - }); - - //map the member login, email, password and groups - var propLogin = _.find(genericTab.properties, function (item) { - return item.alias === "_umb_login"; - }); - var propEmail = _.find(genericTab.properties, function (item) { - return item.alias === "_umb_email"; - }); - var propPass = _.find(genericTab.properties, function (item) { - return item.alias === "_umb_password"; - }); - var propGroups = _.find(genericTab.properties, function (item) { - return item.alias === "_umb_membergroup"; - }); - saveModel.email = propEmail.value.trim(); - saveModel.username = propLogin.value.trim(); - - saveModel.password = this.formatChangePasswordModel(propPass.value); - - var selectedGroups = []; - for (var n in propGroups.value) { - if (propGroups.value[n] === true) { - selectedGroups.push(n); + // Map membership properties + _.each(displayModel.membershipProperties, prop => { + switch (prop.alias) { + case '_umb_login': + saveModel.username = prop.value.trim(); + break; + case '_umb_email': + saveModel.email = prop.value.trim(); + break; + case '_umb_password': + saveModel.password = this.formatChangePasswordModel(prop.value); + break; + case '_umb_membergroup': + saveModel.memberGroups = _.keys(_.pick(prop.value, value => value === true)); + break; } - } - saveModel.memberGroups = selectedGroups; + }); + + // saveModel.password = this.formatChangePasswordModel(propPass.value); + // + // var selectedGroups = []; + // for (var n in propGroups.value) { + // if (propGroups.value[n] === true) { + // selectedGroups.push(n); + // } + // } + // saveModel.memberGroups = selectedGroups; + + // Map custom member provider properties + var memberProviderPropAliases = _.pairs(displayModel.fieldConfig); + _.each(displayModel.tabs, tab => { + _.each(tab.properties, prop => { + var foundAlias = _.find(memberProviderPropAliases, item => prop.alias === item[1]); + if (foundAlias) { + // we know the current property matches an alias, now we need to determine which membership provider property it was for + // by looking at the key + switch (foundAlias[0]) { + case "umbracoMemberLockedOut": + saveModel.isLockedOut = Object.toBoolean(prop.value); + break; + case "umbracoMemberApproved": + saveModel.isApproved = Object.toBoolean(prop.value); + break; + case "umbracoMemberComments": + saveModel.comments = prop.value; + break; + } + } + }); + }); return saveModel; }, diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-file-dropzone.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-file-dropzone.less index b5d8c3cced..284a7a8007 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-file-dropzone.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-file-dropzone.less @@ -1,6 +1,5 @@ .umb-file-dropzone { - // drop zone // tall and small version - animate height .dropzone { @@ -21,17 +20,16 @@ &.is-small { height: 100px; + .illustration { width: 200px; } } - + &.drag-over { border: 1px dashed @gray-1; } } - - // center the content of the drop zone .content { position: absolute; @@ -41,8 +39,6 @@ display: flex; flex-direction: column; } - - // file select link .file-select { background: transparent; @@ -54,11 +50,10 @@ margin-top: 10px; &:hover { - color: @ui-action-discreet-type-hover; - text-decoration: none; + color: @ui-action-discreet-type-hover; + text-decoration: none; } } - // uploading / uploaded file list .file-list { list-style: none; @@ -67,12 +62,10 @@ padding: 10px 20px; .file { - //border-bottom: 1px dashed @orange; - display: block; - width: 100%; padding: 5px 0; position: relative; border-top: 1px solid @gray-8; + &:first-child { border-top: none; } @@ -80,13 +73,21 @@ &.ng-enter { animation: fadeIn 0.5s; } + &.ng-leave { animation: fadeOut 2s; } + .file-description { color: @gray-3; font-size: 12px; + display: flex; + align-items: center; + justify-content: space-between; width: 100%; + } + + .file-messages, .file-messages span { display: block; } @@ -95,25 +96,11 @@ width: 100%; } - .file-icon { - position: absolute; - right: 0; - bottom: 0; - - .icon { - font-size: 20px; - &.ng-enter { - animation: fadeIn 0.5s; - } - &.ng-leave { - animation: fadeIn 0.5s; - } - } + .ok-all { + margin-left: auto; } } } - - // progress bars // could be moved to its own less file .file-progress { @@ -131,5 +118,4 @@ width: 0; } } - } diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-media-grid.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-media-grid.less index 71be01e6ff..d25fe62c08 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-media-grid.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-media-grid.less @@ -225,6 +225,17 @@ .umb-media-grid__list-item.selected, .umb-media-grid__list-item.selected:hover, .umb-media-grid__list-item.selected:focus { border: 2px solid #f5c1bc !important; } +.umb-media-grid__list-item-name:hover { + text-decoration:underline; +} +.umb-media-grid__list-item.-filtered:not(.-folder) { + cursor: not-allowed; + + * { + pointer-events: none; + } +} + .umb-media-grid__list-view .umb-table-cell.umb-table__name { flex: 1 1 25%; diff --git a/src/Umbraco.Web.UI.Client/src/less/main.less b/src/Umbraco.Web.UI.Client/src/less/main.less index 66afbfd73f..43911fccb1 100644 --- a/src/Umbraco.Web.UI.Client/src/less/main.less +++ b/src/Umbraco.Web.UI.Client/src/less/main.less @@ -237,7 +237,6 @@ umb-property:last-of-type .umb-control-group { } .control-description { - max-width:480px;// avoiding description becoming too wide when its placed on top of property. margin-bottom: 5px; } } diff --git a/src/Umbraco.Web.UI.Client/src/less/property-editors.less b/src/Umbraco.Web.UI.Client/src/less/property-editors.less index fd699b79d0..2805e7f79b 100644 --- a/src/Umbraco.Web.UI.Client/src/less/property-editors.less +++ b/src/Umbraco.Web.UI.Client/src/less/property-editors.less @@ -526,7 +526,9 @@ position: absolute; bottom: -20px; height: 20px; + min-width: 100px; right: 0; + text-align: right; z-index: @zindexCropperOverlay - 1; font-size: 12px; opacity: 0.7; @@ -566,12 +568,17 @@ } } -.umb-cropper .crop-controls-wrapper__icon-left { - margin-right: 10px; - -} +.umb-cropper .crop-controls-wrapper__icon-left, .umb-cropper .crop-controls-wrapper__icon-right { - margin-left: 10px; + color: @gray-3; +} + +.umb-cropper .crop-controls-wrapper__icon-left { + margin-right: 15px; +} + +.umb-cropper .crop-controls-wrapper__icon-right { + margin-left: 15px; font-size: 22px; } diff --git a/src/Umbraco.Web.UI.Client/src/views/common/drawers/help/help.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/drawers/help/help.controller.js index 5b9626c676..f69467b0a1 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/drawers/help/help.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/drawers/help/help.controller.js @@ -1,3 +1,4 @@ + (function () { "use strict"; @@ -13,6 +14,12 @@ vm.customDashboard = null; vm.tours = []; vm.systemInfoDisplay = false; + vm.labels = {}; + vm.labels.copiedSuccessInfo = ""; + vm.labels.copySuccessStatus = ""; + vm.labels.copiedErrorInfo = ""; + vm.labels.copyErrorStatus = ""; + vm.closeDrawer = closeDrawer; vm.startTour = startTour; @@ -36,6 +43,24 @@ localizationService.localize("general_help").then(function(data){ vm.title = data; }); + //Set help dashboard messages + var labelKeys = [ + "general_help", + "speechBubbles_copySuccessMessage", + "general_success", + "speechBubbles_cannotCopyInformation", + "general_error" + ]; + localizationService.localizeMany(labelKeys).then(function(resp){ + [ + vm.title, + vm.labels.copiedSuccessInfo, + vm.labels.copySuccessStatus, + vm.labels.copiedErrorInfo, + vm.labels.copyErrorStatus + ] = resp; + }); + currentUserResource.getUserData().then(function(systemInfo){ vm.systemInfo = systemInfo; let browserInfo = platformService.getBrowserInfo(); @@ -43,7 +68,7 @@ vm.systemInfo.push({name :"Browser", data: browserInfo.name + " " + browserInfo.version}); } vm.systemInfo.push({name :"Browser OS", data: getPlatform()}); - }); + } ); tourService.getGroupedTours().then(function(groupedTours) { vm.tours = groupedTours; getTourGroupCompletedPercentage(); @@ -208,22 +233,33 @@ } } function copyInformation(){ - let copyText = "\n\n\n\nCategory | Data\n-- | --\n"; + //Write start and end text for table formatting in github issues + let copyStartText = "\n\n\n\nCategory | Data\n-- | --\n"; + let copyEndText = "\n\n\n"; + + let copyText = copyStartText; vm.systemInfo.forEach(function (info){ copyText += info.name + " | " + info.data + "\n"; }); - copyText += "\n\n\n" - navigator.clipboard.writeText(copyText); - if(copyText != null){ - notificationsService.success("Copied!", "Your system information is now in your clipboard"); + + copyText += copyEndText; + + // Check if copyText is only start + end text + // if it is something went wrong and we will not copy to clipboard + let emptyCopyText = copyStartText + copyEndText; + if(copyText !== emptyCopyText) { + notificationsService.success(vm.labels.copySuccessStatus, vm.labels.copiedSuccessInfo); + navigator.clipboard.writeText(copyText); } - else{ - notificationsService.error("Error", "Could not copy system information"); + else { + notificationsService.error(vm.labels.copyErrorStatus, vm.labels.copiedErrorInfo); } } + function getPlatform() { return window.navigator.platform; } + evts.push(eventsService.on("appState.tour.complete", function (event, tour) { tourService.getGroupedTours().then(function(groupedTours) { vm.tours = groupedTours; @@ -239,8 +275,8 @@ }); oninit(); - } angular.module("umbraco").controller("Umbraco.Drawers.Help", HelpDrawerController); + })(); diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/blockeditor/blockeditor.html b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/blockeditor/blockeditor.html index 8fe5526c53..ed705db26b 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/blockeditor/blockeditor.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/blockeditor/blockeditor.html @@ -6,6 +6,7 @@
- + +
diff --git a/src/Umbraco.Web.UI.Client/src/views/components/umb-content-grid.html b/src/Umbraco.Web.UI.Client/src/views/components/umb-content-grid.html index 5ed778cf16..90f7b11335 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/umb-content-grid.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/umb-content-grid.html @@ -18,7 +18,7 @@
  • -
    :
    +
    Status:
  • diff --git a/src/Umbraco.Web.UI.Client/src/views/components/umb-media-grid.html b/src/Umbraco.Web.UI.Client/src/views/components/umb-media-grid.html index 77ace4af70..3b486e4d11 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/umb-media-grid.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/umb-media-grid.html @@ -79,19 +79,22 @@
-
-
+
+
- - - {{item.name}} +
+ + {{item.name}} +
{{item.updateDate | date:'medium'}} diff --git a/src/Umbraco.Web.UI.Client/src/views/components/upload/umb-file-dropzone.html b/src/Umbraco.Web.UI.Client/src/views/components/upload/umb-file-dropzone.html index f658ecad4c..6581fca14d 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/upload/umb-file-dropzone.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/upload/umb-file-dropzone.html @@ -1,21 +1,20 @@
-
+ ng-hide="hideDropzone === 'true'" + ng-model="filesHolder" + ngf-change="handleFiles($files, $event, $invalidFiles)" + class="dropzone" + ngf-drag-over-class="'drag-over'" + ngf-multiple="true" + ngf-allow-dir="true" + ngf-pattern="{{ accept }}" + ngf-max-size="{{ maxFileSize }}" + ng-class="{'is-small': compact !=='false' || (processed.length + queue.length) > 0 }"> -
+

Drag and drop your file(s) into the area

@@ -24,82 +23,69 @@ -
-
    +
      - -
    • - -
      {{ file.name }}
      +
    • +
      + + +
      +
    • - -
      +
    • + +
      +
      + {{ file.name }} + + {{message.header}}: {{message.message}} + + "{{maxFileSize}}" + +
      + + + -
      + + + +
    • - - -
      {{currentFile.name}}
      - - +
      {{currentFile.name}} {{currentFile.uploadProgress + '%'}}
      - +
    • - -
    • - - -
      {{queued.name}}
      -
    • - -
    • - - -
      - - {{file.name}} - - - Cannot upload this file, it does not have an approved file type - Max file size is "{{maxFileSize}}" - - - - {{file.serverErrorMessage}} - - -
      - - -
      - -
      - +
    • +
      {{ file.name }}
    • +
    -
diff --git a/src/Umbraco.Web.UI.Client/src/views/content/content.rights.controller.js b/src/Umbraco.Web.UI.Client/src/views/content/content.rights.controller.js index f29c25d525..96c2ef0a80 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/content.rights.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/content/content.rights.controller.js @@ -11,7 +11,7 @@ vm.removedUserGroups = []; vm.viewState = "manageGroups"; vm.labels = {}; - + vm.initialState = {}; vm.setViewSate = setViewSate; vm.editPermissions = editPermissions; vm.setPermissions = setPermissions; @@ -45,6 +45,21 @@ assignGroupPermissions(group); } }); + vm.initialState = angular.copy(userGroups); + } + + function resetData() { + vm.selectedUserGroups = []; + vm.availableUserGroups = angular.copy(vm.initialState); + vm.availableUserGroups.forEach(function (group) { + if (group.permissions) { + //if there's explicit permissions assigned than it's selected + group.selected = false; + assignGroupPermissions(group); + } + }); + currentForm = angularHelper.getCurrentForm($scope); + } function setViewSate(state) { @@ -114,6 +129,7 @@ function cancelManagePermissions() { setViewSate("manageGroups"); + resetData(); } function formatSaveModel(permissionsSave, selectedUserGroups, removedUserGroups) { diff --git a/src/Umbraco.Web.UI.Client/src/views/dashboard/content/overlays/delete.html b/src/Umbraco.Web.UI.Client/src/views/dashboard/content/overlays/delete.html index 0a5de2b9e7..e3fa61ccfa 100644 --- a/src/Umbraco.Web.UI.Client/src/views/dashboard/content/overlays/delete.html +++ b/src/Umbraco.Web.UI.Client/src/views/dashboard/content/overlays/delete.html @@ -6,6 +6,6 @@ Redirected To: {{model.redirect.destinationUrl}}
- ? + Are you sure you want to delete?
diff --git a/src/Umbraco.Web.UI.Client/src/views/dashboard/content/redirecturls.html b/src/Umbraco.Web.UI.Client/src/views/dashboard/content/redirecturls.html index 90f9215f7e..284a879f07 100644 --- a/src/Umbraco.Web.UI.Client/src/views/dashboard/content/redirecturls.html +++ b/src/Umbraco.Web.UI.Client/src/views/dashboard/content/redirecturls.html @@ -95,18 +95,31 @@
- -
No redirects have been made
- When a published page gets renamed or moved a redirect will automatically be made to the new page -
+ + - - Sorry, we can not find what you are looking for. - + +
+ No redirects have been made +
+ + When a published page gets renamed or moved a redirect will + automatically be made to the new page + +
+ +
+
+ + + + + + Sorry, we can not find what you are looking for. + + + +
- + Used in Media Types
diff --git a/src/Umbraco.Web.UI.Client/src/views/dictionary/list.html b/src/Umbraco.Web.UI.Client/src/views/dictionary/list.html index 14c7bb4c5c..14628a49e3 100644 --- a/src/Umbraco.Web.UI.Client/src/views/dictionary/list.html +++ b/src/Umbraco.Web.UI.Client/src/views/dictionary/list.html @@ -36,7 +36,7 @@ - + @@ -54,8 +54,8 @@ class="{{ column.hasTranslation ? 'color-green' : 'color-red' }}"> - - + Has translation + Missing translation diff --git a/src/Umbraco.Web.UI.Client/src/views/documentTypes/views/listview/listview.html b/src/Umbraco.Web.UI.Client/src/views/documentTypes/views/listview/listview.html index 7eea4d541b..6c7ae4d1a0 100644 --- a/src/Umbraco.Web.UI.Client/src/views/documentTypes/views/listview/listview.html +++ b/src/Umbraco.Web.UI.Client/src/views/documentTypes/views/listview/listview.html @@ -2,13 +2,13 @@
-
- +
Enable list view
+ Configures the content item to show a sortable and searchable list of its children, the children will not be shown in the tree
- diff --git a/src/Umbraco.Web.UI.Client/src/views/documentTypes/views/permissions/permissions.html b/src/Umbraco.Web.UI.Client/src/views/documentTypes/views/permissions/permissions.html index fdbca299b2..3460180ce6 100644 --- a/src/Umbraco.Web.UI.Client/src/views/documentTypes/views/permissions/permissions.html +++ b/src/Umbraco.Web.UI.Client/src/views/documentTypes/views/permissions/permissions.html @@ -96,28 +96,29 @@
-
- +
History cleanup
+ Allow override the global settings for when history versions are removed.
- -
-

-
+ +
+

+ NOTE! The cleanup of historically content versions are disabled globally. These settings will not take effect before it is enabled. +

+
- - - + + + - - - + + + - - - -
+ + +
diff --git a/src/Umbraco.Web.UI.Client/src/views/logViewer/overview.controller.js b/src/Umbraco.Web.UI.Client/src/views/logViewer/overview.controller.js index d5b4a7ba24..bbcb30368f 100644 --- a/src/Umbraco.Web.UI.Client/src/views/logViewer/overview.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/logViewer/overview.controller.js @@ -138,14 +138,21 @@ vm.commonLogMessages = data; }); - var logLevel = logViewerResource.getLogLevel().then(function(data) { - vm.logLevel = data; - const index = vm.logTypeLabels.findIndex(x => vm.logLevel.startsWith(x)); - vm.logLevelColor = index > -1 ? vm.logTypeColors[index] : '#000'; + var logLevels = logViewerResource.getLogLevels().then(function(data) { + vm.logLevels = {}; + vm.logLevelsCount = 0; + + for (let [key, value] of Object.entries(data)) { + const index = vm.logTypeLabels.findIndex(x => value.startsWith(x)); + if (index > -1) { + vm.logLevels[key] = index; + vm.logLevelsCount++; + } + } }); // Set loading indicator to false when these 3 queries complete - $q.all([savedSearches, numOfErrors, logCounts, commonMsgs, logLevel]).then(function () { + $q.all([savedSearches, numOfErrors, logCounts, commonMsgs, logLevels]).then(function () { vm.loading = false; }); diff --git a/src/Umbraco.Web.UI.Client/src/views/logViewer/overview.html b/src/Umbraco.Web.UI.Client/src/views/logViewer/overview.html index d3f2f86428..eb018db541 100644 --- a/src/Umbraco.Web.UI.Client/src/views/logViewer/overview.html +++ b/src/Umbraco.Web.UI.Client/src/views/logViewer/overview.html @@ -104,8 +104,13 @@ - - {{ vm.logLevel }} + +
+

{{ vm.logTypeLabels[logTypeIndex] }}

+

+ {{sink}}: {{ vm.logTypeLabels[logTypeIndex] }} +

+
diff --git a/src/Umbraco.Web.UI.Client/src/views/logViewer/search.controller.js b/src/Umbraco.Web.UI.Client/src/views/logViewer/search.controller.js index ea803919be..c9adeb8d5c 100644 --- a/src/Umbraco.Web.UI.Client/src/views/logViewer/search.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/logViewer/search.controller.js @@ -124,7 +124,6 @@ vm.logOptions.orderDirection = 'Descending'; vm.fromDatePickerConfig = { - pickDate: true, pickTime: true, useSeconds: false, useCurrent: false, @@ -138,7 +137,6 @@ }; vm.toDatePickerConfig = { - pickDate: true, pickTime: true, useSeconds: false, format: "YYYY-MM-DD HH:mm", diff --git a/src/Umbraco.Web.UI.Client/src/views/logViewer/search.html b/src/Umbraco.Web.UI.Client/src/views/logViewer/search.html index 5e2583ab7a..f506ab11ae 100644 --- a/src/Umbraco.Web.UI.Client/src/views/logViewer/search.html +++ b/src/Umbraco.Web.UI.Client/src/views/logViewer/search.html @@ -150,67 +150,72 @@

Total Items: {{ vm.logItems.totalItems }}

-
Dictionary items
Name
- +
Logs Total Items: {{ vm.logItems.totalItems }}
- - - + + - - - - - - + + + + + + - @@ -264,12 +268,11 @@
+ page-number="vm.logItems.pageNumber" + total-pages="vm.logItems.totalPages" + on-change="vm.changePageNumber(pageNumber)">
- diff --git a/src/Umbraco.Web.UI.Client/src/views/media/sort.html b/src/Umbraco.Web.UI.Client/src/views/media/sort.html index a2ba7c1bba..e0187be6cb 100644 --- a/src/Umbraco.Web.UI.Client/src/views/media/sort.html +++ b/src/Umbraco.Web.UI.Client/src/views/media/sort.html @@ -49,7 +49,7 @@ - + This node has no child nodes to sort diff --git a/src/Umbraco.Web.UI.Client/src/views/mediaTypes/views/permissions/permissions.html b/src/Umbraco.Web.UI.Client/src/views/mediaTypes/views/permissions/permissions.html index 3fe28e49b3..cb95b4707c 100644 --- a/src/Umbraco.Web.UI.Client/src/views/mediaTypes/views/permissions/permissions.html +++ b/src/Umbraco.Web.UI.Client/src/views/mediaTypes/views/permissions/permissions.html @@ -3,8 +3,8 @@
-
- +
Allow as root
+ Allow editors to create content of this type in the root of the content tree.
-
- +
Allowed child node types
+ Allow content of the specified types to be created underneath content of this type.
- +
diff --git a/src/Umbraco.Web.UI.Client/src/views/member/apps/content/content.controller.js b/src/Umbraco.Web.UI.Client/src/views/member/apps/content/content.controller.js index 6665043772..ffa8a34f86 100644 --- a/src/Umbraco.Web.UI.Client/src/views/member/apps/content/content.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/member/apps/content/content.controller.js @@ -9,7 +9,6 @@ vm.activeTabAlias = null; vm.setActiveTab = setActiveTab; - vm.hideSystemProperties = hideSystemProperties; $scope.$watchCollection('content.tabs', (newValue) => { @@ -33,15 +32,6 @@ vm.tabs.forEach(tab => tab.active = false); tab.active = true; } - - function hideSystemProperties (property) { - // hide some specific, known properties by alias - if (property.alias === "_umb_id" || property.alias === "_umb_doctype") { - return false; - } - // hide all label properties with the alias prefix "umbracoMember" (e.g. "umbracoMemberFailedPasswordAttempts") - return property.view !== "readonlyvalue" || property.alias.startsWith('umbracoMember') === false; - } } angular.module("umbraco").controller("Umbraco.Editors.Member.Apps.ContentController", MemberAppContentController); diff --git a/src/Umbraco.Web.UI.Client/src/views/member/apps/content/content.html b/src/Umbraco.Web.UI.Client/src/views/member/apps/content/content.html index 46394d10c9..a7ac38df7c 100644 --- a/src/Umbraco.Web.UI.Client/src/views/member/apps/content/content.html +++ b/src/Umbraco.Web.UI.Client/src/views/member/apps/content/content.html @@ -6,13 +6,8 @@ - - - - + + @@ -21,23 +16,18 @@ class="umb-group-panel" data-element="group-{{group.alias}}" ng-repeat="group in content.tabs" - ng-if="group.type === 'Group'" - ng-show="group.parentAlias === vm.activeTabAlias || vm.tabs.length === 0"> - + ng-if="group.type === 'Group'" ng-show="group.parentAlias === vm.activeTabAlias || vm.tabs.length === 0">
{{ group.label }}
-
-
-
diff --git a/src/Umbraco.Web.UI.Client/src/views/member/apps/membership/membership.html b/src/Umbraco.Web.UI.Client/src/views/member/apps/membership/membership.html new file mode 100644 index 0000000000..d541bb2c56 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/member/apps/membership/membership.html @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/Umbraco.Web.UI.Client/src/views/prevalueeditors/mediafolderpicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/prevalueeditors/mediafolderpicker.controller.js index 1f39c2423e..569fc1007b 100644 --- a/src/Umbraco.Web.UI.Client/src/views/prevalueeditors/mediafolderpicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/prevalueeditors/mediafolderpicker.controller.js @@ -4,7 +4,7 @@ function mediaFolderPickerController($scope, editorService, entityResource) { $scope.folderName = ""; - function retriveFolderData() { + function retrieveFolderData() { var id = $scope.model.value; @@ -21,7 +21,7 @@ function mediaFolderPickerController($scope, editorService, entityResource) { } - retriveFolderData(); + retrieveFolderData(); $scope.add = function() { @@ -35,7 +35,7 @@ function mediaFolderPickerController($scope, editorService, entityResource) { $scope.model.value = model.selection[0].udi; - retriveFolderData(); + retrieveFolderData(); editorService.close(); }, @@ -48,7 +48,7 @@ function mediaFolderPickerController($scope, editorService, entityResource) { $scope.remove = function () { $scope.model.value = null; - retriveFolderData(); + retrieveFolderData(); }; } diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/umbBlockListPropertyEditor.component.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/umbBlockListPropertyEditor.component.js index 11ef37029c..48385a3cd4 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/umbBlockListPropertyEditor.component.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/umbBlockListPropertyEditor.component.js @@ -576,7 +576,7 @@ vm.clipboardItems = []; - var entriesForPaste = clipboardService.retriveEntriesOfType(clipboardService.TYPES.ELEMENT_TYPE, vm.availableContentTypesAliases); + var entriesForPaste = clipboardService.retrieveEntriesOfType(clipboardService.TYPES.ELEMENT_TYPE, vm.availableContentTypesAliases); entriesForPaste.forEach(function (entry) { var pasteEntry = { type: clipboardService.TYPES.ELEMENT_TYPE, @@ -596,7 +596,7 @@ blockPickerModel.clipboardItems.push(pasteEntry); }); - var entriesForPaste = clipboardService.retriveEntriesOfType(clipboardService.TYPES.BLOCK, vm.availableContentTypesAliases); + var entriesForPaste = clipboardService.retrieveEntriesOfType(clipboardService.TYPES.BLOCK, vm.availableContentTypesAliases); entriesForPaste.forEach(function (entry) { var pasteEntry = { type: clipboardService.TYPES.BLOCK, diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/boolean/boolean.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/boolean/boolean.html index aa47e0c667..a0b3f8494d 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/boolean/boolean.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/boolean/boolean.html @@ -4,6 +4,7 @@ {{value.alias}} - {{value.width}}px x {{value.height}}px + {{value.width}} × {{value.height}} px User defined  diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/imagecropper/imagecropper.prevalues.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/imagecropper/imagecropper.prevalues.html index 7d08ce43e4..50e4f3ece1 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/imagecropper/imagecropper.prevalues.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/imagecropper/imagecropper.prevalues.html @@ -62,7 +62,7 @@
-

{{item.alias}} ({{item.width}}px × {{item.height}}px)

+

{{item.alias}} ({{item.width}} × {{item.height}}px)

diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/layouts.prevalues.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/layouts.prevalues.controller.js index db79c31cfd..d8320711c9 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/layouts.prevalues.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/layouts.prevalues.controller.js @@ -9,55 +9,66 @@ (function() { "use strict"; - function ListViewLayoutsPreValsController($scope, editorService) { + function ListViewLayoutsPreValsController($scope, editorService, localizationService, overlayService) { - var vm = this; - vm.focusLayoutName = false; + var vm = this; + vm.focusLayoutName = false; - vm.layoutsSortableOptions = { - axis: "y", - containment: "parent", - distance: 10, - tolerance: "pointer", - opacity: 0.7, - scroll: true, - cursor: "move", - handle: ".list-view-layout__sort-handle" - }; + vm.layoutsSortableOptions = { + axis: "y", + containment: "parent", + distance: 10, + tolerance: "pointer", + opacity: 0.7, + scroll: true, + cursor: "move", + handle: ".list-view-layout__sort-handle" + }; - vm.addLayout = addLayout; - vm.showPrompt = showPrompt; - vm.hidePrompt = hidePrompt; - vm.removeLayout = removeLayout; - vm.openIconPicker = openIconPicker; + vm.addLayout = addLayout; + vm.removeLayout = removeLayout; + vm.openIconPicker = openIconPicker; - function addLayout() { + function addLayout() { - vm.focusLayoutName = false; + vm.focusLayoutName = false; - var layout = { - "name": "", - "path": "", - "icon": "icon-stop", - "selected": true - }; + var layout = { + "name": "", + "path": "", + "icon": "icon-stop", + "selected": true + }; - $scope.model.value.push(layout); - } + $scope.model.value.push(layout); + } - function showPrompt(layout) { - layout.deletePrompt = true; - } + function removeLayout(template, index, event) { - function hidePrompt(layout) { - layout.deletePrompt = false; - } + const dialog = { + view: "views/propertyEditors/listview/overlays/removeListViewLayout.html", + layout: template, + submitButtonLabelKey: "defaultdialogs_yesRemove", + submitButtonStyle: "danger", + submit: function (model) { + $scope.model.value.splice(index, 1); + overlayService.close(); + }, + close: function () { + overlayService.close(); + } + }; - function removeLayout($index, layout) { - $scope.model.value.splice($index, 1); - } + localizationService.localize("general_remove").then(value => { + dialog.title = value; + overlayService.open(dialog); + }); - function openIconPicker(layout) { + event.preventDefault(); + event.stopPropagation(); + } + + function openIconPicker(layout) { var iconPicker = { icon: layout.icon.split(' ')[0], color: layout.icon.split(' ')[1], @@ -80,7 +91,7 @@ editorService.iconPicker(iconPicker); } - } + } angular.module("umbraco").controller("Umbraco.PrevalueEditors.ListViewLayoutsPreValsController", ListViewLayoutsPreValsController); diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/layouts.prevalues.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/layouts.prevalues.html index 95bdb3644f..36ebc45f38 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/layouts.prevalues.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/layouts.prevalues.html @@ -33,18 +33,13 @@
- + -
- - - -
+
+ +
diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/overlays/removeListViewLayout.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/overlays/removeListViewLayout.html new file mode 100644 index 0000000000..80080a82b0 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/overlays/removeListViewLayout.html @@ -0,0 +1,14 @@ +
+
+ You are deleting the layout {{model.layout.name}}. +
+ +

+ + Modifying layout will result in loss of data for any existing content that is based on this configuration. + +

+ + Are you sure you want to delete? + +
\ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker/mediapicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker/mediapicker.controller.js index bdd3251ca7..c7c803fa9a 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker/mediapicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker/mediapicker.controller.js @@ -305,7 +305,7 @@ angular.module('umbraco').controller("Umbraco.PropertyEditors.MediaPickerControl clipboardService.clearEntriesOfType(clipboardService.TYPES.Media, allowedTypes); }; - mediaPicker.clipboardItems = clipboardService.retriveEntriesOfType(clipboardService.TYPES.MEDIA, allowedTypes); + mediaPicker.clipboardItems = clipboardService.retrieveEntriesOfType(clipboardService.TYPES.MEDIA, allowedTypes); mediaPicker.clipboardItems.sort( (a, b) => { return b.date - a.date }); diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker/mediapicker.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker/mediapicker.html index 938bd7b2a2..a1d670a06f 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker/mediapicker.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker/mediapicker.html @@ -1,14 +1,14 @@
-

-

+

You have picked a media item currently deleted or in the recycle bin

+

You have picked media items currently deleted or in the recycle bin

  • - + Trashed

    diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker3/umbMediaPicker3PropertyEditor.component.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker3/umbMediaPicker3PropertyEditor.component.js index 36a7c61620..2699fa479b 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker3/umbMediaPicker3PropertyEditor.component.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker3/umbMediaPicker3PropertyEditor.component.js @@ -193,7 +193,7 @@ clipboardService.clearEntriesOfType(clipboardService.TYPES.Media, vm.allowedTypes || null); }; - mediaPicker.clipboardItems = clipboardService.retriveEntriesOfType(clipboardService.TYPES.MEDIA, vm.allowedTypes || null); + mediaPicker.clipboardItems = clipboardService.retrieveEntriesOfType(clipboardService.TYPES.MEDIA, vm.allowedTypes || null); mediaPicker.clipboardItems.sort( (a, b) => { return b.date - a.date }); diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/membergrouppicker/membergrouppicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/membergrouppicker/membergrouppicker.controller.js index e64633ea31..fee3853351 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/membergrouppicker/membergrouppicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/membergrouppicker/membergrouppicker.controller.js @@ -1,6 +1,6 @@ -//this controller simply tells the dialogs service to open a memberPicker window +//this controller tells the dialogs service to open a memberPicker window //with a specified callback, this callback will receive an object with a selection on it -function memberGroupPicker($scope, editorService, memberGroupResource){ +function memberGroupPicker($scope, editorService, memberGroupResource, localizationService, overlayService){ var vm = this; @@ -13,15 +13,32 @@ function memberGroupPicker($scope, editorService, memberGroupResource){ return str.replace(rgxtrim, ''); } + var removeAllEntriesAction = { + labelKey: 'clipboard_labelForRemoveAllEntries', + labelTokens: [], + icon: 'trash', + method: removeAllEntries, + isDisabled: true + }; + $scope.renderModel = []; $scope.allowRemove = true; $scope.groupIds = []; + if ($scope.model.config && $scope.umbProperty) { + $scope.umbProperty.setPropertyActions([ + removeAllEntriesAction + ]); + } + if ($scope.model.value) { var groupIds = $scope.model.value.split(','); + memberGroupResource.getByIds(groupIds).then(function(groups) { $scope.renderModel = groups; }); + + removeAllEntriesAction.isDisabled = groupIds.length === 0; } function setDirty() { @@ -39,8 +56,14 @@ function memberGroupPicker($scope, editorService, memberGroupResource){ : [model.selectedMemberGroup], function (id) { return parseInt(id); } ); + + var currIds = renderModelIds(); + // figure out which groups are new and fetch them - var newGroupIds = _.difference(selectedGroupIds, renderModelIds()); + var newGroupIds = _.difference(selectedGroupIds, currIds); + + removeAllEntriesAction.isDisabled = currIds.length === 0 && newGroupIds.length === 0; + if (newGroupIds && newGroupIds.length) { memberGroupResource.getByIds(newGroupIds).then(function (groups) { $scope.renderModel = _.union($scope.renderModel, groups); @@ -62,20 +85,41 @@ function memberGroupPicker($scope, editorService, memberGroupResource){ function remove(index) { $scope.renderModel.splice(index, 1); + + var currIds = renderModelIds(); + removeAllEntriesAction.isDisabled = currIds.length === 0; + setDirty(); } function clear() { $scope.renderModel = []; + removeAllEntriesAction.isDisabled = true; + setDirty(); } - function renderModelIds() { - return _.map($scope.renderModel, function (i) { - return i.id; + function removeAllEntries() { + localizationService.localizeMany(["content_nestedContentDeleteAllItems", "general_delete"]).then(data => { + overlayService.confirmDelete({ + title: data[1], + content: data[0], + close: () => { + overlayService.close(); + }, + submit: () => { + vm.clear(); + overlayService.close(); + } + }); }); } + function renderModelIds() { + var currIds = $scope.renderModel.map(i => i.id); + return currIds; + } + var unsubscribe = $scope.$on("formSubmitting", function (ev, args) { $scope.model.value = trim(renderModelIds().join(), ","); }); diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.controller.js index 3510da73a6..0c4da2c4ab 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.controller.js @@ -241,7 +241,7 @@ dialog.pasteItems = []; - var entriesForPaste = clipboardService.retriveEntriesOfType(clipboardService.TYPES.ELEMENT_TYPE, contentTypeAliases); + var entriesForPaste = clipboardService.retrieveEntriesOfType(clipboardService.TYPES.ELEMENT_TYPE, contentTypeAliases); _.each(entriesForPaste, function (entry) { dialog.pasteItems.push({ date: entry.date, diff --git a/src/Umbraco.Web.UI/umbraco/UmbracoBackOffice/Default.cshtml b/src/Umbraco.Web.UI/umbraco/UmbracoBackOffice/Default.cshtml index 50394bc2a1..ce4a8e3066 100644 --- a/src/Umbraco.Web.UI/umbraco/UmbracoBackOffice/Default.cshtml +++ b/src/Umbraco.Web.UI/umbraco/UmbracoBackOffice/Default.cshtml @@ -45,8 +45,15 @@
+ Timestamp - +   LevelMachineLevelMachine Message
{{ log.Timestamp | date:'medium' }}{{ log.Level }}{{ log.Properties.MachineName.Value }}{{ log.RenderedMessage }}
{{ log.Timestamp | date:'medium' }}{{ log.Level }}{{ log.Properties.MachineName.Value }}{{ log.RenderedMessage }}
-
-

Exception

-

{{log.Exception}}

+
+ +
+
{{ log.Exception }}
-

Properties

- - + - + - +
{{log.RenderedMessage}} Properties for {{log.Timestamp | date:'medium'}}
TimestampTimestamp {{log.Timestamp}}
@MessageTemplate@MessageTemplate {{log.MessageTemplateText}}
{{key}}{{key}} - - - - + + + + + {{val.Value}} + {{val.Value}}
- -