diff --git a/.github/CODE_OF_CONDUCT.md b/.github/CODE_OF_CONDUCT.md index 7a81989037..e97b03e7e3 100644 --- a/.github/CODE_OF_CONDUCT.md +++ b/.github/CODE_OF_CONDUCT.md @@ -43,6 +43,8 @@ Community leaders (e.g. Meetup & festival organizers, moderators, maintainers, . Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate. +Specific enforcement steps are listed in the [Code of Conduct Enforcement Guidelines](https://github.com/umbraco/Umbraco-CMS/blob/v8/contrib/.github/CODE_OF_CONDUCT_ENFORCEMENT.md) document which is an appendix of this document, updated and maintained by the Code of Conduct Team. + ## Scope This Code of Conduct applies within all community spaces and events supported by Umbraco HQ or using the Umbraco name. It also applies when an individual is officially representing the community in public spaces. @@ -58,6 +60,8 @@ Or alternatively, you can reach out directly to any of the team members behind t The review process is done with full respect for the privacy and security of the reporter of any incident. +People with a conflict of interest should exclude themselves or if necessary be excluded by the other team members. + ## Enforcement Guidelines Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct: diff --git a/.github/CODE_OF_CONDUCT_ENFORCEMENT.md b/.github/CODE_OF_CONDUCT_ENFORCEMENT.md new file mode 100644 index 0000000000..2bb45644c2 --- /dev/null +++ b/.github/CODE_OF_CONDUCT_ENFORCEMENT.md @@ -0,0 +1,57 @@ +# Umbraco Code of Conduct Enforcement guidelines - Consequence Ladder + +These are the steps followed by the [Umbraco Code of Conduct Team](https://github.com/umbraco/Umbraco-CMS/blob/v8/contrib/.github/CODE_OF_CONDUCT.md) when we respond to an issue or incident brought to our attention by a community member. + +This is an appendix to the Code of Conduct and is updated and maintained by the Code of Conduct Team. + +To make sure that all reports will be reviewed and investigated promptly and fairly, as highlighted in the Umbraco Code of Conduct, we are following [Mozilla’s Consequence Ladder approach](https://github.com/mozilla/inclusion/blob/master/code-of-conduct-enforcement/consequence-ladder.md). + +This approach helps the Team enforce the Code of Conduct in a structured manner and can be used as a way of communicating escalation. Each time the Team takes an action (warning, ban) the individual is made aware of future consequences. The Team can either follow the order of the levels in the ladder or decide to jump levels. When needed, the team can go directly to a permanent ban. + +**Level 0: No Action** +Recommendations do not indicate a violation of the Code of Conduct. + +**Level 1: Simple Warning Issued** +A private, written warning from the Code of Conduct Team, with clarity of violation, consequences of continued behavior. + +**Level 2: Warning** +A private, written warning from the Code of Conduct Team, with clarity of violation, consequences of continued behavior. Additionally: + +* Communication of next-level consequences if behaviors are repeated (according to this ladder). + +**Level 3: Warning + Mandatory Cooling Off Period (Access Retained)** +A private warning from the Code of Conduct Team, with clarity of violation, consequences of continued behavior. Additionally: + +* Request to avoid interaction on community messaging platforms (public forums, Our, commenting on issues). + * This includes avoiding any interactions in any Umbraco channels, spaces/offices, as well as external channels like social media (e.g. Twitter, Facebook, LinkedIn). For example, 'following/liking/retweeting' would be considered a violation of these terms, and consequence would escalate according to this ladder. +* Require they do not interact with others in the report, or those who they suspect are involved in the report. +* Suggestions for 'out of office' type of message on platforms, to reduce curiosity, or suspicion among those not involved. + +**Level 4: Temporary Ban (Access Revoked)** +Private communication of ban from the Code of Conduct Team, with clarity of violation, consequences of continued behavior. Additionally: + +* 3-6 months imposed break. +* All accounts deactivated, or blocked during this time (Our, HQ Slack if applicable). +* Require to avoid interaction on community messaging platforms (public forums, Our, commenting on issues). + * This includes avoiding any interactions in any Umbraco channels, spaces/offices, as well as external channels like social media (e.g. Twitter, Facebook, LinkedIn). For example, 'following/liking/retweeting' would be considered a violation of these terms, and consequence would escalate according to this ladder. +* All community leadership roles (e.g. Community Teams, Meetup/festival organizer, Commit right on Github..) suspended. (onboarding/reapplication required outside of this process) +* No attendance at Umbraco events during the ban period. +* Not allowed to enter Umbraco HQ offices during the ban period. +* Permission to use the MVP title, if applicable, is revoked during this ban period. +* The community leaders running events and other initiatives are informed of the ban. + +**Level 5: Permanent Ban** +Private communication of ban from the Code of Conduct Team, with clarity of violation, consequences of continued behavior. Additionally: + +* All accounts deactivated permanently. +* No attendance at Umbraco events going forward. +* Not allowed to enter Umbraco HQ offices permanently. +* All community leadership roles (e.g. Community Teams, Meetup/festival organizer, Commit right on Github..) permanently suspended. +* Permission to use the MVP title, if applicable, revoked. +* The community leaders running events and other initiatives are informed of the ban. + + +Sources: +* [Mozilla Code of Conduct - Enforcement Consequence Ladder](https://github.com/mozilla/inclusion/blob/master/code-of-conduct-enforcement/consequence-ladder.md) +* [Drupal Conflict Resolution Policy and Process](https://www.drupal.org/conflict-resolution) +* [Django Code of Conduct - Enforcement Manual](https://www.djangoproject.com/conduct/enforcement-manual/) diff --git a/.github/workflows/codeql-config.yml b/.github/workflows/codeql-config.yml index 59b55e48ec..7bac345491 100644 --- a/.github/workflows/codeql-config.yml +++ b/.github/workflows/codeql-config.yml @@ -9,5 +9,6 @@ paths-ignore: - Umbraco.Tests.AcceptanceTest - Umbraco.Tests.Benchmarks - bin + - build.tmp paths: - - src \ No newline at end of file + - src diff --git a/build/NuSpecs/UmbracoCms.Web.nuspec b/build/NuSpecs/UmbracoCms.Web.nuspec index 82d15d2b95..d8815bab63 100644 --- a/build/NuSpecs/UmbracoCms.Web.nuspec +++ b/build/NuSpecs/UmbracoCms.Web.nuspec @@ -28,7 +28,7 @@ - + @@ -42,7 +42,7 @@ - + diff --git a/src/SolutionInfo.cs b/src/SolutionInfo.cs index 3ecfd20f03..2a7386cb45 100644 --- a/src/SolutionInfo.cs +++ b/src/SolutionInfo.cs @@ -18,5 +18,5 @@ using System.Resources; [assembly: AssemblyVersion("8.0.0")] // these are FYI and changed automatically -[assembly: AssemblyFileVersion("8.11.1")] -[assembly: AssemblyInformationalVersion("8.11.1")] +[assembly: AssemblyFileVersion("8.13.0")] +[assembly: AssemblyInformationalVersion("8.13.0-rc")] diff --git a/src/Umbraco.Core/Cache/CacheKeys.cs b/src/Umbraco.Core/Cache/CacheKeys.cs index b8ee0e97c4..0e9a9a3862 100644 --- a/src/Umbraco.Core/Cache/CacheKeys.cs +++ b/src/Umbraco.Core/Cache/CacheKeys.cs @@ -12,5 +12,10 @@ public const string MacroContentCacheKey = "macroContent_"; // used in MacroRenderers public const string MacroFromAliasCacheKey = "macroFromAlias_"; + + public const string UserAllContentStartNodesPrefix = "AllContentStartNodes"; + public const string UserAllMediaStartNodesPrefix = "AllMediaStartNodes"; + public const string UserMediaStartNodePathsPrefix = "MediaStartNodePaths"; + public const string UserContentStartNodePathsPrefix = "ContentStartNodePaths"; } } diff --git a/src/Umbraco.Core/Composing/ComponentCollection.cs b/src/Umbraco.Core/Composing/ComponentCollection.cs index 62b240f10f..6501a3a28c 100644 --- a/src/Umbraco.Core/Composing/ComponentCollection.cs +++ b/src/Umbraco.Core/Composing/ComponentCollection.cs @@ -51,7 +51,7 @@ namespace Umbraco.Core.Composing } catch (Exception ex) { - _logger.Error(ex, "Error while terminating component {ComponentType}.", componentType.FullName); + _logger.Error(ex, "Error while terminating component {ComponentType}.", componentType.FullName); } } } diff --git a/src/Umbraco.Core/Composing/Composers.cs b/src/Umbraco.Core/Composing/Composers.cs index b2e6c9d068..1528c6760d 100644 --- a/src/Umbraco.Core/Composing/Composers.cs +++ b/src/Umbraco.Core/Composing/Composers.cs @@ -114,7 +114,7 @@ namespace Umbraco.Core.Composing // bit verbose but should help for troubleshooting //var text = "Ordered Composers: " + Environment.NewLine + string.Join(Environment.NewLine, sortedComposerTypes) + Environment.NewLine; - _logger.Debug("Ordered Composers: {SortedComposerTypes}", sortedComposerTypes); + _logger.Debug>("Ordered Composers: {SortedComposerTypes}", sortedComposerTypes); return sortedComposerTypes; } @@ -205,7 +205,7 @@ namespace Umbraco.Core.Composing catch (Exception e) { // in case of an error, force-dump everything to log - _logger.Info("Composer Report:\r\n{ComposerReport}", GetComposersReport(requirements)); + _logger.Info("Composer Report:\r\n{ComposerReport}", GetComposersReport(requirements)); _logger.Error(e, "Failed to sort composers."); throw; } diff --git a/src/Umbraco.Core/Composing/TypeFinder.cs b/src/Umbraco.Core/Composing/TypeFinder.cs index 394d9480ae..5bf9eb89a9 100644 --- a/src/Umbraco.Core/Composing/TypeFinder.cs +++ b/src/Umbraco.Core/Composing/TypeFinder.cs @@ -11,7 +11,7 @@ using System.Web.Compilation; using System.Web.Hosting; using Umbraco.Core.Composing; using Umbraco.Core.IO; - +using Umbraco.Core.Logging; namespace Umbraco.Core.Composing { /// @@ -441,7 +441,7 @@ namespace Umbraco.Core.Composing } catch (TypeLoadException ex) { - Current.Logger.Error(typeof(TypeFinder), ex, "Could not query types on {Assembly} assembly, this is most likely due to this assembly not being compatible with the current Umbraco version", assembly); + Current.Logger.Error(typeof(TypeFinder), ex, "Could not query types on {Assembly} assembly, this is most likely due to this assembly not being compatible with the current Umbraco version", assembly); continue; } @@ -507,7 +507,7 @@ namespace Umbraco.Core.Composing } catch (TypeLoadException ex) { - Current.Logger.Error(typeof(TypeFinder), ex, "Could not query types on {Assembly} assembly, this is most likely due to this assembly not being compatible with the current Umbraco version", assembly); + Current.Logger.Error(typeof(TypeFinder), ex, "Could not query types on {Assembly} assembly, this is most likely due to this assembly not being compatible with the current Umbraco version", assembly); continue; } diff --git a/src/Umbraco.Core/Composing/TypeLoader.cs b/src/Umbraco.Core/Composing/TypeLoader.cs index 6d0b1a0514..bee6436cd6 100644 --- a/src/Umbraco.Core/Composing/TypeLoader.cs +++ b/src/Umbraco.Core/Composing/TypeLoader.cs @@ -1,8 +1,9 @@ -using System; +using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Reflection; +using System.Runtime.InteropServices; using System.Runtime.Serialization; using System.Text; using System.Threading; @@ -44,7 +45,7 @@ namespace Umbraco.Core.Composing private IEnumerable _assemblies; private bool _reportedChange; private readonly string _localTempPath; - private string _fileBasePath; + private readonly Lazy _fileBasePath; /// /// Initializes a new instance of the class. @@ -69,6 +70,8 @@ namespace Umbraco.Core.Composing _localTempPath = localTempPath; _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + _fileBasePath = new Lazy(GetFileBasePath); + if (detectChanges) { //first check if the cached hash is string.Empty, if it is then we need @@ -159,7 +162,8 @@ namespace Umbraco.Core.Composing return _cachedAssembliesHash; var typesHashFilePath = GetTypesHashFilePath(); - if (!File.Exists(typesHashFilePath)) return string.Empty; + if (!File.Exists(typesHashFilePath)) + return string.Empty; var hash = File.ReadAllText(typesHashFilePath, Encoding.UTF8); @@ -338,7 +342,9 @@ namespace Umbraco.Core.Composing var typesListFilePath = GetTypesListFilePath(); if (File.Exists(typesListFilePath) == false) + { return cache; + } using (var stream = GetFileStream(typesListFilePath, FileMode.Open, FileAccess.Read, FileShare.Read, ListFileOpenReadTimeout)) using (var reader = new StreamReader(stream)) @@ -346,11 +352,21 @@ namespace Umbraco.Core.Composing while (true) { var baseType = reader.ReadLine(); - if (baseType == null) return cache; // exit - if (baseType.StartsWith("<")) break; // old xml + if (baseType == null) + { + return cache; // exit + } + + if (baseType.StartsWith("<")) + { + break; // old xml + } var attributeType = reader.ReadLine(); - if (attributeType == null) break; + if (attributeType == null) + { + break; + } var types = new List(); while (true) @@ -369,7 +385,10 @@ namespace Umbraco.Core.Composing types.Add(type); } - if (types == null) break; + if (types == null) + { + break; + } } } @@ -378,28 +397,31 @@ namespace Umbraco.Core.Composing } // internal for tests - internal string GetTypesListFilePath() => GetFileBasePath() + ".list"; + internal string GetTypesListFilePath() => _fileBasePath.Value + ".list"; - private string GetTypesHashFilePath() => GetFileBasePath() + ".hash"; + private string GetTypesHashFilePath() => _fileBasePath.Value + ".hash"; + /// + /// Used to produce the Lazy value of _fileBasePath + /// + /// private string GetFileBasePath() { - lock (_locko) + var fileBasePath = Path.Combine(_localTempPath, "TypesCache", "umbraco-types." + NetworkHelper.FileSafeMachineName); + + // ensure that the folder exists + var directory = Path.GetDirectoryName(fileBasePath); + if (directory == null) { - if (_fileBasePath != null) - return _fileBasePath; - - _fileBasePath = Path.Combine(_localTempPath, "TypesCache", "umbraco-types." + NetworkHelper.FileSafeMachineName); - - // ensure that the folder exists - var directory = Path.GetDirectoryName(_fileBasePath); - if (directory == null) - throw new InvalidOperationException($"Could not determine folder for path \"{_fileBasePath}\"."); - if (Directory.Exists(directory) == false) - Directory.CreateDirectory(directory); - - return _fileBasePath; + throw new InvalidOperationException($"Could not determine folder for path \"{fileBasePath}\"."); } + + if (Directory.Exists(directory) == false) + { + Directory.CreateDirectory(directory); + } + + return fileBasePath; } // internal for tests @@ -415,7 +437,10 @@ namespace Umbraco.Core.Composing writer.WriteLine(typeList.BaseType == null ? string.Empty : typeList.BaseType.FullName); writer.WriteLine(typeList.AttributeType == null ? string.Empty : typeList.AttributeType.FullName); foreach (var type in typeList.Types) + { writer.WriteLine(type.AssemblyQualifiedName); + } + writer.WriteLine(); } } @@ -433,16 +458,22 @@ namespace Umbraco.Core.Composing WriteCache(); } catch { /* bah - just don't die */ } - if (!_timing) _timer = null; + if (!_timing) + _timer = null; } } lock (_timerLock) { if (_timer == null) + { _timer = new Timer(TimerRelease, null, ListFileWriteThrottle, Timeout.Infinite); + } else + { _timer.Change(ListFileWriteThrottle, Timeout.Infinite); + } + _timing = true; } } @@ -475,9 +506,11 @@ namespace Umbraco.Core.Composing catch { if (--attempts == 0) + { throw; + } - _logger.Debug("Attempted to get filestream for file {Path} failed, {NumberOfAttempts} attempts left, pausing for {PauseMilliseconds} milliseconds", path, attempts, pauseMilliseconds); + _logger.Debug("Attempted to get filestream for file {Path} failed, {NumberOfAttempts} attempts left, pausing for {PauseMilliseconds} milliseconds", path, attempts, pauseMilliseconds); Thread.Sleep(pauseMilliseconds); } } @@ -498,7 +531,7 @@ namespace Umbraco.Core.Composing if (--attempts == 0) throw; - _logger.Debug("Attempted to delete file {Path} failed, {NumberOfAttempts} attempts left, pausing for {PauseMilliseconds} milliseconds", path, attempts, pauseMilliseconds); + _logger.Debug("Attempted to delete file {Path} failed, {NumberOfAttempts} attempts left, pausing for {PauseMilliseconds} milliseconds", path, attempts, pauseMilliseconds); Thread.Sleep(pauseMilliseconds); } } @@ -542,7 +575,8 @@ namespace Umbraco.Core.Composing /// attributeTypes public IEnumerable GetAssemblyAttributes(params Type[] attributeTypes) { - if (attributeTypes == null) throw new ArgumentNullException(nameof(attributeTypes)); + if (attributeTypes == null) + throw new ArgumentNullException(nameof(attributeTypes)); return AssembliesToScan.SelectMany(a => attributeTypes.SelectMany(at => a.GetCustomAttributes(at))).ToList(); } @@ -562,7 +596,9 @@ namespace Umbraco.Core.Composing public IEnumerable GetTypes(bool cache = true, IEnumerable specificAssemblies = null) { if (_logger == null) + { throw new InvalidOperationException("Cannot get types from a test/blank type loader."); + } // do not cache anything from specific assemblies cache &= specificAssemblies == null; @@ -571,7 +607,7 @@ namespace Umbraco.Core.Composing if (!typeof(IDiscoverable).IsAssignableFrom(typeof(T))) { // warn - _logger.Debug("Running a full, " + (cache ? "" : "non-") + "cached, scan for non-discoverable type {TypeName} (slow).", typeof(T).FullName); + _logger.Debug("Running a full, " + (cache ? "" : "non-") + "cached, scan for non-discoverable type {TypeName} (slow).", typeof(T).FullName); return GetTypesInternal( typeof(T), null, @@ -582,20 +618,20 @@ namespace Umbraco.Core.Composing // get IDiscoverable and always cache var discovered = GetTypesInternal( - typeof (IDiscoverable), null, + typeof(IDiscoverable), null, () => TypeFinder.FindClassesOfType(AssembliesToScan), "scanning assemblies", true); // warn if (!cache) - _logger.Debug("Running a non-cached, filter for discoverable type {TypeName} (slowish).", typeof(T).FullName); + _logger.Debug("Running a non-cached, filter for discoverable type {TypeName} (slowish).", typeof(T).FullName); // filter the cached discovered types (and maybe cache the result) return GetTypesInternal( - typeof (T), null, + typeof(T), null, () => discovered - .Where(x => typeof (T).IsAssignableFrom(x)), + .Where(x => typeof(T).IsAssignableFrom(x)), "filtering IDiscoverable", cache); } @@ -613,7 +649,9 @@ namespace Umbraco.Core.Composing where TAttribute : Attribute { if (_logger == null) + { throw new InvalidOperationException("Cannot get types from a test/blank type loader."); + } // do not cache anything from specific assemblies cache &= specificAssemblies == null; @@ -621,7 +659,7 @@ namespace Umbraco.Core.Composing // if not IDiscoverable, directly get types if (!typeof(IDiscoverable).IsAssignableFrom(typeof(T))) { - _logger.Debug("Running a full, " + (cache ? "" : "non-") + "cached, scan for non-discoverable type {TypeName} / attribute {AttributeName} (slow).", typeof(T).FullName, typeof(TAttribute).FullName); + _logger.Debug("Running a full, " + (cache ? "" : "non-") + "cached, scan for non-discoverable type {TypeName} / attribute {AttributeName} (slow).", typeof(T).FullName, typeof(TAttribute).FullName); return GetTypesInternal( typeof(T), typeof(TAttribute), @@ -632,18 +670,18 @@ namespace Umbraco.Core.Composing // get IDiscoverable and always cache var discovered = GetTypesInternal( - typeof (IDiscoverable), null, + typeof(IDiscoverable), null, () => TypeFinder.FindClassesOfType(AssembliesToScan), "scanning assemblies", true); // warn if (!cache) - _logger.Debug("Running a non-cached, filter for discoverable type {TypeName} / attribute {AttributeName} (slowish).", typeof(T).FullName, typeof(TAttribute).FullName); + _logger.Debug("Running a non-cached, filter for discoverable type {TypeName} / attribute {AttributeName} (slowish).", typeof(T).FullName, typeof(TAttribute).FullName); // filter the cached discovered types (and maybe cache the result) return GetTypesInternal( - typeof (T), typeof (TAttribute), + typeof(T), typeof(TAttribute), () => discovered .Where(x => typeof(T).IsAssignableFrom(x)) .Where(x => x.GetCustomAttributes(false).Any()), @@ -663,16 +701,18 @@ namespace Umbraco.Core.Composing where TAttribute : Attribute { if (_logger == null) + { throw new InvalidOperationException("Cannot get types from a test/blank type loader."); + } // do not cache anything from specific assemblies cache &= specificAssemblies == null; if (!cache) - _logger.Debug("Running a full, non-cached, scan for types / attribute {AttributeName} (slow).", typeof(TAttribute).FullName); + _logger.Debug("Running a full, non-cached, scan for types / attribute {AttributeName} (slow).", typeof(TAttribute).FullName); return GetTypesInternal( - typeof (object), typeof (TAttribute), + typeof(object), typeof(TAttribute), () => TypeFinder.FindClassesWithAttribute(specificAssemblies ?? AssembliesToScan), "scanning assemblies", cache); @@ -692,12 +732,14 @@ namespace Umbraco.Core.Composing var name = GetName(baseType, attributeType); lock (_locko) - using (_logger.DebugDuration( + { + using (_logger.DebugDuration( "Getting " + name, "Got " + name)) // cannot contain typesFound.Count as it's evaluated before the find - { - // get within a lock & timer - return GetTypesInternalLocked(baseType, attributeType, finder, action, cache); + { + // get within a lock & timer + return GetTypesInternalLocked(baseType, attributeType, finder, action, cache); + } } } @@ -719,13 +761,15 @@ namespace Umbraco.Core.Composing var listKey = new CompositeTypeTypeKey(baseType ?? tobject, attributeType ?? tobject); TypeList typeList = null; if (cache) + { _types.TryGetValue(listKey, out typeList); // else null + } // if caching and found, return if (typeList != null) { // need to put some logging here to try to figure out why this is happening: http://issues.umbraco.org/issue/U4-3505 - _logger.Debug("Getting {TypeName}: found a cached type list.", GetName(baseType, attributeType)); + _logger.Debug("Getting {TypeName}: found a cached type list.", GetName(baseType, attributeType)); return typeList.Types; } @@ -756,7 +800,7 @@ namespace Umbraco.Core.Composing // so in this instance there will never be a result. if (cacheResult.Exception is CachedTypeNotFoundInFileException || cacheResult.Success == false) { - _logger.Debug("Getting {TypeName}: failed to load from cache file, must scan assemblies.", GetName(baseType, attributeType)); + _logger.Debug("Getting {TypeName}: failed to load from cache file, must scan assemblies.", GetName(baseType, attributeType)); scan = true; } else @@ -775,7 +819,7 @@ namespace Umbraco.Core.Composing catch (Exception ex) { // in case of any exception, we have to exit, and revert to scanning - _logger.Error(ex, "Getting {TypeName}: failed to load cache file type {CacheType}, reverting to scanning assemblies.", GetName(baseType, attributeType), type); + _logger.Error(ex, "Getting {TypeName}: failed to load cache file type {CacheType}, reverting to scanning assemblies.", GetName(baseType, attributeType), type); scan = true; break; } @@ -783,7 +827,7 @@ namespace Umbraco.Core.Composing if (scan == false) { - _logger.Debug("Getting {TypeName}: loaded types from cache file.", GetName(baseType, attributeType)); + _logger.Debug("Getting {TypeName}: loaded types from cache file.", GetName(baseType, attributeType)); } } } @@ -791,10 +835,12 @@ namespace Umbraco.Core.Composing if (scan) { // either we had to scan, or we could not get the types from the cache file - scan now - _logger.Debug("Getting {TypeName}: " + action + ".", GetName(baseType, attributeType)); + _logger.Debug("Getting {TypeName}: " + action + ".", GetName(baseType, attributeType)); foreach (var t in finder()) + { typeList.Add(t); + } } // if we are to cache the results, do so @@ -806,14 +852,16 @@ namespace Umbraco.Core.Composing _types[listKey] = typeList; //if we are scanning then update the cache file if (scan) + { UpdateCache(); + } } - _logger.Debug("Got {TypeName}, caching ({CacheType}).", GetName(baseType, attributeType), added.ToString().ToLowerInvariant()); + _logger.Debug("Got {TypeName}, caching ({CacheType}).", GetName(baseType, attributeType), added.ToString().ToLowerInvariant()); } else { - _logger.Debug("Got {TypeName}.", GetName(baseType, attributeType)); + _logger.Debug("Got {TypeName}.", GetName(baseType, attributeType)); } return typeList.Types; diff --git a/src/Umbraco.Core/ConfigsExtensions.cs b/src/Umbraco.Core/ConfigsExtensions.cs index d1672c6c7f..10594fc970 100644 --- a/src/Umbraco.Core/ConfigsExtensions.cs +++ b/src/Umbraco.Core/ConfigsExtensions.cs @@ -1,10 +1,10 @@ using System.IO; using Umbraco.Core.Cache; -using Umbraco.Core.Composing; using Umbraco.Core.Configuration; using Umbraco.Core.Configuration.Grid; using Umbraco.Core.Configuration.HealthChecks; using Umbraco.Core.Configuration.UmbracoSettings; +using Umbraco.Core.Dashboards; using Umbraco.Core.IO; using Umbraco.Core.Logging; using Umbraco.Core.Manifest; @@ -48,6 +48,8 @@ namespace Umbraco.Core configDir, factory.GetInstance(), factory.GetInstance().Debug)); + + configs.Add(() => new ContentDashboardSettings()); } } } diff --git a/src/Umbraco.Core/Configuration/GlobalSettings.cs b/src/Umbraco.Core/Configuration/GlobalSettings.cs index 1d1ccaf7b4..c844abe75e 100644 --- a/src/Umbraco.Core/Configuration/GlobalSettings.cs +++ b/src/Umbraco.Core/Configuration/GlobalSettings.cs @@ -6,7 +6,9 @@ using System.Web; using System.Web.Configuration; using System.Web.Hosting; using System.Xml.Linq; +using Umbraco.Core.Composing; using Umbraco.Core.IO; +using Umbraco.Core.Logging; namespace Umbraco.Core.Configuration { @@ -23,6 +25,7 @@ namespace Umbraco.Core.Configuration // TODO these should not be static private static string _reservedPaths; private static string _reservedUrls; + private static int _sqlWriteLockTimeOut; //ensure the built on (non-changeable) reserved paths are there at all times internal const string StaticReservedPaths = "~/app_plugins/,~/install/,~/mini-profiler-resources/,"; //must end with a comma! @@ -392,27 +395,47 @@ namespace Umbraco.Core.Configuration } } - /// /// An int value representing the time in milliseconds to lock the database for a write operation /// /// - /// The default value is 1800 milliseconds + /// The default value is 5000 milliseconds /// /// The timeout in milliseconds. public int SqlWriteLockTimeOut { get { - try + if (_sqlWriteLockTimeOut != default) return _sqlWriteLockTimeOut; + + var timeOut = GetSqlWriteLockTimeoutFromConfigFile(Current.Logger); + + _sqlWriteLockTimeOut = timeOut; + return _sqlWriteLockTimeOut; + } + } + + internal static int GetSqlWriteLockTimeoutFromConfigFile(ILogger logger) + { + var timeOut = 5000; // 5 seconds + var appSettingSqlWriteLockTimeOut = ConfigurationManager.AppSettings[Constants.AppSettings.SqlWriteLockTimeOut]; + if (int.TryParse(appSettingSqlWriteLockTimeOut, out var configuredTimeOut)) + { + // Only apply this setting if it's not excessively high or low + const int minimumTimeOut = 100; + const int maximumTimeOut = 20000; + if (configuredTimeOut >= minimumTimeOut && configuredTimeOut <= maximumTimeOut) // between 0.1 and 20 seconds { - return int.Parse(ConfigurationManager.AppSettings[Constants.AppSettings.SqlWriteLockTimeOut]); + timeOut = configuredTimeOut; } - catch + else { - return 1800; + logger.Warn( + $"The `{Constants.AppSettings.SqlWriteLockTimeOut}` setting in web.config is not between the minimum of {minimumTimeOut} ms and maximum of {maximumTimeOut} ms, defaulting back to {timeOut}"); } } + + return timeOut; } } } diff --git a/src/Umbraco.Core/Configuration/Grid/GridEditorsConfig.cs b/src/Umbraco.Core/Configuration/Grid/GridEditorsConfig.cs index d434da8c70..82f9bd2afe 100644 --- a/src/Umbraco.Core/Configuration/Grid/GridEditorsConfig.cs +++ b/src/Umbraco.Core/Configuration/Grid/GridEditorsConfig.cs @@ -43,7 +43,7 @@ namespace Umbraco.Core.Configuration.Grid } catch (Exception ex) { - _logger.Error(ex, "Could not parse the contents of grid.editors.config.js into a JSON array '{Json}", sourceString); + _logger.Error(ex, "Could not parse the contents of grid.editors.config.js into a JSON array '{Json}", sourceString); } } diff --git a/src/Umbraco.Core/Configuration/UmbracoSettings/ContentElement.cs b/src/Umbraco.Core/Configuration/UmbracoSettings/ContentElement.cs index 3ebb632882..7c6ff4405f 100644 --- a/src/Umbraco.Core/Configuration/UmbracoSettings/ContentElement.cs +++ b/src/Umbraco.Core/Configuration/UmbracoSettings/ContentElement.cs @@ -29,11 +29,11 @@ namespace Umbraco.Core.Configuration.UmbracoSettings internal InnerTextConfigurationElement MacroErrors => GetOptionalTextElement("MacroErrors", MacroErrorBehaviour.Inline); [ConfigurationProperty("disallowedUploadFiles")] - internal CommaDelimitedConfigurationElement DisallowedUploadFiles => GetOptionalDelimitedElement("disallowedUploadFiles", new[] {"ashx", "aspx", "ascx", "config", "cshtml", "vbhtml", "asmx", "air", "axd"}); + internal CommaDelimitedConfigurationElement DisallowedUploadFiles => GetOptionalDelimitedElement("disallowedUploadFiles", new[] {"ashx", "aspx", "ascx", "config", "cshtml", "vbhtml", "asmx", "air", "axd", "xamlx"}); [ConfigurationProperty("allowedUploadFiles")] internal CommaDelimitedConfigurationElement AllowedUploadFiles => GetOptionalDelimitedElement("allowedUploadFiles", new string[0]); - + [ConfigurationProperty("showDeprecatedPropertyEditors")] internal InnerTextConfigurationElement ShowDeprecatedPropertyEditors => GetOptionalTextElement("showDeprecatedPropertyEditors", false); diff --git a/src/Umbraco.Core/Constants-AppSettings.cs b/src/Umbraco.Core/Constants-AppSettings.cs index 0182034011..1f096ab9f9 100644 --- a/src/Umbraco.Core/Constants-AppSettings.cs +++ b/src/Umbraco.Core/Constants-AppSettings.cs @@ -110,6 +110,11 @@ namespace Umbraco.Core /// public const string UseHttps = "Umbraco.Core.UseHttps"; + /// + /// A true/false value indicating whether the content dashboard should be visible for all user groups. + /// + public const string AllowContentDashboardAccessToAllUsers = "Umbraco.Core.AllowContentDashboardAccessToAllUsers"; + /// /// TODO: FILL ME IN /// @@ -144,9 +149,6 @@ namespace Umbraco.Core /// /// An int value representing the time in milliseconds to lock the database for a write operation /// - /// - /// The default value is 1800 milliseconds - /// public const string SqlWriteLockTimeOut = "Umbraco.Core.SqlWriteLockTimeOut"; } } diff --git a/src/Umbraco.Core/Constants-Icons.cs b/src/Umbraco.Core/Constants-Icons.cs index 05213ed1c4..d5cc37c9a5 100644 --- a/src/Umbraco.Core/Constants-Icons.cs +++ b/src/Umbraco.Core/Constants-Icons.cs @@ -24,6 +24,26 @@ /// public const string DataType = "icon-autofill"; + /// + /// System dictionary icon + /// + public const string Dictionary = "icon-book-alt"; + + /// + /// System generic folder icon + /// + public const string Folder = "icon-folder"; + + /// + /// System language icon + /// + public const string Language = "icon-globe"; + + /// + /// System logviewer icon + /// + public const string LogViewer = "icon-box-alt"; + /// /// System list view icon /// @@ -69,6 +89,11 @@ /// public const string MemberType = "icon-users"; + /// + /// System packages icon + /// + public const string Packages = "icon-box"; + /// /// System property editor icon /// diff --git a/src/Umbraco.Core/Constants-PropertyEditors.cs b/src/Umbraco.Core/Constants-PropertyEditors.cs index 90f5fbd0d0..87739469d1 100644 --- a/src/Umbraco.Core/Constants-PropertyEditors.cs +++ b/src/Umbraco.Core/Constants-PropertyEditors.cs @@ -51,6 +51,11 @@ namespace Umbraco.Core /// public const string ColorPicker = "Umbraco.ColorPicker"; + /// + /// EyeDropper Color Picker. + /// + public const string ColorPickerEyeDropper = "Umbraco.ColorPicker.EyeDropper"; + /// /// Content Picker. /// diff --git a/src/Umbraco.Core/Constants-Security.cs b/src/Umbraco.Core/Constants-Security.cs index f900288ef5..2b6244debb 100644 --- a/src/Umbraco.Core/Constants-Security.cs +++ b/src/Umbraco.Core/Constants-Security.cs @@ -25,7 +25,7 @@ namespace Umbraco.Core /// /// The name of the 'unknown' user. /// - public const string UnknownUserName = "SYTEM"; + public const string UnknownUserName = "SYSTEM"; public const string AdminGroupAlias = "admin"; public const string EditorGroupAlias = "editor"; diff --git a/src/Umbraco.Core/Constants-SvgSanitizer.cs b/src/Umbraco.Core/Constants-SvgSanitizer.cs deleted file mode 100644 index c92b9f56c7..0000000000 --- a/src/Umbraco.Core/Constants-SvgSanitizer.cs +++ /dev/null @@ -1,23 +0,0 @@ -using System.Collections.Generic; - -namespace Umbraco.Core -{ - public static partial class Constants - { - /// - /// Defines the alias identifiers for Umbraco's core application sections. - /// - public static class SvgSanitizer - { - /// - /// Allowlist for SVG attributes. - /// - public static readonly IList Attributes = new [] { "accent-height", "accumulate", "additive", "alignment-baseline", "allowReorder", "alphabetic", "amplitude", "arabic-form", "ascent", "attributeName", "attributeType", "autoReverse", "azimuth", "baseFrequency", "baseline-shift", "baseProfile", "bbox", "begin", "bias", "by", "calcMode", "cap-height", "class", "clip", "clipPathUnits", "clip-path", "clip-rule", "color", "color-interpolation", "color-interpolation-filters", "color-profile", "color-rendering", "contentScriptType", "contentStyleType", "cursor", "cx", "cy", "d", "decelerate", "descent", "diffuseConstant", "direction", "display", "divisor", "dominant-baseline", "dur", "dx", "dy", "edgeMode", "elevation", "enable-background", "end", "exponent", "externalResourcesRequired", "Section", "fill", "fill-opacity", "fill-rule", "filter", "filterRes", "filterUnits", "flood-color", "flood-opacity", "font-family", "font-size", "font-size-adjust", "font-stretch", "font-style", "font-variant", "font-weight", "format", "from", "fr", "fx", "fy", "g1", "g2", "glyph-name", "glyph-orientation-horizontal", "glyph-orientation-vertical", "glyphRef", "gradientTransform", "gradientUnits", "hanging", "height", "href", "hreflang", "horiz-adv-x", "horiz-origin-x", "ISection", "id", "ideographic", "image-rendering", "in", "in2", "intercept", "k", "k1", "k2", "k3", "k4", "kernelMatrix", "kernelUnitLength", "kerning", "keyPoints", "keySplines", "keyTimes", "lang", "lengthAdjust", "letter-spacing", "lighting-color", "limitingConeAngle", "local", "MSection", "marker-end", "marker-mid", "marker-start", "markerHeight", "markerUnits", "markerWidth", "mask", "maskContentUnits", "maskUnits", "mathematical", "max", "media", "method", "min", "mode", "NSection", "name", "numOctaves", "offset", "opacity", "operator", "order", "orient", "orientation", "origin", "overflow", "overline-position", "overline-thickness", "panose-1", "paint-order", "path", "pathLength", "patternContentUnits", "patternTransform", "patternUnits", "ping", "pointer-events", "points", "pointsAtX", "pointsAtY", "pointsAtZ", "preserveAlpha", "preserveAspectRatio", "primitiveUnits", "r", "radius", "referrerPolicy", "refX", "refY", "rel", "rendering-intent", "repeatCount", "repeatDur", "requiredExtensions", "requiredFeatures", "restart", "result", "rotate", "rx", "ry", "scale", "seed", "shape-rendering", "slope", "spacing", "specularConstant", "specularExponent", "speed", "spreadMethod", "startOffset", "stdDeviation", "stemh", "stemv", "stitchTiles", "stop-color", "stop-opacity", "strikethrough-position", "strikethrough-thickness", "string", "stroke", "stroke-dasharray", "stroke-dashoffset", "stroke-linecap", "stroke-linejoin", "stroke-miterlimit", "stroke-opacity", "stroke-width", "style", "surfaceScale", "systemLanguage", "tabindex", "tableValues", "target", "targetX", "targetY", "text-anchor", "text-decoration", "text-rendering", "textLength", "to", "transform", "type", "u1", "u2", "underline-position", "underline-thickness", "unicode", "unicode-bidi", "unicode-range", "units-per-em", "v-alphabetic", "v-hanging", "v-ideographic", "v-mathematical", "values", "vector-effect", "version", "vert-adv-y", "vert-origin-x", "vert-origin-y", "viewBox", "viewTarget", "visibility", "width", "widths", "word-spacing", "writing-mode", "x", "x-height", "x1", "x2", "xChannelSelector", "xlink:actuate", "xlink:arcrole", "xlink:href", "xlink:role", "xlink:show", "xlink:title", "xlink:type", "xml:base", "xml:lang", "xml:space", "y", "y1", "y2", "yChannelSelector", "z", "zoomAndPan" }; - - /// - /// Allowlist for SVG tabs. - /// - public static readonly IList Tags = new [] { "a", "altGlyph", "altGlyphDef", "altGlyphItem", "animate", "animateColor", "animateMotion", "animateTransform", "circle", "clipPath", "color-profile", "cursor", "defs", "desc", "discard", "ellipse", "feBlend", "feColorMatrix", "feComponentTransfer", "feComposite", "feConvolveMatrix", "feDiffuseLighting", "feDisplacementMap", "feDistantLight", "feDropShadow", "feFlood", "feFuncA", "feFuncB", "feFuncG", "feFuncR", "feGaussianBlur", "feImage", "feMerge", "feMergeNode", "feMorphology", "feOffset", "fePointLight", "feSpecularLighting", "feSpotLight", "feTile", "feTurbulence", "filter", "font", "font-face", "font-face-format", "font-face-name", "font-face-src", "font-face-uri", "foreignObject", "g", "glyph", "glyphRef", "hatch", "hatchpath", "hkern", "image", "line", "linearGradient", "marker", "mask", "mesh", "meshgradient", "meshpatch", "meshrow", "metadata", "missing-glyph", "mpath", "path", "pattern", "polygon", "polyline", "radialGradient", "rect", "set", "solidcolor", "stop", "svg", "switch", "symbol", "text", "textPath", "title", "tref", "tspan", "unknown", "use", "view", "vkern" }; - } - } -} diff --git a/src/Umbraco.Core/Dashboards/ContentDashboardSettings.cs b/src/Umbraco.Core/Dashboards/ContentDashboardSettings.cs new file mode 100644 index 0000000000..f8fb5c7b06 --- /dev/null +++ b/src/Umbraco.Core/Dashboards/ContentDashboardSettings.cs @@ -0,0 +1,24 @@ +using System.Configuration; + +namespace Umbraco.Core.Dashboards +{ + public class ContentDashboardSettings: IContentDashboardSettings + { + + /// + /// Gets a value indicating whether the content dashboard should be available to all users. + /// + /// + /// true if the dashboard is visible for all user groups; otherwise, false + /// and the default access rules for that dashboard will be in use. + /// + public bool AllowContentDashboardAccessToAllUsers + { + get + { + bool.TryParse(ConfigurationManager.AppSettings[Constants.AppSettings.AllowContentDashboardAccessToAllUsers], out var value); + return value; + } + } + } +} diff --git a/src/Umbraco.Core/Dashboards/IContentDashboardSettings.cs b/src/Umbraco.Core/Dashboards/IContentDashboardSettings.cs new file mode 100644 index 0000000000..862a28b90e --- /dev/null +++ b/src/Umbraco.Core/Dashboards/IContentDashboardSettings.cs @@ -0,0 +1,14 @@ +namespace Umbraco.Core.Dashboards +{ + public interface IContentDashboardSettings + { + /// + /// Gets a value indicating whether the content dashboard should be available to all users. + /// + /// + /// true if the dashboard is visible for all user groups; otherwise, false + /// and the default access rules for that dashboard will be in use. + /// + bool AllowContentDashboardAccessToAllUsers { get; } + } +} diff --git a/src/Umbraco.Core/IO/FileSystemWrapper.cs b/src/Umbraco.Core/IO/FileSystemWrapper.cs index 14d028c16d..3091a645d5 100644 --- a/src/Umbraco.Core/IO/FileSystemWrapper.cs +++ b/src/Umbraco.Core/IO/FileSystemWrapper.cs @@ -23,94 +23,94 @@ namespace Umbraco.Core.IO internal IFileSystem InnerFileSystem { get; set; } - public IEnumerable GetDirectories(string path) + public virtual IEnumerable GetDirectories(string path) { return InnerFileSystem.GetDirectories(path); } - public void DeleteDirectory(string path) + public virtual void DeleteDirectory(string path) { InnerFileSystem.DeleteDirectory(path); } - public void DeleteDirectory(string path, bool recursive) + public virtual void DeleteDirectory(string path, bool recursive) { InnerFileSystem.DeleteDirectory(path, recursive); } - public bool DirectoryExists(string path) + public virtual bool DirectoryExists(string path) { return InnerFileSystem.DirectoryExists(path); } - public void AddFile(string path, Stream stream) + public virtual void AddFile(string path, Stream stream) { InnerFileSystem.AddFile(path, stream); } - public void AddFile(string path, Stream stream, bool overrideExisting) + public virtual void AddFile(string path, Stream stream, bool overrideExisting) { InnerFileSystem.AddFile(path, stream, overrideExisting); } - public IEnumerable GetFiles(string path) + public virtual IEnumerable GetFiles(string path) { return InnerFileSystem.GetFiles(path); } - public IEnumerable GetFiles(string path, string filter) + public virtual IEnumerable GetFiles(string path, string filter) { return InnerFileSystem.GetFiles(path, filter); } - public Stream OpenFile(string path) + public virtual Stream OpenFile(string path) { return InnerFileSystem.OpenFile(path); } - public void DeleteFile(string path) + public virtual void DeleteFile(string path) { InnerFileSystem.DeleteFile(path); } - public bool FileExists(string path) + public virtual bool FileExists(string path) { return InnerFileSystem.FileExists(path); } - public string GetRelativePath(string fullPathOrUrl) + public virtual string GetRelativePath(string fullPathOrUrl) { return InnerFileSystem.GetRelativePath(fullPathOrUrl); } - public string GetFullPath(string path) + public virtual string GetFullPath(string path) { return InnerFileSystem.GetFullPath(path); } - public string GetUrl(string path) + public virtual string GetUrl(string path) { return InnerFileSystem.GetUrl(path); } - public DateTimeOffset GetLastModified(string path) + public virtual DateTimeOffset GetLastModified(string path) { return InnerFileSystem.GetLastModified(path); } - public DateTimeOffset GetCreated(string path) + public virtual DateTimeOffset GetCreated(string path) { return InnerFileSystem.GetCreated(path); } - public long GetSize(string path) + public virtual long GetSize(string path) { return InnerFileSystem.GetSize(path); } - public bool CanAddPhysical => InnerFileSystem.CanAddPhysical; + public virtual bool CanAddPhysical => InnerFileSystem.CanAddPhysical; - public void AddFile(string path, string physicalPath, bool overrideIfExists = true, bool copy = false) + public virtual void AddFile(string path, string physicalPath, bool overrideIfExists = true, bool copy = false) { InnerFileSystem.AddFile(path, physicalPath, overrideIfExists, copy); } diff --git a/src/Umbraco.Core/IO/FileSystems.cs b/src/Umbraco.Core/IO/FileSystems.cs index 8906752dd1..3b05adb20f 100644 --- a/src/Umbraco.Core/IO/FileSystems.cs +++ b/src/Umbraco.Core/IO/FileSystems.cs @@ -225,7 +225,7 @@ namespace Umbraco.Core.IO _shadowCurrentId = id; - _logger.Debug("Shadow '{ShadowId}'", _shadowCurrentId); + _logger.Debug("Shadow '{ShadowId}'", _shadowCurrentId); foreach (var wrapper in _shadowWrappers) wrapper.Shadow(_shadowCurrentId); @@ -242,7 +242,7 @@ namespace Umbraco.Core.IO if (id != _shadowCurrentId) throw new InvalidOperationException("Not the current shadow."); - _logger.Debug("UnShadow '{ShadowId}' {Status}", id, completed ? "complete" : "abort"); + _logger.Debug("UnShadow '{ShadowId}' {Status}", id, completed ? "complete" : "abort"); var exceptions = new List(); foreach (var wrapper in _shadowWrappers) diff --git a/src/Umbraco.Core/IO/IOHelper.cs b/src/Umbraco.Core/IO/IOHelper.cs index 8661f73fb1..69ce50de9c 100644 --- a/src/Umbraco.Core/IO/IOHelper.cs +++ b/src/Umbraco.Core/IO/IOHelper.cs @@ -81,6 +81,7 @@ namespace Umbraco.Core.IO public static string MapPath(string path, bool useHttpContext) { if (path == null) throw new ArgumentNullException("path"); + useHttpContext = useHttpContext && IsHosted; // Check if the path is already mapped @@ -89,10 +90,8 @@ namespace Umbraco.Core.IO { return path; } - // Check that we even have an HttpContext! otherwise things will fail anyways - // http://umbraco.codeplex.com/workitem/30946 - if (useHttpContext && HttpContext.Current != null) + if (useHttpContext) { //string retval; if (String.IsNullOrEmpty(path) == false && (path.StartsWith("~") || path.StartsWith(SystemDirectories.Root))) diff --git a/src/Umbraco.Core/IO/MediaFileSystem.cs b/src/Umbraco.Core/IO/MediaFileSystem.cs index 05c02171ba..6743275be0 100644 --- a/src/Umbraco.Core/IO/MediaFileSystem.cs +++ b/src/Umbraco.Core/IO/MediaFileSystem.cs @@ -51,7 +51,7 @@ namespace Umbraco.Core.IO } catch (Exception e) { - _logger.Error(e, "Failed to delete media file '{File}'.", file); + _logger.Error(e, "Failed to delete media file '{File}'.", file); } }); } diff --git a/src/Umbraco.Core/IO/PhysicalFileSystem.cs b/src/Umbraco.Core/IO/PhysicalFileSystem.cs index a833ba43af..5ebe6817e5 100644 --- a/src/Umbraco.Core/IO/PhysicalFileSystem.cs +++ b/src/Umbraco.Core/IO/PhysicalFileSystem.cs @@ -74,11 +74,11 @@ namespace Umbraco.Core.IO } catch (UnauthorizedAccessException ex) { - Current.Logger.Error(ex, "Not authorized to get directories for '{Path}'", fullPath); + Current.Logger.Error(ex, "Not authorized to get directories for '{Path}'", fullPath); } catch (DirectoryNotFoundException ex) { - Current.Logger.Error(ex, "Directory not found for '{Path}'", fullPath); + Current.Logger.Error(ex, "Directory not found for '{Path}'", fullPath); } return Enumerable.Empty(); @@ -110,7 +110,7 @@ namespace Umbraco.Core.IO } catch (DirectoryNotFoundException ex) { - Current.Logger.Error(ex, "Directory not found for '{Path}'", fullPath); + Current.Logger.Error(ex, "Directory not found for '{Path}'", fullPath); } } @@ -190,11 +190,11 @@ namespace Umbraco.Core.IO } catch (UnauthorizedAccessException ex) { - Current.Logger.Error(ex, "Not authorized to get directories for '{Path}'", fullPath); + Current.Logger.Error(ex, "Not authorized to get directories for '{Path}'", fullPath); } catch (DirectoryNotFoundException ex) { - Current.Logger.Error(ex, "Directory not found for '{FullPath}'", fullPath); + Current.Logger.Error(ex, "Directory not found for '{FullPath}'", fullPath); } return Enumerable.Empty(); @@ -227,7 +227,7 @@ namespace Umbraco.Core.IO } catch (FileNotFoundException ex) { - Current.Logger.Error(ex.InnerException, "DeleteFile failed with FileNotFoundException for '{Path}'", fullPath); + Current.Logger.Error(ex.InnerException, "DeleteFile failed with FileNotFoundException for '{Path}'", fullPath); } } diff --git a/src/Umbraco.Core/IO/SystemDirectories.cs b/src/Umbraco.Core/IO/SystemDirectories.cs index 485fd7f965..9d09bf2f0d 100644 --- a/src/Umbraco.Core/IO/SystemDirectories.cs +++ b/src/Umbraco.Core/IO/SystemDirectories.cs @@ -25,6 +25,8 @@ namespace Umbraco.Core.IO public static string AppPlugins => "~/App_Plugins"; + public static string AppPluginIcons => "/Backoffice/Icons"; + public static string MvcViews => "~/Views"; public static string PartialViews => MvcViews + "/Partials/"; diff --git a/src/Umbraco.Core/Logging/DebugDiagnosticsLogger.cs b/src/Umbraco.Core/Logging/DebugDiagnosticsLogger.cs index d1bde55306..8f26f7e75c 100644 --- a/src/Umbraco.Core/Logging/DebugDiagnosticsLogger.cs +++ b/src/Umbraco.Core/Logging/DebugDiagnosticsLogger.cs @@ -129,5 +129,65 @@ namespace Umbraco.Core.Logging { System.Diagnostics.Debug.WriteLine(MessageTemplates.Render(messageTemplate, propertyValues), reporting.FullName); } + /// + public void Fatal(Type reporting, Exception exception, string messageTemplate, T0 propertyValue0, + T1 propertyValue1, T2 propertyValue2) + => Fatal(reporting, exception, messageTemplate, new object[] { propertyValue0, propertyValue1, propertyValue2 }); + /// + public void Fatal(Type reporting, Exception exception, string messageTemplate, T0 propertyValue0, T1 propertyValue1) + => Fatal(reporting, exception, messageTemplate, new object[] { propertyValue0, propertyValue1 }); + /// + public void Fatal(Type reporting, Exception exception, string messageTemplate, T0 propertyValue0) + => Fatal(reporting, exception, messageTemplate, new object[] { propertyValue0 }); + /// + public void Error(Type reporting, Exception exception, string messageTemplate, T0 propertyValue0, + T1 propertyValue1, T2 propertyValue2) + => Error(reporting, exception, messageTemplate, new object[] { propertyValue0, propertyValue1, propertyValue2 }); + /// + public void Error(Type reporting, Exception exception, string messageTemplate, T0 propertyValue0, T1 propertyValue1) + => Error(reporting, exception, messageTemplate, new object[] { propertyValue0, propertyValue1 }); + /// + public void Error(Type reporting, Exception exception, string messageTemplate, T0 propertyValue0) + => Error(reporting, exception, messageTemplate, new object[] { propertyValue0 }); + /// + public void Warn(Type reporting, Exception exception, string messageTemplate, T0 propertyValue0, + T1 propertyValue1, T2 propertyValue2) + => Warn(reporting, exception, messageTemplate, new object[] { propertyValue0, propertyValue1,propertyValue2 }); + /// + public void Warn(Type reporting, Exception exception, string messageTemplate, T0 propertyValue0, T1 propertyValue1) + => Warn(reporting, exception, messageTemplate, new object[] { propertyValue0, propertyValue1 }); + + public void Warn(Type reporting, Exception exception, string messageTemplate, T0 propertyValue0) + => Warn(reporting, exception, messageTemplate, new object[] { propertyValue0 }); + + public void Warn(Type reporting, string message, T0 propertyValue0) + => Warn(reporting, message, new object[] { propertyValue0 }); + + public void Info(Type reporting, string messageTemplate, T0 propertyValue0, T1 propertyValue1, T2 propertyValue2) + => Info(reporting, messageTemplate, new object[] { propertyValue0, propertyValue1, propertyValue2 }); + + public void Info(Type reporting, string messageTemplate, T0 propertyValue0, T1 propertyValue1) + => Info(reporting, messageTemplate, new object[] { propertyValue0, propertyValue1 }); + + public void Info(Type reporting, string messageTemplate, T0 propertyValue0) + => Info(reporting, messageTemplate, new object[] { propertyValue0 }); + + public void Debug(Type reporting, string messageTemplate, T0 propertyValue0, T1 propertyValue1, T2 propertyValue2) + => Debug(reporting, messageTemplate, new object[] { propertyValue0, propertyValue1, propertyValue2 }); + + public void Debug(Type reporting, string messageTemplate, T0 propertyValue0, T1 propertyValue1) + => Debug(reporting, messageTemplate, new object[] { propertyValue0, propertyValue1 }); + + public void Debug(Type reporting, string messageTemplate, T0 propertyValue0) + => Debug(reporting, messageTemplate, new object[] { propertyValue0 }); + + public void Verbose(Type reporting, string messageTemplate, T0 propertyValue0, T1 propertyValue1, T2 propertyValue2) + => Verbose(reporting, messageTemplate, new object[] { propertyValue0, propertyValue1,propertyValue2 }); + + public void Verbose(Type reporting, string messageTemplate, T0 propertyValue0, T1 propertyValue1) + => Verbose(reporting, messageTemplate, new object[] { propertyValue0, propertyValue1 }); + + public void Verbose(Type reporting, string messageTemplate, T0 propertyValue0) + => Verbose(reporting, messageTemplate, new object[] { propertyValue0 }); } } diff --git a/src/Umbraco.Core/Logging/DisposableTimer.cs b/src/Umbraco.Core/Logging/DisposableTimer.cs index ed98e5cfab..63ae3c2792 100644 --- a/src/Umbraco.Core/Logging/DisposableTimer.cs +++ b/src/Umbraco.Core/Logging/DisposableTimer.cs @@ -37,10 +37,10 @@ namespace Umbraco.Core.Logging switch (_level) { case LogLevel.Debug: - logger.Debug(loggerType, "{StartMessage} [Timing {TimingId}]", startMessage, _timingId); + logger.Debug(loggerType, "{StartMessage} [Timing {TimingId}]", startMessage, _timingId); break; case LogLevel.Information: - logger.Info(loggerType, "{StartMessage} [Timing {TimingId}]", startMessage, _timingId); + logger.Info(loggerType, "{StartMessage} [Timing {TimingId}]", startMessage, _timingId); break; default: throw new ArgumentOutOfRangeException(nameof(level)); @@ -84,15 +84,15 @@ namespace Umbraco.Core.Logging { if (_failed) { - _logger.Error(_loggerType, _failException, "{FailMessage} ({Duration}ms) [Timing {TimingId}]", _failMessage, Stopwatch.ElapsedMilliseconds, _timingId); + _logger.Error(_loggerType, _failException, "{FailMessage} ({Duration}ms) [Timing {TimingId}]", _failMessage, Stopwatch.ElapsedMilliseconds, _timingId); } else switch (_level) { case LogLevel.Debug: - _logger.Debug(_loggerType, "{EndMessage} ({Duration}ms) [Timing {TimingId}]", _endMessage, Stopwatch.ElapsedMilliseconds, _timingId); + _logger.Debug(_loggerType, "{EndMessage} ({Duration}ms) [Timing {TimingId}]", _endMessage, Stopwatch.ElapsedMilliseconds, _timingId); break; case LogLevel.Information: - _logger.Info(_loggerType, "{EndMessage} ({Duration}ms) [Timing {TimingId}]", _endMessage, Stopwatch.ElapsedMilliseconds, _timingId); + _logger.Info(_loggerType, "{EndMessage} ({Duration}ms) [Timing {TimingId}]", _endMessage, Stopwatch.ElapsedMilliseconds, _timingId); break; // filtered in the ctor //default: diff --git a/src/Umbraco.Core/Logging/ILogger.cs b/src/Umbraco.Core/Logging/ILogger.cs index 4f49d0b3b4..d8272b6aa0 100644 --- a/src/Umbraco.Core/Logging/ILogger.cs +++ b/src/Umbraco.Core/Logging/ILogger.cs @@ -52,7 +52,7 @@ namespace Umbraco.Core.Logging /// A message template. /// Property values. void Fatal(Type reporting, Exception exception, string messageTemplate, params object[] propertyValues); - + /// /// Logs a fatal message. /// @@ -92,7 +92,7 @@ namespace Umbraco.Core.Logging /// A message template. /// Property values. void Error(Type reporting, Exception exception, string messageTemplate, params object[] propertyValues); - + /// /// Logs an error message. /// @@ -132,6 +132,7 @@ namespace Umbraco.Core.Logging /// A message template. /// Property values. void Warn(Type reporting, Exception exception, string messageTemplate, params object[] propertyValues); + /// /// Logs an information message. @@ -147,6 +148,7 @@ namespace Umbraco.Core.Logging /// A message template. /// Property values. void Info(Type reporting, string messageTemplate, params object[] propertyValues); + /// /// Logs a debugging message. @@ -177,5 +179,6 @@ namespace Umbraco.Core.Logging /// A message template. /// Property values. void Verbose(Type reporting, string messageTemplate, params object[] propertyValues); + } } diff --git a/src/Umbraco.Core/Logging/ILogger2.cs b/src/Umbraco.Core/Logging/ILogger2.cs new file mode 100644 index 0000000000..0db8021f58 --- /dev/null +++ b/src/Umbraco.Core/Logging/ILogger2.cs @@ -0,0 +1,240 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Umbraco.Core.Logging +{ + public interface ILogger2 : ILogger + { + /// + /// Logs a fatal message with an exception. + /// + /// The reporting type. + /// An exception. + /// A message template. + /// Property value 0 + /// Property value 1 + /// Property value 2 + void Fatal(Type reporting, Exception exception, string messageTemplate, T0 propertyValue0, T1 propertyValue1, T2 propertyValue2); + /// + /// Logs a fatal message with an exception. + /// + /// The reporting type. + /// An exception. + /// A message template. + /// Property value 0 + /// Property value 1 + void Fatal(Type reporting, Exception exception, string messageTemplate, T0 propertyValue0, T1 propertyValue1); + /// + /// Logs a fatal message with an exception. + /// + /// The reporting type. + /// An exception. + /// A message template. + /// Property value 0 + void Fatal(Type reporting, Exception exception, string messageTemplate, T0 propertyValue0); + + /// + /// Logs an error message with an exception. + /// + /// The reporting type. + /// An exception. + /// A message template. + /// Property value 0 + /// Property value 1 + /// Property value 2 + void Error(Type reporting, Exception exception, string messageTemplate, T0 propertyValue0, T1 propertyValue1, T2 propertyValue2); + /// + /// Logs an error message with an exception. + /// + /// The reporting type. + /// An exception. + /// A message template. + /// Property value 0 + /// Property value 1 + void Error(Type reporting, Exception exception, string messageTemplate, T0 propertyValue0, T1 propertyValue1); + /// + /// Logs an error message with an exception. + /// + /// The reporting type. + /// An exception. + /// A message template. + /// Property value 0 + void Error(Type reporting, Exception exception, string messageTemplate, T0 propertyValue0); + + /// + /// Logs a warning message with an exception. + /// + /// The reporting type. + /// An exception. + /// A message template. + /// Property value 0 + /// Property value 1 + /// Property value 2 + void Warn(Type reporting, Exception exception, string messageTemplate, T0 propertyValue0, T1 propertyValue1, T2 propertyValue2); + /// + /// Logs a warning message with an exception. + /// + /// The reporting type. + /// An exception. + /// A message template. + /// Property value 0 + /// Property value 1 + void Warn(Type reporting, Exception exception, string messageTemplate, T0 propertyValue0, T1 propertyValue1); + /// + /// Logs a warning message with an exception. + /// + /// The reporting type. + /// An exception. + /// A message template. + /// Property value 0 + void Warn(Type reporting, Exception exception, string messageTemplate, T0 propertyValue0); + /// + /// Logs a warning message with an exception. + /// + /// The reporting type. + /// A message template. + /// Property value 0 + void Warn(Type reporting, string message, T0 propertyValue0); + + /// + /// Logs a info message. + /// + /// The reporting type. + /// A message template. + /// Property value 0 + /// Property value 1 + /// Property value 2 + void Info(Type reporting, string messageTemplate, T0 propertyValue0, T1 propertyValue1, T2 propertyValue2); + /// + /// Logs a info message. + /// + /// The reporting type. + /// A message template. + /// Property value 0 + /// Property value 1 + void Info(Type reporting, string messageTemplate, T0 propertyValue0, T1 propertyValue1); + /// + /// Logs a info message. + /// + /// The reporting type. + /// A message template. + /// Property value 0 + void Info(Type reporting, string messageTemplate, T0 propertyValue0); + + /// + /// Logs a debug message. + /// + /// The reporting type. + /// A message template. + /// Property value 0 + /// Property value 1 + /// Property value 2 + void Debug(Type reporting, string messageTemplate, T0 propertyValue0, T1 propertyValue1, T2 propertyValue2); + /// + /// Logs a debug message. + /// + /// The reporting type. + /// A message template. + /// Property value 0 + /// Property value 1 + void Debug(Type reporting, string messageTemplate, T0 propertyValue0, T1 propertyValue1); + /// + /// Logs a debug message. + /// + /// The reporting type. + /// A message template. + /// Property value 0 + void Debug(Type reporting, string messageTemplate, T0 propertyValue0); + + /// + /// Logs a verbose message. + /// + /// The reporting type. + /// A message template. + /// Property value 0 + /// Property value 1 + /// Property value 2 + void Verbose(Type reporting, string messageTemplate, T0 propertyValue0, T1 propertyValue1, T2 propertyValue2); + /// + /// Logs a verbose message. + /// + /// The reporting type. + /// A message template. + /// Property value 0 + /// Property value 1 + void Verbose(Type reporting, string messageTemplate, T0 propertyValue0, T1 propertyValue1); + /// + /// Logs a verbose message. + /// + /// The reporting type. + /// A message template. + /// Property value 0 + void Verbose(Type reporting, string messageTemplate, T0 propertyValue0); + + + /// + /// Logs a error message. + /// + /// The reporting type. + /// A message template. + /// Property value 0 + void Error(Type reporting, string messageTemplate, T0 propertyValue0); + + /// + /// Logs a error message. + /// + /// The reporting type. + /// A message template. + /// Property value 0 + /// Property value 1 + void Error(Type reporting, string messageTemplate, T0 propertyValue0, T1 propertyValue1); + + /// + /// Logs a error message. + /// + /// The reporting type. + /// A message template. + /// Property value 0 + /// Property value 1 + /// Property value 2 + void Error(Type reporting, string messageTemplate, T0 propertyValue0, T1 propertyValue1, T2 propertyValue2); + + /// + /// Logs a warning message. + /// + /// The reporting type. + /// A message template. + /// Property value 0 + /// Property value 1 + /// Property value 2 + void Warn(string messageTemplate, T0 propertyValue0, T1 propertyValue1); + /// + /// Logs a warning message. + /// + /// The reporting type. + /// A message template. + /// Property value 0 + /// Property value 1 + void Warn(string messageTemplate, T0 propertyValue0, T1 propertyValue1, T2 propertyValue2); + /// + /// Logs a warning message with an exception. + /// + /// The reporting type. + /// A message template. + /// Property value 0 + /// Property value 1 + /// Property value 2 + void Warn(Type reporting, string messageTemplate, T0 propertyValue0, T1 propertyValue1, T2 propertyValue2); + /// + /// Logs a warning message with an exception. + /// + /// The reporting type. + /// A message template. + /// Property value 0 + /// Property value 1 + void Warn(Type reporting, string messageTemplate, T0 propertyValue0, T1 propertyValue1); + } +} diff --git a/src/Umbraco.Core/Logging/LogProfiler.cs b/src/Umbraco.Core/Logging/LogProfiler.cs index 74dae545b4..c0f8d1b525 100644 --- a/src/Umbraco.Core/Logging/LogProfiler.cs +++ b/src/Umbraco.Core/Logging/LogProfiler.cs @@ -24,8 +24,8 @@ namespace Umbraco.Core.Logging /// public IDisposable Step(string name) { - _logger.Debug("Begin: {ProfileName}", name); - return new LightDisposableTimer(duration => _logger.Info("End {ProfileName} ({ProfileDuration}ms)", name, duration)); + _logger.Debug("Begin: {ProfileName}", name); + return new LightDisposableTimer(duration => _logger.Info("End {ProfileName} ({ProfileDuration}ms)", name, duration)); } /// diff --git a/src/Umbraco.Core/Logging/Logger2Extensions.cs b/src/Umbraco.Core/Logging/Logger2Extensions.cs new file mode 100644 index 0000000000..c6c1352055 --- /dev/null +++ b/src/Umbraco.Core/Logging/Logger2Extensions.cs @@ -0,0 +1,442 @@ +using System; + +namespace Umbraco.Core.Logging +{ + public static class Logger2Extensions + { + public static void Debug(this ILogger logger, Type reporting, string messageTemplate, T0 propertyValue0, T1 propertyValue1, T2 propertyValue2) + { + if (logger is ILogger2 logger2) + { + logger2.Debug(reporting, messageTemplate, propertyValue0, propertyValue1, propertyValue2); + } + else + { + logger.Debug(reporting, messageTemplate, propertyValue0, propertyValue1, propertyValue2); + } + } + + public static void Debug(this ILogger logger, Type reporting, string messageTemplate, T0 propertyValue0, T1 propertyValue1) + { + if (logger is ILogger2 logger2) + { + logger2.Debug(reporting, messageTemplate, propertyValue0, propertyValue1); + } + else + { + logger.Debug(reporting, messageTemplate, propertyValue0, propertyValue1); + } + } + + public static void Debug(this ILogger logger, Type reporting, string messageTemplate, T0 propertyValue0) + { + if (logger is ILogger2 logger2) + { + logger2.Debug(reporting, messageTemplate, propertyValue0); + } + else + { + logger.Debug(reporting, messageTemplate, propertyValue0); + } + } + + public static void Error(this ILogger logger, Type reporting, Exception exception, string messageTemplate, T0 propertyValue0, T1 propertyValue1, T2 propertyValue2) + { + if (logger is ILogger2 logger2) + { + logger2.Error(reporting, exception, messageTemplate, propertyValue0, propertyValue1, propertyValue2); + } + else + { + logger.Error(reporting, exception, messageTemplate, propertyValue0, propertyValue1, propertyValue2); + } + } + + public static void Error(this ILogger logger, Type reporting, Exception exception, string messageTemplate, T0 propertyValue0, T1 propertyValue1) + { + if (logger is ILogger2 logger2) + { + logger2.Error(reporting, exception, messageTemplate, propertyValue0, propertyValue1); + } + else + { + logger.Error(reporting, exception, messageTemplate, propertyValue0, propertyValue1); + } + } + + public static void Error(this ILogger logger, Type reporting, Exception exception, string messageTemplate, T0 propertyValue0) + { + if (logger is ILogger2 logger2) + { + logger2.Error(reporting, exception, messageTemplate, propertyValue0); + } + else + { + logger.Error(reporting, exception, messageTemplate, propertyValue0); + } + } + + public static void Fatal(this ILogger logger, Type reporting, Exception exception, string messageTemplate, T0 propertyValue0, T1 propertyValue1, T2 propertyValue2) + { + if (logger is ILogger2 logger2) + { + logger2.Fatal(reporting, exception, messageTemplate, propertyValue0, propertyValue1, propertyValue2); + } + else + { + logger.Fatal(reporting, exception, messageTemplate, propertyValue0, propertyValue1, propertyValue2); + } + } + + public static void Fatal(this ILogger logger, Type reporting, Exception exception, string messageTemplate, T0 propertyValue0, T1 propertyValue1) + { + if (logger is ILogger2 logger2) + { + logger2.Fatal(reporting, exception, messageTemplate, propertyValue0, propertyValue1); + } + else + { + logger.Fatal(reporting, exception, messageTemplate, propertyValue0, propertyValue1); + } + } + + public static void Fatal(this ILogger logger, Type reporting, Exception exception, string messageTemplate, T0 propertyValue0) + { + if (logger is ILogger2 logger2) + { + logger2.Fatal(reporting, exception, messageTemplate, propertyValue0); + } + else + { + logger.Fatal(reporting, exception, messageTemplate, propertyValue0); + } + } + + public static void Info(this ILogger logger, Type reporting, string messageTemplate, T0 propertyValue0, T1 propertyValue1, T2 propertyValue2) + { + if (logger is ILogger2 logger2) + { + logger2.Info(reporting, messageTemplate, propertyValue0, propertyValue1, propertyValue2); + } + else + { + logger.Info(reporting, messageTemplate, propertyValue0, propertyValue1, propertyValue2); + } + } + + public static void Info(this ILogger logger, Type reporting, string messageTemplate, T0 propertyValue0, T1 propertyValue1) + { + if (logger is ILogger2 logger2) + { + logger2.Info(reporting, messageTemplate, propertyValue0, propertyValue1); + } + else + { + logger.Info(reporting, messageTemplate, propertyValue0, propertyValue1); + } + } + + public static void Info(this ILogger logger, Type reporting, string messageTemplate, T0 propertyValue0) + { + if (logger is ILogger2 logger2) + { + logger2.Info(reporting, messageTemplate, propertyValue0); + } + else + { + logger.Info(reporting, messageTemplate, propertyValue0); + } + } + + public static void Verbose(this ILogger logger, Type reporting, string messageTemplate, T0 propertyValue0, T1 propertyValue1, T2 propertyValue2) + { + if (logger is ILogger2 logger2) + { + logger2.Verbose(reporting, messageTemplate, propertyValue0, propertyValue1, propertyValue2); + } + else + { + logger.Verbose(reporting, messageTemplate, propertyValue0, propertyValue1, propertyValue2); + } + } + + public static void Verbose(this ILogger logger, Type reporting, string messageTemplate, T0 propertyValue0, T1 propertyValue1) + { + if (logger is ILogger2 logger2) + { + logger2.Verbose(reporting, messageTemplate, propertyValue0, propertyValue1); + } + else + { + logger.Verbose(reporting, messageTemplate, propertyValue0, propertyValue1); + } + } + + public static void Verbose(this ILogger logger, Type reporting, string messageTemplate, T0 propertyValue0) + { + if (logger is ILogger2 logger2) + { + logger2.Verbose(reporting, messageTemplate, propertyValue0); + } + else + { + logger.Verbose(reporting, messageTemplate, propertyValue0); + } + } + + public static void Warn(this ILogger logger, Type reporting, Exception exception, string messageTemplate, T0 propertyValue0, T1 propertyValue1, T2 propertyValue2) + { + if (logger is ILogger2 logger2) + { + logger2.Warn(reporting, messageTemplate, exception, propertyValue0, propertyValue1, propertyValue2); + } + else + { + logger.Warn(reporting, messageTemplate, exception, propertyValue0, propertyValue1, propertyValue2); + } + } + + public static void Warn(this ILogger logger, Type reporting, Exception exception, string messageTemplate, T0 propertyValue0, T1 propertyValue1) + { + if (logger is ILogger2 logger2) + { + logger2.Warn(reporting, messageTemplate, exception, propertyValue0, propertyValue1); + } + else + { + logger.Warn(reporting, messageTemplate, exception, propertyValue0, propertyValue1); + } + } + + public static void Warn(this ILogger logger, Type reporting, Exception exception, string messageTemplate, T0 propertyValue0) + { + if (logger is ILogger2 logger2) + { + logger2.Warn(reporting, messageTemplate, exception, propertyValue0); + } + else + { + logger.Warn(reporting, messageTemplate, exception, propertyValue0); + } + } + + public static void Warn(this ILogger logger, Type reporting, string message, T0 propertyValue0) + { + if (logger is ILogger2 logger2) + { + logger2.Warn(reporting, message, propertyValue0); + } + else + { + logger.Warn(reporting, message, propertyValue0); + } + } + + // + public static void Error(this ILogger logger, Exception exception, string messageTemplate, T0 propertyValue0) + { + if (logger is ILogger2 logger2) + { + logger2.Error(typeof(T), exception, messageTemplate, propertyValue0); + } + else + { + logger.Error(typeof(T), exception, messageTemplate, propertyValue0); + } + } + + public static void Error(this ILogger logger, Exception exception, string messageTemplate, T0 propertyValue0, T1 propertyValue1) + { + if (logger is ILogger2 logger2) + { + logger2.Error(typeof(T), exception, messageTemplate, propertyValue0, propertyValue1); + } + else + { + logger.Error(typeof(T), exception, messageTemplate, propertyValue0, propertyValue1); + } + } + + public static void Error(this ILogger logger, Exception exception, string messageTemplate, T0 propertyValue0, T1 propertyValue1, T2 propertyValue2) + { + if (logger is ILogger2 logger2) + { + logger2.Error(typeof(T), exception, messageTemplate, propertyValue0, propertyValue1, propertyValue2); + } + else + { + logger.Error(typeof(T), exception, messageTemplate, propertyValue0, propertyValue1, propertyValue2); + } + } + public static void Error(this ILogger logger, string messageTemplate, T0 propertyValue0) + { + if (logger is ILogger2 logger2) + { + logger2.Error(typeof(T), messageTemplate, propertyValue0); + } + else + { + logger.Error(typeof(T), messageTemplate, propertyValue0); + } + } + public static void Error(this ILogger logger, string messageTemplate, T0 propertyValue0, T1 propertyValue1) + { + if (logger is ILogger2 logger2) + { + logger2.Error(typeof(T), messageTemplate, propertyValue0, propertyValue1); + } + else + { + logger.Error(typeof(T), messageTemplate, propertyValue0, propertyValue1); + } + } + public static void Error(this ILogger logger, string messageTemplate, T0 propertyValue0, T1 propertyValue1, T2 propertyValue2) + { + if (logger is ILogger2 logger2) + { + logger2.Error(typeof(T), messageTemplate, propertyValue0, propertyValue1, propertyValue2); + } + else + { + logger.Error(typeof(T), messageTemplate, propertyValue0, propertyValue1, propertyValue2); + } + } + public static void Warn(this ILogger logger, Exception exception, string messageTemplate, T0 propertyValue0) + { + if (logger is ILogger2 logger2) + { + logger2.Warn(typeof(T), exception, messageTemplate, propertyValue0); + } + else + { + logger.Warn(typeof(T), exception, messageTemplate, propertyValue0); + } + } + + public static void Warn(this ILogger logger, Exception exception, string messageTemplate, T0 propertyValue0, T1 propertyValue1) + { + if (logger is ILogger2 logger2) + { + logger2.Warn(typeof(T), exception, messageTemplate, propertyValue0, propertyValue1); + } + else + { + logger.Warn(typeof(T), exception, messageTemplate, propertyValue0, propertyValue1); + } + } + + public static void Warn(this ILogger logger, Exception exception, string messageTemplate, T0 propertyValue0, T1 propertyValue1, T2 propertyValue2) + { + if (logger is ILogger2 logger2) + { + logger2.Warn(typeof(T), exception, messageTemplate, propertyValue0, propertyValue1, propertyValue2); + } + else + { + logger.Warn(typeof(T), exception, messageTemplate, propertyValue0, propertyValue1, propertyValue2); + } + } + + public static void Warn(this ILogger logger, string messageTemplate, T0 propertyValue0) + { + if (logger is ILogger2 logger2) + { + logger2.Warn(typeof(T), messageTemplate, propertyValue0); + } + else + { + logger.Warn(typeof(T), messageTemplate, propertyValue0); + } + } + public static void Warn(this ILogger logger, string messageTemplate, T0 propertyValue0, T1 propertyValue1) + { + if (logger is ILogger2 logger2) + { + logger2.Warn(typeof(T), messageTemplate, propertyValue0, propertyValue1); + } + else + { + logger.Warn(typeof(T), messageTemplate, propertyValue0, propertyValue1); + } + } + public static void Warn(this ILogger logger, string messageTemplate, T0 propertyValue0, T1 propertyValue1, T2 propertyValue2) + { + if (logger is ILogger2 logger2) + { + logger2.Warn(typeof(T), messageTemplate, propertyValue0, propertyValue1, propertyValue2); + } + else + { + logger.Warn(typeof(T), messageTemplate, propertyValue0, propertyValue1, propertyValue2); + } + } + + + public static void Info(this ILogger logger, string messageTemplate, T0 propertyValue0) + { + if (logger is ILogger2 logger2) + { + logger2.Info(typeof(T), messageTemplate, propertyValue0); + } + else + { + logger.Info(typeof(T), messageTemplate, propertyValue0); + } + } + public static void Info(this ILogger logger, string messageTemplate, T0 propertyValue0, T1 propertyValue1) + { + if (logger is ILogger2 logger2) + { + logger2.Info(typeof(T), messageTemplate, propertyValue0, propertyValue1); + } + else + { + logger.Info(typeof(T), messageTemplate, propertyValue0, propertyValue1); + } + } + public static void Info(this ILogger logger, string messageTemplate, T0 propertyValue0, T1 propertyValue1, T2 propertyValue2) + { + if (logger is ILogger2 logger2) + { + logger2.Info(typeof(T), messageTemplate, propertyValue0, propertyValue1, propertyValue2); + } + else + { + logger.Info(typeof(T), messageTemplate, propertyValue0, propertyValue1, propertyValue2); + } + } + public static void Debug(this ILogger logger, string messageTemplate, T0 propertyValue0) + { + if (logger is ILogger2 logger2) + { + logger2.Debug(typeof(T), messageTemplate, propertyValue0); + } + else + { + logger.Debug(typeof(T), messageTemplate, propertyValue0); + } + } + public static void Debug(this ILogger logger, string messageTemplate, T0 propertyValue0, T1 propertyValue1) + { + if (logger is ILogger2 logger2) + { + logger2.Debug(typeof(T), messageTemplate, propertyValue0, propertyValue1); + } + else + { + logger.Debug(typeof(T), messageTemplate, propertyValue0, propertyValue1); + } + } + public static void Debug(this ILogger logger, string messageTemplate, T0 propertyValue0, T1 propertyValue1, T2 propertyValue2) + { + if (logger is ILogger2 logger2) + { + logger2.Debug(typeof(T), messageTemplate, propertyValue0, propertyValue1, propertyValue2); + } + else + { + logger.Debug(typeof(T), messageTemplate, propertyValue0, propertyValue1, propertyValue2); + } + } + } +} diff --git a/src/Umbraco.Core/Logging/OwinLogger.cs b/src/Umbraco.Core/Logging/OwinLogger.cs index 5601cb53f2..7fc36e748c 100644 --- a/src/Umbraco.Core/Logging/OwinLogger.cs +++ b/src/Umbraco.Core/Logging/OwinLogger.cs @@ -26,34 +26,34 @@ namespace Umbraco.Core.Logging switch (eventType) { case TraceEventType.Critical: - _logger.Fatal(_type.Value, exception, "[{EventType}] Event Id: {EventId}, State: {State}", eventType, eventId, state); + _logger.Fatal(_type.Value, exception, "[{EventType}] Event Id: {EventId}, State: {State}", eventType, eventId, state); return true; case TraceEventType.Error: - _logger.Error(_type.Value, exception, "[{EventType}] Event Id: {EventId}, State: {State}", eventType, eventId, state); + _logger.Error(_type.Value, exception, "[{EventType}] Event Id: {EventId}, State: {State}", eventType, eventId, state); return true; case TraceEventType.Warning: _logger.Warn(_type.Value, "[{EventType}] Event Id: {EventId}, State: {State}", eventType, eventId, state); return true; case TraceEventType.Information: - _logger.Info(_type.Value, "[{EventType}] Event Id: {EventId}, State: {State}", eventType, eventId, state); + _logger.Info(_type.Value, "[{EventType}] Event Id: {EventId}, State: {State}", eventType, eventId, state); return true; case TraceEventType.Verbose: - _logger.Debug(_type.Value, "[{EventType}] Event Id: {EventId}, State: {State}", eventType, eventId, state); + _logger.Debug(_type.Value, "[{EventType}] Event Id: {EventId}, State: {State}", eventType, eventId, state); return true; case TraceEventType.Start: - _logger.Debug(_type.Value, "[{EventType}] Event Id: {EventId}, State: {State}", eventType, eventId, state); + _logger.Debug(_type.Value, "[{EventType}] Event Id: {EventId}, State: {State}", eventType, eventId, state); return true; case TraceEventType.Stop: - _logger.Debug(_type.Value, "[{EventType}] Event Id: {EventId}, State: {State}", eventType, eventId, state); + _logger.Debug(_type.Value, "[{EventType}] Event Id: {EventId}, State: {State}", eventType, eventId, state); return true; case TraceEventType.Suspend: - _logger.Debug(_type.Value, "[{EventType}] Event Id: {EventId}, State: {State}", eventType, eventId, state); + _logger.Debug(_type.Value, "[{EventType}] Event Id: {EventId}, State: {State}", eventType, eventId, state); return true; case TraceEventType.Resume: - _logger.Debug(_type.Value, "[{EventType}] Event Id: {EventId}, State: {State}", eventType, eventId, state); + _logger.Debug(_type.Value, "[{EventType}] Event Id: {EventId}, State: {State}", eventType, eventId, state); return true; case TraceEventType.Transfer: - _logger.Debug(_type.Value, "[{EventType}] Event Id: {EventId}, State: {State}", eventType, eventId, state); + _logger.Debug(_type.Value, "[{EventType}] Event Id: {EventId}, State: {State}", eventType, eventId, state); return true; default: throw new ArgumentOutOfRangeException("eventType"); diff --git a/src/Umbraco.Core/Logging/ProfilingLogger.cs b/src/Umbraco.Core/Logging/ProfilingLogger.cs index d642926147..e49aaa26c6 100644 --- a/src/Umbraco.Core/Logging/ProfilingLogger.cs +++ b/src/Umbraco.Core/Logging/ProfilingLogger.cs @@ -127,6 +127,64 @@ namespace Umbraco.Core.Logging public void Verbose(Type reporting, string messageTemplate, params object[] propertyValues) => Logger.Verbose(reporting, messageTemplate, propertyValues); + public void Fatal(Type reporting, Exception exception, string messageTemplate, T0 propertyValue0, + T1 propertyValue1, T2 propertyValue2) + => Logger.Fatal(reporting, exception, messageTemplate, propertyValue0, propertyValue1, propertyValue2); + + public void Fatal(Type reporting, Exception exception, string messageTemplate, T0 propertyValue0, T1 propertyValue1) + => Logger.Fatal(reporting, exception, messageTemplate, propertyValue0, propertyValue1); + + public void Fatal(Type reporting, Exception exception, string messageTemplate, T0 propertyValue0) + => Logger.Fatal(reporting, exception, messageTemplate, propertyValue0); + + public void Error(Type reporting, Exception exception, string messageTemplate, + T0 propertyValue0, T1 propertyValue1, T2 propertyValue2) + => Logger.Error(reporting, exception, messageTemplate, propertyValue0, propertyValue1, propertyValue2); + + public void Error(Type reporting, Exception exception, string messageTemplate, T0 propertyValue0, T1 propertyValue1) + => Logger.Error(reporting, exception, messageTemplate, propertyValue0, propertyValue1); + + public void Error(Type reporting, Exception exception, string messageTemplate, T0 propertyValue0) + => Logger.Error(reporting, exception, messageTemplate, propertyValue0); + + public void Warn(Type reporting, Exception exception, string messageTemplate, T0 propertyValue0, T1 propertyValue1, T2 propertyValue2) + => Logger.Warn(reporting, exception, messageTemplate, propertyValue0, propertyValue1, propertyValue2); + + public void Warn(Type reporting, Exception exception, string messageTemplate, T0 propertyValue0, T1 propertyValue1) + => Logger.Warn(reporting, exception, messageTemplate, propertyValue0, propertyValue1); + + public void Warn(Type reporting, Exception exception, string messageTemplate, T0 propertyValue0) + => Logger.Warn(reporting, exception, messageTemplate, propertyValue0); + + public void Warn(Type reporting, string message, T0 propertyValue0) => Logger.Warn(reporting, message, propertyValue0); + + public void Info(Type reporting, string messageTemplate, T0 propertyValue0, T1 propertyValue1, T2 propertyValue2) + => Logger.Info(reporting, messageTemplate, propertyValue0, propertyValue1, propertyValue2); + + public void Info(Type reporting, string messageTemplate, T0 propertyValue0, T1 propertyValue1) + => Logger.Info(reporting, messageTemplate, propertyValue0, propertyValue1); + + public void Info(Type reporting, string messageTemplate, T0 propertyValue0) + => Logger.Info(reporting, messageTemplate, propertyValue0); + + public void Debug(Type reporting, string messageTemplate, T0 propertyValue0, T1 propertyValue1, T2 propertyValue2) + => Logger.Debug(reporting, messageTemplate, propertyValue0, propertyValue1, propertyValue2); + + public void Debug(Type reporting, string messageTemplate, T0 propertyValue0, T1 propertyValue1) + => Logger.Debug(reporting, messageTemplate, propertyValue0, propertyValue1); + + public void Debug(Type reporting, string messageTemplate, T0 propertyValue0) + => Logger.Debug(reporting, messageTemplate, propertyValue0); + + public void Verbose(Type reporting, string messageTemplate, T0 propertyValue0, T1 propertyValue1, T2 propertyValue2) + => Logger.Verbose(reporting, messageTemplate, propertyValue0, propertyValue1, propertyValue2); + + public void Verbose(Type reporting, string messageTemplate, T0 propertyValue0, T1 propertyValue1) + => Logger.Verbose(reporting, messageTemplate, propertyValue0, propertyValue1); + + public void Verbose(Type reporting, string messageTemplate, T0 propertyValue0) + => Logger.Verbose(reporting, messageTemplate, propertyValue0); + #endregion } } diff --git a/src/Umbraco.Core/Logging/Serilog/SerilogLogger.cs b/src/Umbraco.Core/Logging/Serilog/SerilogLogger.cs index a51628030e..0f6121c1ee 100644 --- a/src/Umbraco.Core/Logging/Serilog/SerilogLogger.cs +++ b/src/Umbraco.Core/Logging/Serilog/SerilogLogger.cs @@ -118,6 +118,30 @@ namespace Umbraco.Core.Logging.Serilog logger.Fatal(exception, messageTemplate, propertyValues); } + /// + public void Fatal(Type reporting, Exception exception, string messageTemplate, T0 propertyValue0) + { + var logger = LoggerFor(reporting); + DumpThreadAborts(logger, LogEventLevel.Fatal, exception, ref messageTemplate); + logger.Fatal(exception, messageTemplate, propertyValue0); + } + + /// + public void Fatal(Type reporting, Exception exception, string messageTemplate, T0 propertyValue0, T1 propertyValue1) + { + var logger = LoggerFor(reporting); + DumpThreadAborts(logger, LogEventLevel.Fatal, exception, ref messageTemplate); + logger.Fatal(exception, messageTemplate, propertyValue0, propertyValue1); + } + + /// + public void Fatal(Type reporting, Exception exception, string messageTemplate, T0 propertyValue0, T1 propertyValue1, T2 propertyValue2) + { + var logger = LoggerFor(reporting); + DumpThreadAborts(logger, LogEventLevel.Fatal, exception, ref messageTemplate); + logger.Fatal(exception, messageTemplate, propertyValue0, propertyValue1, propertyValue2); + } + /// public void Error(Type reporting, Exception exception, string message) { @@ -154,6 +178,29 @@ namespace Umbraco.Core.Logging.Serilog DumpThreadAborts(logger, LogEventLevel.Error, exception, ref messageTemplate); logger.Error(exception, messageTemplate, propertyValues); } + /// + public void Error(Type reporting, Exception exception, string messageTemplate, T0 propertyValue0) + { + var logger = LoggerFor(reporting); + DumpThreadAborts(logger, LogEventLevel.Error, exception, ref messageTemplate); + logger.Error(exception, messageTemplate, propertyValue0); + } + + /// + public void Error(Type reporting, Exception exception, string messageTemplate, T0 propertyValue0, T1 propertyValue1) + { + var logger = LoggerFor(reporting); + DumpThreadAborts(logger, LogEventLevel.Error, exception, ref messageTemplate); + logger.Error(exception, messageTemplate, propertyValue0, propertyValue1); + } + + /// + public void Error(Type reporting, Exception exception, string messageTemplate, T0 propertyValue0, T1 propertyValue1, T2 propertyValue2) + { + var logger = LoggerFor(reporting); + DumpThreadAborts(logger, LogEventLevel.Error, exception, ref messageTemplate); + logger.Error(exception, messageTemplate, propertyValue0, propertyValue1, propertyValue2); + } private static void DumpThreadAborts(global::Serilog.ILogger logger, LogEventLevel level, Exception exception, ref string messageTemplate) { @@ -224,6 +271,12 @@ namespace Umbraco.Core.Logging.Serilog LoggerFor(reporting).Warning(message, propertyValues); } + /// + public void Warn(Type reporting, string message, T0 propertyValue0) + { + LoggerFor(reporting).Warning(message, propertyValue0); + } + /// public void Warn(Type reporting, Exception exception, string message) { @@ -235,6 +288,29 @@ namespace Umbraco.Core.Logging.Serilog { LoggerFor(reporting).Warning(exception, messageTemplate, propertyValues); } + /// + public void Warn(Type reporting, Exception exception, string messageTemplate, T0 propertyValue0) + { + var logger = LoggerFor(reporting); + DumpThreadAborts(logger, LogEventLevel.Warning, exception, ref messageTemplate); + logger.Warning(exception, messageTemplate, propertyValue0); + } + + /// + public void Warn(Type reporting, Exception exception, string messageTemplate, T0 propertyValue0, T1 propertyValue1) + { + var logger = LoggerFor(reporting); + DumpThreadAborts(logger, LogEventLevel.Warning, exception, ref messageTemplate); + logger.Warning(exception, messageTemplate, propertyValue0, propertyValue1); + } + + /// + public void Warn(Type reporting, Exception exception, string messageTemplate, T0 propertyValue0, T1 propertyValue1, T2 propertyValue2) + { + var logger = LoggerFor(reporting); + DumpThreadAborts(logger, LogEventLevel.Warning, exception, ref messageTemplate); + logger.Warning(exception, messageTemplate, propertyValue0, propertyValue1, propertyValue2); + } /// public void Info(Type reporting, string message) @@ -248,6 +324,23 @@ namespace Umbraco.Core.Logging.Serilog LoggerFor(reporting).Information(messageTemplate, propertyValues); } + + /// + public void Info(Type reporting, string messageTemplate, T0 propertyValue0) + { + LoggerFor(reporting).Information(messageTemplate, propertyValue0); + } + /// + public void Info(Type reporting, string messageTemplate, T0 propertyValue0, T1 propertyValue1) + { + LoggerFor(reporting).Information(messageTemplate, propertyValue0, propertyValue1); + } + /// + public void Info(Type reporting, string messageTemplate, T0 propertyValue0, T1 propertyValue1, T2 propertyValue2) + { + LoggerFor(reporting).Information(messageTemplate, propertyValue0, propertyValue1, propertyValue2); + } + /// public void Debug(Type reporting, string message) { @@ -259,6 +352,21 @@ namespace Umbraco.Core.Logging.Serilog { LoggerFor(reporting).Debug(messageTemplate, propertyValues); } + /// + public void Debug(Type reporting, string messageTemplate, T0 propertyValue0) + { + LoggerFor(reporting).Debug(messageTemplate, propertyValue0); + } + /// + public void Debug(Type reporting, string messageTemplate, T0 propertyValue0, T1 propertyValue1) + { + LoggerFor(reporting).Debug(messageTemplate, propertyValue0, propertyValue1); + } + /// + public void Debug(Type reporting, string messageTemplate, T0 propertyValue0, T1 propertyValue1, T2 propertyValue2) + { + LoggerFor(reporting).Debug(messageTemplate, propertyValue0, propertyValue1, propertyValue2); + } /// public void Verbose(Type reporting, string message) @@ -272,6 +380,22 @@ namespace Umbraco.Core.Logging.Serilog LoggerFor(reporting).Verbose(messageTemplate, propertyValues); } + /// + public void Verbose(Type reporting, string messageTemplate, T0 propertyValue0) + { + LoggerFor(reporting).Verbose(messageTemplate, propertyValue0); + } + /// + public void Verbose(Type reporting, string messageTemplate, T0 propertyValue0, T1 propertyValue1) + { + LoggerFor(reporting).Verbose(messageTemplate, propertyValue0, propertyValue1); + } + /// + public void Verbose(Type reporting, string messageTemplate, T0 propertyValue0, T1 propertyValue1, T2 propertyValue2) + { + LoggerFor(reporting).Verbose(messageTemplate, propertyValue0, propertyValue1, propertyValue2); + } + public void Dispose() { Log.CloseAndFlush(); diff --git a/src/Umbraco.Core/Manifest/DataEditorConverter.cs b/src/Umbraco.Core/Manifest/DataEditorConverter.cs index 86982e17f2..efa9347edb 100644 --- a/src/Umbraco.Core/Manifest/DataEditorConverter.cs +++ b/src/Umbraco.Core/Manifest/DataEditorConverter.cs @@ -62,7 +62,7 @@ namespace Umbraco.Core.Manifest PrepareForPropertyEditor(jobject, dataEditor); else PrepareForParameterEditor(jobject, dataEditor); - + base.Deserialize(jobject, target, serializer); } @@ -86,26 +86,33 @@ namespace Umbraco.Core.Manifest if (jobject["editor"]["validation"] is JObject validation) jobject["editor"]["validation"] = RewriteValidators(validation); - if (jobject["prevalues"] is JObject config) + var prevalues = jobject["prevalues"] as JObject; + var defaultConfig = jobject["defaultConfig"] as JObject; + if (prevalues != null || defaultConfig != null) { // explicitly assign a configuration editor of type ConfigurationEditor // (else the deserializer will try to read it before setting it) // (and besides it's an interface) target.ExplicitConfigurationEditor = new ConfigurationEditor(); - // see note about validators, above - same applies to field validators - if (config["fields"] is JArray jarray) + var config = new JObject(); + if (prevalues != null) { - foreach (var field in jarray) + config = prevalues; + // see note about validators, above - same applies to field validators + if (config["fields"] is JArray jarray) { - if (field["validation"] is JObject fvalidation) - field["validation"] = RewriteValidators(fvalidation); + foreach (var field in jarray) + { + if (field["validation"] is JObject fvalidation) + field["validation"] = RewriteValidators(fvalidation); + } } } // in the manifest, default configuration is at editor level // move it down to configuration editor level so it can be deserialized properly - if (jobject["defaultConfig"] is JObject defaultConfig) + if (defaultConfig != null) { config["defaultConfig"] = defaultConfig; jobject.Remove("defaultConfig"); diff --git a/src/Umbraco.Core/Manifest/ManifestParser.cs b/src/Umbraco.Core/Manifest/ManifestParser.cs index 1ecc738b95..9bbb0875d8 100644 --- a/src/Umbraco.Core/Manifest/ManifestParser.cs +++ b/src/Umbraco.Core/Manifest/ManifestParser.cs @@ -85,7 +85,7 @@ namespace Umbraco.Core.Manifest } catch (Exception e) { - _logger.Error(e, "Failed to parse manifest at '{Path}', ignoring.", path); + _logger.Error(e, "Failed to parse manifest at '{Path}', ignoring.", path); } } diff --git a/src/Umbraco.Core/Manifest/ManifestWatcher.cs b/src/Umbraco.Core/Manifest/ManifestWatcher.cs index 4c0ddbf822..6c195edb45 100644 --- a/src/Umbraco.Core/Manifest/ManifestWatcher.cs +++ b/src/Umbraco.Core/Manifest/ManifestWatcher.cs @@ -54,7 +54,7 @@ namespace Umbraco.Core.Manifest if (_isRestarting) return; _isRestarting = true; - _logger.Info("Manifest has changed, app pool is restarting ({Path})", e.FullPath); + _logger.Info("Manifest has changed, app pool is restarting ({Path})", e.FullPath); HttpRuntime.UnloadAppDomain(); Dispose(); // uh? if the app restarts then this should be disposed anyways? } diff --git a/src/Umbraco.Core/Mapping/UmbracoMapper.cs b/src/Umbraco.Core/Mapping/UmbracoMapper.cs index e62825101c..36e3f9eab9 100644 --- a/src/Umbraco.Core/Mapping/UmbracoMapper.cs +++ b/src/Umbraco.Core/Mapping/UmbracoMapper.cs @@ -3,7 +3,9 @@ using System.Collections; using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; +using Umbraco.Core.Composing; using Umbraco.Core.Exceptions; +using Umbraco.Core.Scoping; namespace Umbraco.Core.Mapping { @@ -42,16 +44,29 @@ namespace Umbraco.Core.Mapping private readonly ConcurrentDictionary>> _maps = new ConcurrentDictionary>>(); + private readonly IScopeProvider _scopeProvider; + /// /// Initializes a new instance of the class. /// /// - public UmbracoMapper(MapDefinitionCollection profiles) + /// + public UmbracoMapper(MapDefinitionCollection profiles, IScopeProvider scopeProvider) { + _scopeProvider = scopeProvider; + foreach (var profile in profiles) profile.DefineMaps(this); } + /// + /// Initializes a new instance of the class. + /// + /// + [Obsolete("This constructor is no longer used and will be removed in future versions, use the other constructor instead")] + public UmbracoMapper(MapDefinitionCollection profiles) : this(profiles, Current.ScopeProvider) + {} + #region Define private static TTarget ThrowCtor(TSource source, MapperContext context) @@ -203,7 +218,10 @@ namespace Umbraco.Core.Mapping if (ctor != null && map != null) { var target = ctor(source, context); - map(source, target, context); + using (var scope = _scopeProvider.CreateScope(autoComplete: true)) + { + map(source, target, context); + } return (TTarget)target; } @@ -248,11 +266,14 @@ namespace Umbraco.Core.Mapping { var targetList = (IList)Activator.CreateInstance(typeof(List<>).MakeGenericType(targetGenericArg)); - foreach (var sourceItem in source) + using (var scope = _scopeProvider.CreateScope(autoComplete: true)) { - var targetItem = ctor(sourceItem, context); - map(sourceItem, targetItem, context); - targetList.Add(targetItem); + foreach (var sourceItem in source) + { + var targetItem = ctor(sourceItem, context); + map(sourceItem, targetItem, context); + targetList.Add(targetItem); + } } object target = targetList; @@ -315,7 +336,10 @@ namespace Umbraco.Core.Mapping // if there is a direct map, map if (map != null) { - map(source, target, context); + using (var scope = _scopeProvider.CreateScope(autoComplete: true)) + { + map(source, target, context); + } return target; } diff --git a/src/Umbraco.Core/Migrations/Install/DatabaseBuilder.cs b/src/Umbraco.Core/Migrations/Install/DatabaseBuilder.cs index fb9b8af46d..7106ef188e 100644 --- a/src/Umbraco.Core/Migrations/Install/DatabaseBuilder.cs +++ b/src/Umbraco.Core/Migrations/Install/DatabaseBuilder.cs @@ -332,9 +332,9 @@ namespace Umbraco.Core.Migrations.Install } // save - logger.Info("Saving connection string to {ConfigFile}.", fileSource); + logger.Info("Saving connection string to {ConfigFile}.", fileSource); xml.Save(fileName, SaveOptions.DisableFormatting); - logger.Info("Saved connection string to {ConfigFile}.", fileSource); + logger.Info("Saved connection string to {ConfigFile}.", fileSource); } private static void AddOrUpdateAttribute(XElement element, string name, string value) @@ -459,7 +459,7 @@ namespace Umbraco.Core.Migrations.Install message = message + "

Installation completed!

"; //now that everything is done, we need to determine the version of SQL server that is executing - _logger.Info("Database configuration status: {DbConfigStatus}", message); + _logger.Info("Database configuration status: {DbConfigStatus}", message); return new Result { Message = message, Success = true, Percentage = "100" }; } @@ -508,7 +508,7 @@ namespace Umbraco.Core.Migrations.Install //now that everything is done, we need to determine the version of SQL server that is executing - _logger.Info("Database configuration status: {DbConfigStatus}", message); + _logger.Info("Database configuration status: {DbConfigStatus}", message); return new Result { Message = message, Success = true, Percentage = "100" }; } @@ -539,7 +539,7 @@ namespace Umbraco.Core.Migrations.Install if (_databaseSchemaValidationResult != null) { - _logger.Info("The database schema validation produced the following summary: {DbSchemaSummary}", _databaseSchemaValidationResult.GetSummary()); + _logger.Info("The database schema validation produced the following summary: {DbSchemaSummary}", _databaseSchemaValidationResult.GetSummary()); } return new Result diff --git a/src/Umbraco.Core/Migrations/Install/DatabaseDataCreator.cs b/src/Umbraco.Core/Migrations/Install/DatabaseDataCreator.cs index 9bd26749ad..44de611348 100644 --- a/src/Umbraco.Core/Migrations/Install/DatabaseDataCreator.cs +++ b/src/Umbraco.Core/Migrations/Install/DatabaseDataCreator.cs @@ -29,7 +29,7 @@ namespace Umbraco.Core.Migrations.Install /// Name of the table to create base data for public void InitializeBaseData(string tableName) { - _logger.Info("Creating data in {TableName}", tableName); + _logger.Info("Creating data in {TableName}", tableName); if (tableName.Equals(Constants.DatabaseSchema.Tables.Node)) CreateNodeData(); @@ -73,7 +73,7 @@ namespace Umbraco.Core.Migrations.Install if (tableName.Equals(Constants.DatabaseSchema.Tables.KeyValue)) CreateKeyValueData(); - _logger.Info("Done creating table {TableName} data.", tableName); + _logger.Info("Done creating table {TableName} data.", tableName); } private void CreateNodeData() diff --git a/src/Umbraco.Core/Migrations/Install/DatabaseSchemaCreator.cs b/src/Umbraco.Core/Migrations/Install/DatabaseSchemaCreator.cs index e9580da74a..c3756cfaad 100644 --- a/src/Umbraco.Core/Migrations/Install/DatabaseSchemaCreator.cs +++ b/src/Umbraco.Core/Migrations/Install/DatabaseSchemaCreator.cs @@ -100,7 +100,7 @@ namespace Umbraco.Core.Migrations.Install var tableNameAttribute = table.FirstAttribute(); var tableName = tableNameAttribute == null ? table.Name : tableNameAttribute.Value; - _logger.Info("Uninstall {TableName}", tableName); + _logger.Info("Uninstall {TableName}", tableName); try { @@ -111,7 +111,7 @@ namespace Umbraco.Core.Migrations.Install { //swallow this for now, not sure how best to handle this with diff databases... though this is internal // and only used for unit tests. If this fails its because the table doesn't exist... generally! - _logger.Error(ex, "Could not drop table {TableName}", tableName); + _logger.Error(ex, "Could not drop table {TableName}", tableName); } } } @@ -436,7 +436,7 @@ namespace Umbraco.Core.Migrations.Install var tableExist = TableExists(tableName); if (overwrite && tableExist) { - _logger.Info("Table {TableName} already exists, but will be recreated", tableName); + _logger.Info("Table {TableName} already exists, but will be recreated", tableName); DropTable(tableName); tableExist = false; @@ -445,19 +445,19 @@ namespace Umbraco.Core.Migrations.Install if (tableExist) { // The table exists and was not recreated/overwritten. - _logger.Info("Table {TableName} already exists - no changes were made", tableName); + _logger.Info("Table {TableName} already exists - no changes were made", tableName); return; } //Execute the Create Table sql _database.Execute(new Sql(createSql)); - _logger.Info("Create Table {TableName}: \n {Sql}", tableName, createSql); + _logger.Info("Create Table {TableName}: \n {Sql}", tableName, createSql); //If any statements exists for the primary key execute them here if (string.IsNullOrEmpty(createPrimaryKeySql) == false) { _database.Execute(new Sql(createPrimaryKeySql)); - _logger.Info("Create Primary Key:\n {Sql}", createPrimaryKeySql); + _logger.Info("Create Primary Key:\n {Sql}", createPrimaryKeySql); } if (SqlSyntax.SupportsIdentityInsert() && tableDefinition.Columns.Any(x => x.IsIdentity)) @@ -475,23 +475,23 @@ namespace Umbraco.Core.Migrations.Install foreach (var sql in indexSql) { _database.Execute(new Sql(sql)); - _logger.Info("Create Index:\n {Sql}", sql); + _logger.Info("Create Index:\n {Sql}", sql); } //Loop through foreignkey statements and execute sql foreach (var sql in foreignSql) { _database.Execute(new Sql(sql)); - _logger.Info("Create Foreign Key:\n {Sql}", sql); + _logger.Info("Create Foreign Key:\n {Sql}", sql); } if (overwrite) { - _logger.Info("Table {TableName} was recreated", tableName); + _logger.Info("Table {TableName} was recreated", tableName); } else { - _logger.Info("New table {TableName} was created", tableName); + _logger.Info("New table {TableName} was created", tableName); } } diff --git a/src/Umbraco.Core/Migrations/MigrationPlan.cs b/src/Umbraco.Core/Migrations/MigrationPlan.cs index 89c3c809e8..088d990221 100644 --- a/src/Umbraco.Core/Migrations/MigrationPlan.cs +++ b/src/Umbraco.Core/Migrations/MigrationPlan.cs @@ -294,11 +294,11 @@ namespace Umbraco.Core.Migrations if (migrationBuilder == null) throw new ArgumentNullException(nameof(migrationBuilder)); if (logger == null) throw new ArgumentNullException(nameof(logger)); - logger.Info("Starting '{MigrationName}'...", Name); + logger.Info("Starting '{MigrationName}'...", Name); var origState = fromState ?? string.Empty; - logger.Info("At {OrigState}", string.IsNullOrWhiteSpace(origState) ? "origin": origState); + logger.Info("At {OrigState}", string.IsNullOrWhiteSpace(origState) ? "origin": origState); if (!_transitions.TryGetValue(origState, out var transition)) ThrowOnUnknownInitialState(origState); @@ -308,7 +308,7 @@ namespace Umbraco.Core.Migrations while (transition != null) { - logger.Info("Execute {MigrationType}", transition.MigrationType.Name); + logger.Info("Execute {MigrationType}", transition.MigrationType.Name); var migration = migrationBuilder.Build(transition.MigrationType, context); migration.Migrate(); @@ -316,7 +316,7 @@ namespace Umbraco.Core.Migrations var nextState = transition.TargetState; origState = nextState; - logger.Info("At {OrigState}", origState); + logger.Info("At {OrigState}", origState); // throw a raw exception here: this should never happen as the plan has // been validated - this is just a paranoid safety test diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DataTypeMigration.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DataTypeMigration.cs index 95b272dcb4..b69158e433 100644 --- a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DataTypeMigration.cs +++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DataTypeMigration.cs @@ -94,7 +94,7 @@ namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0 { if (!LegacyAliases.Contains(dataType.EditorAlias)) { - _logger.Warn( + _logger.Warn( "Skipping validation of configuration for data type {NodeId} : {EditorAlias}." + " Please ensure that the configuration is valid. The site may fail to start and / or load data types and run.", dataType.NodeId, dataType.EditorAlias); @@ -104,7 +104,7 @@ namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0 { if (!LegacyAliases.Contains(newAlias)) { - _logger.Warn("Skipping validation of configuration for data type {NodeId} : {NewEditorAlias} (was: {EditorAlias})" + _logger.Warn("Skipping validation of configuration for data type {NodeId} : {NewEditorAlias} (was: {EditorAlias})" + " because no property editor with that alias was found." + " Please ensure that the configuration is valid. The site may fail to start and / or load data types and run.", dataType.NodeId, newAlias, dataType.EditorAlias); @@ -119,7 +119,7 @@ namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0 } catch (Exception e) { - _logger.Warn(e, "Failed to validate configuration for data type {NodeId} : {NewEditorAlias} (was: {EditorAlias})." + _logger.Warn(e, "Failed to validate configuration for data type {NodeId} : {NewEditorAlias} (was: {EditorAlias})." + " Please fix the configuration and ensure it is valid. The site may fail to start and / or load data types and run.", dataType.NodeId, newAlias, dataType.EditorAlias); } diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DataTypes/PreValueMigratorCollection.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DataTypes/PreValueMigratorCollection.cs index 06f5d45deb..2eadb6c045 100644 --- a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DataTypes/PreValueMigratorCollection.cs +++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DataTypes/PreValueMigratorCollection.cs @@ -19,7 +19,7 @@ namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0.DataTypes public IPreValueMigrator GetMigrator(string editorAlias) { var migrator = this.FirstOrDefault(x => x.CanMigrate(editorAlias)); - _logger.Debug(GetType(), "Getting migrator for \"{EditorAlias}\" = {MigratorType}", editorAlias, migrator == null ? "" : migrator.GetType().Name); + _logger.Debug(GetType(), "Getting migrator for \"{EditorAlias}\" = {MigratorType}", editorAlias, migrator == null ? "" : migrator.GetType().Name); return migrator; } } diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DropDownPropertyEditorsMigration.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DropDownPropertyEditorsMigration.cs index d30719231a..ed2681e6f1 100644 --- a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DropDownPropertyEditorsMigration.cs +++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DropDownPropertyEditorsMigration.cs @@ -46,7 +46,7 @@ namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0 } catch (Exception ex) { - Logger.Error( + Logger.Error( ex, "Invalid configuration: \"{Configuration}\", cannot convert editor.", dataType.Configuration); diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/MergeDateAndDateTimePropertyEditor.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/MergeDateAndDateTimePropertyEditor.cs index 37a83bdd67..d4f483c46e 100644 --- a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/MergeDateAndDateTimePropertyEditor.cs +++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/MergeDateAndDateTimePropertyEditor.cs @@ -36,7 +36,7 @@ namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0 } catch (Exception ex) { - Logger.Error( + Logger.Error( ex, "Invalid property editor configuration detected: \"{Configuration}\", cannot convert editor, values will be cleared", dataType.Configuration); diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/RadioAndCheckboxPropertyEditorsMigration.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/RadioAndCheckboxPropertyEditorsMigration.cs index e96fa1f7e9..fadc3579f2 100644 --- a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/RadioAndCheckboxPropertyEditorsMigration.cs +++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/RadioAndCheckboxPropertyEditorsMigration.cs @@ -50,7 +50,7 @@ namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0 } catch (Exception ex) { - Logger.Error( + Logger.Error( ex, "Invalid configuration: \"{Configuration}\", cannot convert editor.", dataType.Configuration); diff --git a/src/Umbraco.Core/Models/Identity/IdentityMapDefinition.cs b/src/Umbraco.Core/Models/Identity/IdentityMapDefinition.cs index 57e1c9ee5c..2d7eda0bc2 100644 --- a/src/Umbraco.Core/Models/Identity/IdentityMapDefinition.cs +++ b/src/Umbraco.Core/Models/Identity/IdentityMapDefinition.cs @@ -1,4 +1,6 @@ using System; +using Umbraco.Core.Cache; +using Umbraco.Core.Composing; using Umbraco.Core.Configuration; using Umbraco.Core.Mapping; using Umbraco.Core.Models.Membership; @@ -11,12 +13,27 @@ namespace Umbraco.Core.Models.Identity private readonly ILocalizedTextService _textService; private readonly IEntityService _entityService; private readonly IGlobalSettings _globalSettings; + private readonly AppCaches _appCaches; - public IdentityMapDefinition(ILocalizedTextService textService, IEntityService entityService, IGlobalSettings globalSettings) + [Obsolete("Use constructor specifying all dependencies")] + public IdentityMapDefinition( + ILocalizedTextService textService, + IEntityService entityService, + IGlobalSettings globalSettings) + : this(textService, entityService, globalSettings, Current.AppCaches) + { + } + + public IdentityMapDefinition( + ILocalizedTextService textService, + IEntityService entityService, + IGlobalSettings globalSettings, + AppCaches appCaches) { _textService = textService; _entityService = entityService; _globalSettings = globalSettings; + _appCaches = appCaches; } public void DefineMaps(UmbracoMapper mapper) @@ -46,8 +63,8 @@ namespace Umbraco.Core.Models.Identity target.Groups = source.Groups.ToArray(); */ - target.CalculatedMediaStartNodeIds = source.CalculateMediaStartNodeIds(_entityService); - target.CalculatedContentStartNodeIds = source.CalculateContentStartNodeIds(_entityService); + target.CalculatedMediaStartNodeIds = source.CalculateMediaStartNodeIds(_entityService, _appCaches); + target.CalculatedContentStartNodeIds = source.CalculateContentStartNodeIds(_entityService, _appCaches); target.Email = source.Email; target.UserName = source.Username; target.LastPasswordChangeDateUtc = source.LastPasswordChangeDate.ToUniversalTime(); diff --git a/src/Umbraco.Core/Models/Member.cs b/src/Umbraco.Core/Models/Member.cs index 49e07a486d..07f43074a7 100644 --- a/src/Umbraco.Core/Models/Member.cs +++ b/src/Umbraco.Core/Models/Member.cs @@ -501,7 +501,7 @@ namespace Umbraco.Core.Models { void DoLog(string logPropertyAlias, string logPropertyName) { - Current.Logger.Warn("Trying to access the '{PropertyName}' property on '{MemberType}' " + + Current.Logger.Warn("Trying to access the '{PropertyName}' property on '{MemberType}' " + "but the {PropertyAlias} property does not exist on the member type so a default value is returned. " + "Ensure that you have a property type with alias: {PropertyAlias} configured on your member type in order to use the '{PropertyName}' property on the model correctly.", logPropertyName, @@ -526,7 +526,7 @@ namespace Umbraco.Core.Models { void DoLog(string logPropertyAlias, string logPropertyName) { - Current.Logger.Warn("An attempt was made to set a value on the property '{PropertyName}' on type '{MemberType}' but the " + + Current.Logger.Warn("An attempt was made to set a value on the property '{PropertyName}' on type '{MemberType}' but the " + "property type {PropertyAlias} does not exist on the member type, ensure that this property type exists so that setting this property works correctly.", logPropertyName, typeof(Member), diff --git a/src/Umbraco.Core/Models/Membership/User.cs b/src/Umbraco.Core/Models/Membership/User.cs index 3d071b0a18..dc39463925 100644 --- a/src/Umbraco.Core/Models/Membership/User.cs +++ b/src/Umbraco.Core/Models/Membership/User.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.ComponentModel; using System.Linq; using System.Runtime.Serialization; using Umbraco.Core.Composing; @@ -384,11 +385,10 @@ namespace Umbraco.Core.Models.Membership #endregion - /// - /// This is used as an internal cache for this entity - specifically for calculating start nodes so we don't re-calculated all of the time - /// [IgnoreDataMember] [DoNotClone] + [EditorBrowsable(EditorBrowsableState.Never)] + [Obsolete("This should not be used, it's currently used for only a single edge case - should probably be removed for netcore")] internal IDictionary AdditionalData { get @@ -402,6 +402,8 @@ namespace Umbraco.Core.Models.Membership [IgnoreDataMember] [DoNotClone] + [EditorBrowsable(EditorBrowsableState.Never)] + [Obsolete("Not used, will be removed in future versions")] internal object AdditionalDataLock => _additionalDataLock; protected override void PerformDeepClone(object clone) diff --git a/src/Umbraco.Core/Models/Membership/UserGroup.cs b/src/Umbraco.Core/Models/Membership/UserGroup.cs index 31421f990d..eb797d6621 100644 --- a/src/Umbraco.Core/Models/Membership/UserGroup.cs +++ b/src/Umbraco.Core/Models/Membership/UserGroup.cs @@ -19,7 +19,7 @@ namespace Umbraco.Core.Models.Membership private string _icon; private string _name; private IEnumerable _permissions; - private readonly List _sectionCollection; + private List _sectionCollection; //Custom comparer for enumerable private static readonly DelegateEqualityComparer> StringEnumerableComparer = @@ -101,7 +101,10 @@ namespace Umbraco.Core.Models.Membership set => SetPropertyValueAndDetectChanges(value, ref _permissions, nameof(Permissions), StringEnumerableComparer); } - public IEnumerable AllowedSections => _sectionCollection; + public IEnumerable AllowedSections + { + get => _sectionCollection; + } public void RemoveAllowedSection(string sectionAlias) { @@ -121,5 +124,16 @@ namespace Umbraco.Core.Models.Membership } public int UserCount { get; } + + protected override void PerformDeepClone(object clone) + { + + base.PerformDeepClone(clone); + + var clonedEntity = (UserGroup)clone; + + //manually clone the start node props + clonedEntity._sectionCollection = new List(_sectionCollection); + } } } diff --git a/src/Umbraco.Core/Models/UserExtensions.cs b/src/Umbraco.Core/Models/UserExtensions.cs index 5be66bac47..fdb833a821 100644 --- a/src/Umbraco.Core/Models/UserExtensions.cs +++ b/src/Umbraco.Core/Models/UserExtensions.cs @@ -150,48 +150,40 @@ namespace Umbraco.Core.Models } } - internal static bool HasContentRootAccess(this IUser user, IEntityService entityService) - { - return ContentPermissionsHelper.HasPathAccess(Constants.System.RootString, user.CalculateContentStartNodeIds(entityService), Constants.System.RecycleBinContent); - } + internal static bool HasContentRootAccess(this IUser user, IEntityService entityService, AppCaches appCaches) + => ContentPermissionsHelper.HasPathAccess(Constants.System.RootString, user.CalculateContentStartNodeIds(entityService, appCaches), Constants.System.RecycleBinContent); - internal static bool HasContentBinAccess(this IUser user, IEntityService entityService) - { - return ContentPermissionsHelper.HasPathAccess(Constants.System.RecycleBinContentString, user.CalculateContentStartNodeIds(entityService), Constants.System.RecycleBinContent); - } + internal static bool HasContentBinAccess(this IUser user, IEntityService entityService, AppCaches appCaches) + => ContentPermissionsHelper.HasPathAccess(Constants.System.RecycleBinContentString, user.CalculateContentStartNodeIds(entityService, appCaches), Constants.System.RecycleBinContent); - internal static bool HasMediaRootAccess(this IUser user, IEntityService entityService) - { - return ContentPermissionsHelper.HasPathAccess(Constants.System.RootString, user.CalculateMediaStartNodeIds(entityService), Constants.System.RecycleBinMedia); - } + internal static bool HasMediaRootAccess(this IUser user, IEntityService entityService, AppCaches appCaches) + => ContentPermissionsHelper.HasPathAccess(Constants.System.RootString, user.CalculateMediaStartNodeIds(entityService, appCaches), Constants.System.RecycleBinMedia); - internal static bool HasMediaBinAccess(this IUser user, IEntityService entityService) - { - return ContentPermissionsHelper.HasPathAccess(Constants.System.RecycleBinMediaString, user.CalculateMediaStartNodeIds(entityService), Constants.System.RecycleBinMedia); - } + internal static bool HasMediaBinAccess(this IUser user, IEntityService entityService, AppCaches appCaches) + => ContentPermissionsHelper.HasPathAccess(Constants.System.RecycleBinMediaString, user.CalculateMediaStartNodeIds(entityService, appCaches), Constants.System.RecycleBinMedia); - internal static bool HasPathAccess(this IUser user, IContent content, IEntityService entityService) + internal static bool HasPathAccess(this IUser user, IContent content, IEntityService entityService, AppCaches appCaches) { if (content == null) throw new ArgumentNullException(nameof(content)); - return ContentPermissionsHelper.HasPathAccess(content.Path, user.CalculateContentStartNodeIds(entityService), Constants.System.RecycleBinContent); + return ContentPermissionsHelper.HasPathAccess(content.Path, user.CalculateContentStartNodeIds(entityService, appCaches), Constants.System.RecycleBinContent); } - internal static bool HasPathAccess(this IUser user, IMedia media, IEntityService entityService) + internal static bool HasPathAccess(this IUser user, IMedia media, IEntityService entityService, AppCaches appCaches) { if (media == null) throw new ArgumentNullException(nameof(media)); - return ContentPermissionsHelper.HasPathAccess(media.Path, user.CalculateMediaStartNodeIds(entityService), Constants.System.RecycleBinMedia); + return ContentPermissionsHelper.HasPathAccess(media.Path, user.CalculateMediaStartNodeIds(entityService, appCaches), Constants.System.RecycleBinMedia); } - internal static bool HasContentPathAccess(this IUser user, IUmbracoEntity entity, IEntityService entityService) + internal static bool HasContentPathAccess(this IUser user, IUmbracoEntity entity, IEntityService entityService, AppCaches appCaches) { if (entity == null) throw new ArgumentNullException(nameof(entity)); - return ContentPermissionsHelper.HasPathAccess(entity.Path, user.CalculateContentStartNodeIds(entityService), Constants.System.RecycleBinContent); + return ContentPermissionsHelper.HasPathAccess(entity.Path, user.CalculateContentStartNodeIds(entityService, appCaches), Constants.System.RecycleBinContent); } - internal static bool HasMediaPathAccess(this IUser user, IUmbracoEntity entity, IEntityService entityService) + internal static bool HasMediaPathAccess(this IUser user, IUmbracoEntity entity, IEntityService entityService, AppCaches appCaches) { if (entity == null) throw new ArgumentNullException(nameof(entity)); - return ContentPermissionsHelper.HasPathAccess(entity.Path, user.CalculateMediaStartNodeIds(entityService), Constants.System.RecycleBinMedia); + return ContentPermissionsHelper.HasPathAccess(entity.Path, user.CalculateMediaStartNodeIds(entityService, appCaches), Constants.System.RecycleBinMedia); } /// @@ -204,84 +196,92 @@ namespace Umbraco.Core.Models return user.Groups != null && user.Groups.Any(x => x.Alias == Constants.Security.SensitiveDataGroupAlias); } - // calc. start nodes, combining groups' and user's, and excluding what's in the bin + [Obsolete("Use the overload specifying all parameters instead")] public static int[] CalculateContentStartNodeIds(this IUser user, IEntityService entityService) - { - const string cacheKey = "AllContentStartNodes"; - //try to look them up from cache so we don't recalculate - var valuesInUserCache = FromUserCache(user, cacheKey); - if (valuesInUserCache != null) return valuesInUserCache; + => CalculateContentStartNodeIds(user, entityService, Current.AppCaches); - var gsn = user.Groups.Where(x => x.StartContentId.HasValue).Select(x => x.StartContentId.Value).Distinct().ToArray(); - var usn = user.StartContentIds; - var vals = CombineStartNodes(UmbracoObjectTypes.Document, gsn, usn, entityService); - ToUserCache(user, cacheKey, vals); - return vals; + /// + /// Calculate start nodes, combining groups' and user's, and excluding what's in the bin + /// + /// + /// + /// + /// + public static int[] CalculateContentStartNodeIds(this IUser user, IEntityService entityService, AppCaches appCaches) + { + var cacheKey = CacheKeys.UserAllContentStartNodesPrefix + user.Id; + var runtimeCache = appCaches.IsolatedCaches.GetOrCreate(); + var result = runtimeCache.GetCacheItem(cacheKey, () => + { + var gsn = user.Groups.Where(x => x.StartContentId.HasValue).Select(x => x.StartContentId.Value).Distinct().ToArray(); + var usn = user.StartContentIds; + var vals = CombineStartNodes(UmbracoObjectTypes.Document, gsn, usn, entityService); + return vals; + }, TimeSpan.FromMinutes(2), true); + + return result; } - // calc. start nodes, combining groups' and user's, and excluding what's in the bin + [Obsolete("Use the overload specifying all parameters instead")] public static int[] CalculateMediaStartNodeIds(this IUser user, IEntityService entityService) - { - const string cacheKey = "AllMediaStartNodes"; - //try to look them up from cache so we don't recalculate - var valuesInUserCache = FromUserCache(user, cacheKey); - if (valuesInUserCache != null) return valuesInUserCache; + => CalculateMediaStartNodeIds(user, entityService, Current.AppCaches); - var gsn = user.Groups.Where(x => x.StartMediaId.HasValue).Select(x => x.StartMediaId.Value).Distinct().ToArray(); - var usn = user.StartMediaIds; - var vals = CombineStartNodes(UmbracoObjectTypes.Media, gsn, usn, entityService); - ToUserCache(user, cacheKey, vals); - return vals; + /// + /// Calculate start nodes, combining groups' and user's, and excluding what's in the bin + /// + /// + /// + /// + /// + public static int[] CalculateMediaStartNodeIds(this IUser user, IEntityService entityService, AppCaches appCaches) + { + var cacheKey = CacheKeys.UserAllMediaStartNodesPrefix + user.Id; + var runtimeCache = appCaches.IsolatedCaches.GetOrCreate(); + var result = runtimeCache.GetCacheItem(cacheKey, () => + { + var gsn = user.Groups.Where(x => x.StartMediaId.HasValue).Select(x => x.StartMediaId.Value).Distinct().ToArray(); + var usn = user.StartMediaIds; + var vals = CombineStartNodes(UmbracoObjectTypes.Media, gsn, usn, entityService); + return vals; + }, TimeSpan.FromMinutes(2), true); + + return result; } + [Obsolete("Use the overload specifying all parameters instead")] public static string[] GetMediaStartNodePaths(this IUser user, IEntityService entityService) - { - const string cacheKey = "MediaStartNodePaths"; - //try to look them up from cache so we don't recalculate - var valuesInUserCache = FromUserCache(user, cacheKey); - if (valuesInUserCache != null) return valuesInUserCache; + => GetMediaStartNodePaths(user, entityService, Current.AppCaches); - var startNodeIds = user.CalculateMediaStartNodeIds(entityService); - var vals = entityService.GetAllPaths(UmbracoObjectTypes.Media, startNodeIds).Select(x => x.Path).ToArray(); - ToUserCache(user, cacheKey, vals); - return vals; + public static string[] GetMediaStartNodePaths(this IUser user, IEntityService entityService, AppCaches appCaches) + { + var cacheKey = CacheKeys.UserMediaStartNodePathsPrefix + user.Id; + var runtimeCache = appCaches.IsolatedCaches.GetOrCreate(); + var result = runtimeCache.GetCacheItem(cacheKey, () => + { + var startNodeIds = user.CalculateMediaStartNodeIds(entityService, appCaches); + var vals = entityService.GetAllPaths(UmbracoObjectTypes.Media, startNodeIds).Select(x => x.Path).ToArray(); + return vals; + }, TimeSpan.FromMinutes(2), true); + + return result; } + [Obsolete("Use the overload specifying all parameters instead")] public static string[] GetContentStartNodePaths(this IUser user, IEntityService entityService) + => GetContentStartNodePaths(user, entityService, Current.AppCaches); + + public static string[] GetContentStartNodePaths(this IUser user, IEntityService entityService, AppCaches appCaches) { - const string cacheKey = "ContentStartNodePaths"; - //try to look them up from cache so we don't recalculate - var valuesInUserCache = FromUserCache(user, cacheKey); - if (valuesInUserCache != null) return valuesInUserCache; - - var startNodeIds = user.CalculateContentStartNodeIds(entityService); - var vals = entityService.GetAllPaths(UmbracoObjectTypes.Document, startNodeIds).Select(x => x.Path).ToArray(); - ToUserCache(user, cacheKey, vals); - return vals; - } - - private static T FromUserCache(IUser user, string cacheKey) - where T: class - { - if (!(user is User entityUser)) return null; - - lock (entityUser.AdditionalDataLock) + var cacheKey = CacheKeys.UserContentStartNodePathsPrefix + user.Id; + var runtimeCache = appCaches.IsolatedCaches.GetOrCreate(); + var result = runtimeCache.GetCacheItem(cacheKey, () => { - return entityUser.AdditionalData.TryGetValue(cacheKey, out var allContentStartNodes) - ? allContentStartNodes as T - : null; - } - } + var startNodeIds = user.CalculateContentStartNodeIds(entityService, appCaches); + var vals = entityService.GetAllPaths(UmbracoObjectTypes.Document, startNodeIds).Select(x => x.Path).ToArray(); + return vals; + }, TimeSpan.FromMinutes(2), true); - private static void ToUserCache(IUser user, string cacheKey, T vals) - where T: class - { - if (!(user is User entityUser)) return; - - lock (entityUser.AdditionalDataLock) - { - entityUser.AdditionalData[cacheKey] = vals; - } + return result; } private static bool StartsWithPath(string test, string path) diff --git a/src/Umbraco.Core/Packaging/PackageActionRunner.cs b/src/Umbraco.Core/Packaging/PackageActionRunner.cs index 8434f52f30..41dbc5cf4c 100644 --- a/src/Umbraco.Core/Packaging/PackageActionRunner.cs +++ b/src/Umbraco.Core/Packaging/PackageActionRunner.cs @@ -34,7 +34,7 @@ namespace Umbraco.Core.Packaging catch (Exception ex) { e.Add($"{ipa.Alias()} - {ex.Message}"); - _logger.Error(ex, "Error loading package action '{PackageActionAlias}' for package {PackageName}", ipa.Alias(), packageName); + _logger.Error(ex, "Error loading package action '{PackageActionAlias}' for package {PackageName}", ipa.Alias(), packageName); } } @@ -56,7 +56,7 @@ namespace Umbraco.Core.Packaging catch (Exception ex) { e.Add($"{ipa.Alias()} - {ex.Message}"); - _logger.Error(ex, "Error undoing package action '{PackageActionAlias}' for package {PackageName}", ipa.Alias(), packageName); + _logger.Error(ex, "Error undoing package action '{PackageActionAlias}' for package {PackageName}", ipa.Alias(), packageName); } } errors = e; diff --git a/src/Umbraco.Core/Packaging/PackageDataInstallation.cs b/src/Umbraco.Core/Packaging/PackageDataInstallation.cs index 4d1f12baaa..016b349282 100644 --- a/src/Umbraco.Core/Packaging/PackageDataInstallation.cs +++ b/src/Umbraco.Core/Packaging/PackageDataInstallation.cs @@ -358,12 +358,28 @@ namespace Umbraco.Core.Packaging Key = key }; + // Handle culture specific node names + const string nodeNamePrefix = "nodeName-"; + // Get the installed culture iso names, we create a localized content node with a culture that does not exist in the project + // We have to use Invariant comparisons, because when we get them from ContentBase in EntityXmlSerializer they're all lowercase. + var installedLanguages = _localizationService.GetAllLanguages().Select(l => l.IsoCode).ToArray(); + foreach (var localizedNodeName in element.Attributes().Where(a => a.Name.LocalName.InvariantStartsWith(nodeNamePrefix))) + { + var newCulture = localizedNodeName.Name.LocalName.Substring(nodeNamePrefix.Length); + // Skip the culture if it does not exist in the current project + if (installedLanguages.InvariantContains(newCulture)) + { + content.SetCultureName(localizedNodeName.Value, newCulture); + } + } + //Here we make sure that we take composition properties in account as well //otherwise we would skip them and end up losing content var propTypes = contentType.CompositionPropertyTypes.Any() ? contentType.CompositionPropertyTypes.ToDictionary(x => x.Alias, x => x) : contentType.PropertyTypes.ToDictionary(x => x.Alias, x => x); + var foundLanguages = new HashSet(); foreach (var property in properties) { string propertyTypeAlias = property.Name.LocalName; @@ -371,14 +387,30 @@ namespace Umbraco.Core.Packaging { var propertyValue = property.Value; + // Handle properties language attributes + var propertyLang = property.Attribute(XName.Get("lang"))?.Value; + foundLanguages.Add(propertyLang); if (propTypes.TryGetValue(propertyTypeAlias, out var propertyType)) { - //set property value - content.SetValue(propertyTypeAlias, propertyValue); + // set property value + // Skip unsupported language variation, otherwise we'll get a "not supported error" + // We allow null, because that's invariant + if (installedLanguages.InvariantContains(propertyLang) || propertyLang is null) + { + content.SetValue(propertyTypeAlias, propertyValue, propertyLang); + } } } } + foreach (var propertyLang in foundLanguages) + { + if (string.IsNullOrEmpty(content.GetCultureName(propertyLang)) && installedLanguages.InvariantContains(propertyLang)) + { + content.SetCultureName(nodeName, propertyLang); + } + } + return content; } @@ -532,7 +564,7 @@ namespace Umbraco.Core.Packaging var tryCreateFolder = _contentTypeService.CreateContainer(-1, rootFolder); if (tryCreateFolder == false) { - _logger.Error(tryCreateFolder.Exception, "Could not create folder: {FolderName}", rootFolder); + _logger.Error(tryCreateFolder.Exception, "Could not create folder: {FolderName}", rootFolder); throw tryCreateFolder.Exception; } var rootFolderId = tryCreateFolder.Result.Entity.Id; @@ -566,7 +598,7 @@ namespace Umbraco.Core.Packaging var tryCreateFolder = _contentTypeService.CreateContainer(current.Id, folderName); if (tryCreateFolder == false) { - _logger.Error(tryCreateFolder.Exception, "Could not create folder: {FolderName}", folderName); + _logger.Error(tryCreateFolder.Exception, "Could not create folder: {FolderName}", folderName); throw tryCreateFolder.Exception; } return _contentTypeService.GetContainer(tryCreateFolder.Result.Entity.Id); @@ -681,7 +713,7 @@ namespace Umbraco.Core.Packaging } else { - _logger.Warn("Packager: Error handling allowed templates. Template with alias '{TemplateAlias}' could not be found.", alias); + _logger.Warn("Packager: Error handling allowed templates. Template with alias '{TemplateAlias}' could not be found.", alias); } } @@ -697,7 +729,7 @@ namespace Umbraco.Core.Packaging } else { - _logger.Warn("Packager: Error handling default template. Default template with alias '{DefaultTemplateAlias}' could not be found.", defaultTemplateElement.Value); + _logger.Warn("Packager: Error handling default template. Default template with alias '{DefaultTemplateAlias}' could not be found.", defaultTemplateElement.Value); } } } @@ -769,7 +801,7 @@ namespace Umbraco.Core.Packaging if (dataTypeDefinition == null) { // TODO: We should expose this to the UI during install! - _logger.Warn("Packager: Error handling creation of PropertyType '{PropertyType}'. Could not find DataTypeDefintion with unique id '{DataTypeDefinitionId}' nor one referencing the DataType with a property editor alias (or legacy control id) '{PropertyEditorAlias}'. Did the package creator forget to package up custom datatypes? This property will be converted to a label/readonly editor if one exists.", + _logger.Warn("Packager: Error handling creation of PropertyType '{PropertyType}'. Could not find DataTypeDefintion with unique id '{DataTypeDefinitionId}' nor one referencing the DataType with a property editor alias (or legacy control id) '{PropertyEditorAlias}'. Did the package creator forget to package up custom datatypes? This property will be converted to a label/readonly editor if one exists.", property.Element("Name").Value, dataTypeDefinitionId, property.Element("Type").Value.Trim()); //convert to a label! @@ -831,7 +863,7 @@ namespace Umbraco.Core.Packaging var allowedChild = importedContentTypes.ContainsKey(alias) ? importedContentTypes[alias] : _contentTypeService.Get(alias); if (allowedChild == null) { - _logger.Warn( + _logger.Warn( "Packager: Error handling DocumentType structure. DocumentType with alias '{DoctypeAlias}' could not be found and was not added to the structure for '{DoctypeStructureAlias}'.", alias, contentType.Alias); continue; @@ -952,7 +984,7 @@ namespace Umbraco.Core.Packaging var tryCreateFolder = _dataTypeService.CreateContainer(-1, rootFolder); if (tryCreateFolder == false) { - _logger.Error(tryCreateFolder.Exception, "Could not create folder: {FolderName}", rootFolder); + _logger.Error(tryCreateFolder.Exception, "Could not create folder: {FolderName}", rootFolder); throw tryCreateFolder.Exception; } current = _dataTypeService.GetContainer(tryCreateFolder.Result.Entity.Id); @@ -985,7 +1017,7 @@ namespace Umbraco.Core.Packaging var tryCreateFolder = _dataTypeService.CreateContainer(current.Id, folderName); if (tryCreateFolder == false) { - _logger.Error(tryCreateFolder.Exception, "Could not create folder: {FolderName}", folderName); + _logger.Error(tryCreateFolder.Exception, "Could not create folder: {FolderName}", folderName); throw tryCreateFolder.Exception; } return _dataTypeService.GetContainer(tryCreateFolder.Result.Entity.Id); @@ -1290,7 +1322,7 @@ namespace Umbraco.Core.Packaging else if (string.IsNullOrEmpty((string)elementCopy.Element("Master")) == false && templateElements.Any(x => (string)x.Element("Alias") == (string)elementCopy.Element("Master")) == false) { - _logger.Info( + _logger.Info( "Template '{TemplateAlias}' has an invalid Master '{TemplateMaster}', so the reference has been ignored.", (string)elementCopy.Element("Alias"), (string)elementCopy.Element("Master")); diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/ContentRepositoryBase.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/ContentRepositoryBase.cs index a78a073554..ddac0ee3d6 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/ContentRepositoryBase.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/ContentRepositoryBase.cs @@ -701,7 +701,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement { if (ContentRepositoryBase.ThrowOnWarning) throw new InvalidOperationException($"The query returned multiple property sets for content {temp.Id}, {temp.ContentType.Name}"); - Logger.Warn>("The query returned multiple property sets for content {ContentId}, {ContentTypeName}", temp.Id, temp.ContentType.Name); + Logger.Warn, int, string>("The query returned multiple property sets for content {ContentId}, {ContentTypeName}", temp.Id, temp.ContentType.Name); } result[temp.VersionId] = new PropertyCollection(properties); diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/ContentTypeCommonRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/ContentTypeCommonRepository.cs index 90774e4c0b..ec998660fd 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/ContentTypeCommonRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/ContentTypeCommonRepository.cs @@ -171,9 +171,11 @@ namespace Umbraco.Core.Persistence.Repositories.Implement while (compositionIx < compositionDtos.Count && compositionDtos[compositionIx].ChildId == contentType.Id) { var parentDto = compositionDtos[compositionIx]; - if (!contentTypes.TryGetValue(parentDto.ParentId, out var parentContentType)) continue; - contentType.AddContentType(parentContentType); compositionIx++; + + if (!contentTypes.TryGetValue(parentDto.ParentId, out var parentContentType)) + continue; + contentType.AddContentType(parentContentType); } } } diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/ContentTypeRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/ContentTypeRepository.cs index 483f2393af..6571a847a9 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/ContentTypeRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/ContentTypeRepository.cs @@ -228,7 +228,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement if (string.IsNullOrWhiteSpace(entity.Alias)) { var ex = new Exception($"ContentType '{entity.Name}' cannot have an empty Alias. This is most likely due to invalid characters stripped from the Alias."); - Logger.Error("ContentType '{EntityName}' cannot have an empty Alias. This is most likely due to invalid characters stripped from the Alias.", entity.Name); + Logger.Error("ContentType '{EntityName}' cannot have an empty Alias. This is most likely due to invalid characters stripped from the Alias.", entity.Name); throw ex; } diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/ContentTypeRepositoryBase.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/ContentTypeRepositoryBase.cs index ed0c288691..a061337127 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/ContentTypeRepositoryBase.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/ContentTypeRepositoryBase.cs @@ -1203,7 +1203,7 @@ AND umbracoNode.id <> @id", { var ex = new InvalidOperationException($"Property Type '{pt.Name}' cannot have an empty Alias. This is most likely due to invalid characters stripped from the Alias."); - Logger.Error>("Property Type '{PropertyTypeName}' cannot have an empty Alias. This is most likely due to invalid characters stripped from the Alias.", + Logger.Error, string>("Property Type '{PropertyTypeName}' cannot have an empty Alias. This is most likely due to invalid characters stripped from the Alias.", pt.Name); throw ex; @@ -1216,7 +1216,7 @@ AND umbracoNode.id <> @id", { var ex = new InvalidOperationException($"{typeof(TEntity).Name} '{entity.Name}' cannot have an empty Alias. This is most likely due to invalid characters stripped from the Alias."); - Logger.Error>("{EntityTypeName} '{EntityName}' cannot have an empty Alias. This is most likely due to invalid characters stripped from the Alias.", + Logger.Error, string, string>("{EntityTypeName} '{EntityName}' cannot have an empty Alias. This is most likely due to invalid characters stripped from the Alias.", typeof(TEntity).Name, entity.Name); @@ -1248,7 +1248,7 @@ AND umbracoNode.id <> @id", } else { - Logger.Warn>("Could not assign a data type for the property type {PropertyTypeAlias} since no data type was found with a property editor {PropertyEditorAlias}", propertyType.Alias, propertyType.PropertyEditorAlias); + Logger.Warn, string, string>("Could not assign a data type for the property type {PropertyTypeAlias} since no data type was found with a property editor {PropertyEditorAlias}", propertyType.Alias, propertyType.PropertyEditorAlias); } } } diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/UpgradeCheckRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/UpgradeCheckRepository.cs index 365e8ba481..95f699d952 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/UpgradeCheckRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/UpgradeCheckRepository.cs @@ -24,6 +24,11 @@ namespace Umbraco.Core.Persistence.Repositories.Implement return result ?? new UpgradeResult("None", "", ""); } + catch (UnsupportedMediaTypeException) + { + // this occurs if the server for Our is up but doesn't return a valid result (ex. content type) + return new UpgradeResult("None", "", ""); + } catch (HttpRequestException) { // this occurs if the server for Our is down or cannot be reached diff --git a/src/Umbraco.Core/Persistence/SqlSyntax/ISqlSyntaxProvider.cs b/src/Umbraco.Core/Persistence/SqlSyntax/ISqlSyntaxProvider.cs index 7ae001bf24..4c322f9648 100644 --- a/src/Umbraco.Core/Persistence/SqlSyntax/ISqlSyntaxProvider.cs +++ b/src/Umbraco.Core/Persistence/SqlSyntax/ISqlSyntaxProvider.cs @@ -9,6 +9,12 @@ using Umbraco.Core.Persistence.Querying; namespace Umbraco.Core.Persistence.SqlSyntax { + public interface ISqlSyntaxProvider2 : ISqlSyntaxProvider + { + void ReadLock(IDatabase db, TimeSpan timeout, int lockId); + void WriteLock(IDatabase db, TimeSpan timeout, int lockId); + } + /// /// Defines an SqlSyntaxProvider /// @@ -77,7 +83,7 @@ namespace Umbraco.Core.Persistence.SqlSyntax string ConvertIntegerToOrderableString { get; } string ConvertDateToOrderableString { get; } string ConvertDecimalToOrderableString { get; } - + /// /// Returns the default isolation level for the database /// diff --git a/src/Umbraco.Core/Persistence/SqlSyntax/SqlCeSyntaxProvider.cs b/src/Umbraco.Core/Persistence/SqlSyntax/SqlCeSyntaxProvider.cs index 046f54405a..127d00b561 100644 --- a/src/Umbraco.Core/Persistence/SqlSyntax/SqlCeSyntaxProvider.cs +++ b/src/Umbraco.Core/Persistence/SqlSyntax/SqlCeSyntaxProvider.cs @@ -158,6 +158,16 @@ where table_name=@0 and column_name=@1", tableName, columnName).FirstOrDefault() return result > 0; } + public override void WriteLock(IDatabase db, TimeSpan timeout, int lockId) + { + // soon as we get Database, a transaction is started + + if (db.Transaction.IsolationLevel < IsolationLevel.RepeatableRead) + throw new InvalidOperationException("A transaction with minimum RepeatableRead isolation level is required."); + + ObtainWriteLock(db, timeout, lockId); + } + public override void WriteLock(IDatabase db, params int[] lockIds) { // soon as we get Database, a transaction is started @@ -165,17 +175,32 @@ where table_name=@0 and column_name=@1", tableName, columnName).FirstOrDefault() if (db.Transaction.IsolationLevel < IsolationLevel.RepeatableRead) throw new InvalidOperationException("A transaction with minimum RepeatableRead isolation level is required."); - var timeOut = Current.Configs.Global().SqlWriteLockTimeOut; - db.Execute(@"SET LOCK_TIMEOUT " + timeOut + ";"); - // *not* using a unique 'WHERE IN' query here because the *order* of lockIds is important to avoid deadlocks + var timeout = TimeSpan.FromMilliseconds(Current.Configs.Global().SqlWriteLockTimeOut); + foreach (var lockId in lockIds) { - var i = db.Execute(@"UPDATE umbracoLock SET value = (CASE WHEN (value=1) THEN -1 ELSE 1 END) WHERE id=@id", new { id = lockId }); - if (i == 0) // ensure we are actually locking! - throw new ArgumentException($"LockObject with id={lockId} does not exist."); + ObtainWriteLock(db, timeout, lockId); } } + private static void ObtainWriteLock(IDatabase db, TimeSpan timeout, int lockId) + { + db.Execute(@"SET LOCK_TIMEOUT " + timeout.TotalMilliseconds + ";"); + var i = db.Execute(@"UPDATE umbracoLock SET value = (CASE WHEN (value=1) THEN -1 ELSE 1 END) WHERE id=@id", new { id = lockId }); + if (i == 0) // ensure we are actually locking! + throw new ArgumentException($"LockObject with id={lockId} does not exist."); + } + + public override void ReadLock(IDatabase db, TimeSpan timeout, int lockId) + { + // soon as we get Database, a transaction is started + + if (db.Transaction.IsolationLevel < IsolationLevel.RepeatableRead) + throw new InvalidOperationException("A transaction with minimum RepeatableRead isolation level is required."); + + ObtainReadLock(db, timeout, lockId); + } + public override void ReadLock(IDatabase db, params int[] lockIds) { // soon as we get Database, a transaction is started @@ -183,15 +208,25 @@ where table_name=@0 and column_name=@1", tableName, columnName).FirstOrDefault() if (db.Transaction.IsolationLevel < IsolationLevel.RepeatableRead) throw new InvalidOperationException("A transaction with minimum RepeatableRead isolation level is required."); - // *not* using a unique 'WHERE IN' query here because the *order* of lockIds is important to avoid deadlocks foreach (var lockId in lockIds) { - var i = db.ExecuteScalar("SELECT value FROM umbracoLock WHERE id=@id", new { id = lockId }); - if (i == null) // ensure we are actually locking! - throw new ArgumentException($"LockObject with id={lockId} does not exist."); + ObtainReadLock(db, null, lockId); } } + private static void ObtainReadLock(IDatabase db, TimeSpan? timeout, int lockId) + { + if (timeout.HasValue) + { + db.Execute(@"SET LOCK_TIMEOUT " + timeout.Value.TotalMilliseconds + ";"); + } + + var i = db.ExecuteScalar("SELECT value FROM umbracoLock WHERE id=@id", new {id = lockId}); + + if (i == null) // ensure we are actually locking! + throw new ArgumentException($"LockObject with id={lockId} does not exist."); + } + protected override string FormatIdentity(ColumnDefinition column) { return column.IsIdentity ? GetIdentityString(column) : string.Empty; diff --git a/src/Umbraco.Core/Persistence/SqlSyntax/SqlServerSyntaxProvider.cs b/src/Umbraco.Core/Persistence/SqlSyntax/SqlServerSyntaxProvider.cs index 3fc5e36f6e..a038d06121 100644 --- a/src/Umbraco.Core/Persistence/SqlSyntax/SqlServerSyntaxProvider.cs +++ b/src/Umbraco.Core/Persistence/SqlSyntax/SqlServerSyntaxProvider.cs @@ -254,30 +254,50 @@ where tbl.[name]=@0 and col.[name]=@1;", tableName, columnName) return result > 0; } - public override void WriteLock(IDatabase db, params int[] lockIds) - { - var timeOut = Current.Configs.Global().SqlWriteLockTimeOut; - WriteLock(db, TimeSpan.FromMilliseconds(timeOut), lockIds); - } - - public void WriteLock(IDatabase db, TimeSpan timeout, params int[] lockIds) + public override void WriteLock(IDatabase db, TimeSpan timeout, int lockId) { // soon as we get Database, a transaction is started if (db.Transaction.IsolationLevel < IsolationLevel.ReadCommitted) throw new InvalidOperationException("A transaction with minimum ReadCommitted isolation level is required."); + ObtainWriteLock(db, timeout, lockId); + } + + public override void WriteLock(IDatabase db, params int[] lockIds) + { + // soon as we get Database, a transaction is started + + if (db.Transaction.IsolationLevel < IsolationLevel.ReadCommitted) + throw new InvalidOperationException("A transaction with minimum ReadCommitted isolation level is required."); + + var timeout = TimeSpan.FromMilliseconds(Current.Configs.Global().SqlWriteLockTimeOut); - // *not* using a unique 'WHERE IN' query here because the *order* of lockIds is important to avoid deadlocks foreach (var lockId in lockIds) { - db.Execute($"SET LOCK_TIMEOUT {timeout.TotalMilliseconds};"); - var i = db.Execute(@"UPDATE umbracoLock WITH (REPEATABLEREAD) SET value = (CASE WHEN (value=1) THEN -1 ELSE 1 END) WHERE id=@id", new { id = lockId }); - if (i == 0) // ensure we are actually locking! - throw new ArgumentException($"LockObject with id={lockId} does not exist."); + ObtainWriteLock(db, timeout, lockId); } } + private static void ObtainWriteLock(IDatabase db, TimeSpan timeout, int lockId) + { + db.Execute("SET LOCK_TIMEOUT " + timeout.TotalMilliseconds + ";"); + var i = db.Execute( + @"UPDATE umbracoLock WITH (REPEATABLEREAD) SET value = (CASE WHEN (value=1) THEN -1 ELSE 1 END) WHERE id=@id", + new {id = lockId}); + if (i == 0) // ensure we are actually locking! + throw new ArgumentException($"LockObject with id={lockId} does not exist."); + } + + public override void ReadLock(IDatabase db, TimeSpan timeout, int lockId) + { + // soon as we get Database, a transaction is started + + if (db.Transaction.IsolationLevel < IsolationLevel.ReadCommitted) + throw new InvalidOperationException("A transaction with minimum RepeatableRead isolation level is required."); + + ObtainReadLock(db, timeout, lockId); + } public override void ReadLock(IDatabase db, params int[] lockIds) { @@ -286,15 +306,25 @@ where tbl.[name]=@0 and col.[name]=@1;", tableName, columnName) if (db.Transaction.IsolationLevel < IsolationLevel.ReadCommitted) throw new InvalidOperationException("A transaction with minimum ReadCommitted isolation level is required."); - // *not* using a unique 'WHERE IN' query here because the *order* of lockIds is important to avoid deadlocks foreach (var lockId in lockIds) { - var i = db.ExecuteScalar("SELECT value FROM umbracoLock WITH (REPEATABLEREAD) WHERE id=@id", new { id = lockId }); - if (i == null) // ensure we are actually locking! - throw new ArgumentException($"LockObject with id={lockId} does not exist.", nameof(lockIds)); + ObtainReadLock(db, null, lockId); } } + private static void ObtainReadLock(IDatabase db, TimeSpan? timeout, int lockId) + { + if (timeout.HasValue) + { + db.Execute(@"SET LOCK_TIMEOUT " + timeout.Value.TotalMilliseconds + ";"); + } + + var i = db.ExecuteScalar("SELECT value FROM umbracoLock WITH (REPEATABLEREAD) WHERE id=@id", new {id = lockId}); + + if (i == null) // ensure we are actually locking! + throw new ArgumentException($"LockObject with id={lockId} does not exist."); + } + public override string FormatColumnRename(string tableName, string oldName, string newName) { return string.Format(RenameColumn, tableName, oldName, newName); diff --git a/src/Umbraco.Core/Persistence/SqlSyntax/SqlSyntaxProviderBase.cs b/src/Umbraco.Core/Persistence/SqlSyntax/SqlSyntaxProviderBase.cs index 8570c49f69..6f13afb24c 100644 --- a/src/Umbraco.Core/Persistence/SqlSyntax/SqlSyntaxProviderBase.cs +++ b/src/Umbraco.Core/Persistence/SqlSyntax/SqlSyntaxProviderBase.cs @@ -19,7 +19,7 @@ namespace Umbraco.Core.Persistence.SqlSyntax /// All Sql Syntax provider implementations should derive from this abstract class. /// /// - public abstract class SqlSyntaxProviderBase : ISqlSyntaxProvider + public abstract class SqlSyntaxProviderBase : ISqlSyntaxProvider2 where TSyntax : ISqlSyntaxProvider { protected SqlSyntaxProviderBase() @@ -235,6 +235,9 @@ namespace Umbraco.Core.Persistence.SqlSyntax public abstract void ReadLock(IDatabase db, params int[] lockIds); public abstract void WriteLock(IDatabase db, params int[] lockIds); + public abstract void ReadLock(IDatabase db, TimeSpan timeout, int lockId); + + public abstract void WriteLock(IDatabase db, TimeSpan timeout, int lockId); public virtual bool DoesTableExist(IDatabase db, string tableName) { diff --git a/src/Umbraco.Core/Persistence/UmbracoDatabase.cs b/src/Umbraco.Core/Persistence/UmbracoDatabase.cs index a95d95ea08..5e6192eb5c 100644 --- a/src/Umbraco.Core/Persistence/UmbracoDatabase.cs +++ b/src/Umbraco.Core/Persistence/UmbracoDatabase.cs @@ -203,10 +203,10 @@ namespace Umbraco.Core.Persistence protected override void OnException(Exception ex) { - _logger.Error(ex, "Exception ({InstanceId}).", InstanceId); - _logger.Debug("At:\r\n{StackTrace}", Environment.StackTrace); + _logger.Error(ex, "Exception ({InstanceId}).", InstanceId); + _logger.Debug("At:\r\n{StackTrace}", Environment.StackTrace); if (EnableSqlTrace == false) - _logger.Debug("Sql:\r\n{Sql}", CommandToString(LastSQL, LastArgs)); + _logger.Debug("Sql:\r\n{Sql}", CommandToString(LastSQL, LastArgs)); base.OnException(ex); } @@ -219,7 +219,7 @@ namespace Umbraco.Core.Persistence cmd.CommandTimeout = cmd.Connection.ConnectionTimeout; if (EnableSqlTrace) - _logger.Debug("SQL Trace:\r\n{Sql}", CommandToString(cmd).Replace("{", "{{").Replace("}", "}}")); // TODO: these escapes should be builtin + _logger.Debug("SQL Trace:\r\n{Sql}", CommandToString(cmd).Replace("{", "{{").Replace("}", "}}")); // TODO: these escapes should be builtin #if DEBUG_DATABASES // detects whether the command is already in use (eg still has an open reader...) diff --git a/src/Umbraco.Core/Persistence/UmbracoDatabaseFactory.cs b/src/Umbraco.Core/Persistence/UmbracoDatabaseFactory.cs index baab8c486e..b113769a1a 100644 --- a/src/Umbraco.Core/Persistence/UmbracoDatabaseFactory.cs +++ b/src/Umbraco.Core/Persistence/UmbracoDatabaseFactory.cs @@ -162,7 +162,7 @@ namespace Umbraco.Core.Persistence // else leave unchanged } - _logger.Debug("SqlServer {SqlServerVersion}, DatabaseType is {DatabaseType} ({Source}).", + _logger.Debug("SqlServer {SqlServerVersion}, DatabaseType is {DatabaseType} ({Source}).", versionName, _databaseType, fromSettings ? "settings" : "detected"); } diff --git a/src/Umbraco.Core/PropertyEditors/DataValueEditor.cs b/src/Umbraco.Core/PropertyEditors/DataValueEditor.cs index eebe5f5722..fbcd5ec440 100644 --- a/src/Umbraco.Core/PropertyEditors/DataValueEditor.cs +++ b/src/Umbraco.Core/PropertyEditors/DataValueEditor.cs @@ -219,7 +219,7 @@ namespace Umbraco.Core.PropertyEditors var result = TryConvertValueToCrlType(editorValue.Value); if (result.Success == false) { - Current.Logger.Warn("The value {EditorValue} cannot be converted to the type {StorageTypeValue}", editorValue.Value, ValueTypes.ToStorageType(ValueType)); + Current.Logger.Warn("The value {EditorValue} cannot be converted to the type {StorageTypeValue}", editorValue.Value, ValueTypes.ToStorageType(ValueType)); return null; } return result.Result; diff --git a/src/Umbraco.Core/PropertyEditors/EyeDropperColorPickerConfiguration.cs b/src/Umbraco.Core/PropertyEditors/EyeDropperColorPickerConfiguration.cs new file mode 100644 index 0000000000..3e74b074f9 --- /dev/null +++ b/src/Umbraco.Core/PropertyEditors/EyeDropperColorPickerConfiguration.cs @@ -0,0 +1,14 @@ +namespace Umbraco.Core.PropertyEditors +{ + /// + /// Represents the configuration for the Eye Dropper picker value editor. + /// + public class EyeDropperColorPickerConfiguration + { + [ConfigurationField("showAlpha", "Show alpha", "boolean", Description = "Allow alpha transparency selection.")] + public bool ShowAlpha { get; set; } + + [ConfigurationField("showPalette", "Show palette", "boolean", Description = "Show a palette next to the color picker.")] + public bool ShowPalette { get; set; } + } +} diff --git a/src/Umbraco.Core/PropertyEditors/ValueConverters/GridValueConverter.cs b/src/Umbraco.Core/PropertyEditors/ValueConverters/GridValueConverter.cs index b3685457ec..fdb0a8c842 100644 --- a/src/Umbraco.Core/PropertyEditors/ValueConverters/GridValueConverter.cs +++ b/src/Umbraco.Core/PropertyEditors/ValueConverters/GridValueConverter.cs @@ -96,7 +96,7 @@ namespace Umbraco.Core.PropertyEditors.ValueConverters } catch (Exception ex) { - Current.Logger.Error(ex, "Could not parse the string '{JsonString}' to a json object", sourceString); + Current.Logger.Error(ex, "Could not parse the string '{JsonString}' to a json object", sourceString); } } diff --git a/src/Umbraco.Core/PropertyEditors/ValueConverters/ImageCropperValueConverter.cs b/src/Umbraco.Core/PropertyEditors/ValueConverters/ImageCropperValueConverter.cs index 8926174c03..f8ef82076e 100644 --- a/src/Umbraco.Core/PropertyEditors/ValueConverters/ImageCropperValueConverter.cs +++ b/src/Umbraco.Core/PropertyEditors/ValueConverters/ImageCropperValueConverter.cs @@ -25,6 +25,12 @@ namespace Umbraco.Core.PropertyEditors.ValueConverters public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType) => PropertyCacheLevel.Element; + private static readonly JsonSerializerSettings ImageCropperValueJsonSerializerSettings = new JsonSerializerSettings + { + Culture = CultureInfo.InvariantCulture, + FloatParseHandling = FloatParseHandling.Decimal + }; + /// public override object ConvertSourceToIntermediate(IPublishedElement owner, IPublishedPropertyType propertyType, object source, bool preview) { @@ -34,16 +40,12 @@ namespace Umbraco.Core.PropertyEditors.ValueConverters ImageCropperValue value; try { - value = JsonConvert.DeserializeObject(sourceString, new JsonSerializerSettings - { - Culture = CultureInfo.InvariantCulture, - FloatParseHandling = FloatParseHandling.Decimal - }); + value = JsonConvert.DeserializeObject(sourceString, ImageCropperValueJsonSerializerSettings); } catch (Exception ex) { - // cannot deserialize, assume it may be a raw image URL - Current.Logger.Error(ex, "Could not deserialize string '{JsonString}' into an image cropper value.", sourceString); + // cannot deserialize, assume it may be a raw image url + Current.Logger.Error(ex, "Could not deserialize string '{JsonString}' into an image cropper value.", sourceString); value = new ImageCropperValue { Src = sourceString }; } diff --git a/src/Umbraco.Core/PropertyEditors/ValueConverters/JsonValueConverter.cs b/src/Umbraco.Core/PropertyEditors/ValueConverters/JsonValueConverter.cs index 12e6238705..270c8c3b0b 100644 --- a/src/Umbraco.Core/PropertyEditors/ValueConverters/JsonValueConverter.cs +++ b/src/Umbraco.Core/PropertyEditors/ValueConverters/JsonValueConverter.cs @@ -57,7 +57,7 @@ namespace Umbraco.Core.PropertyEditors.ValueConverters } catch (Exception ex) { - Current.Logger.Error(ex, "Could not parse the string '{JsonString}' to a json object", sourceString); + Current.Logger.Error(ex, "Could not parse the string '{JsonString}' to a json object", sourceString); } } diff --git a/src/Umbraco.Core/Runtime/CoreRuntime.cs b/src/Umbraco.Core/Runtime/CoreRuntime.cs index 60e7851251..25bb5d3151 100644 --- a/src/Umbraco.Core/Runtime/CoreRuntime.cs +++ b/src/Umbraco.Core/Runtime/CoreRuntime.cs @@ -98,7 +98,7 @@ namespace Umbraco.Core.Runtime HostingEnvironment.ApplicationID, HostingEnvironment.ApplicationPhysicalPath, NetworkHelper.MachineName); - logger.Debug("Runtime: {Runtime}", GetType().FullName); + logger.Debug("Runtime: {Runtime}", GetType().FullName); // application environment ConfigureUnhandledException(); @@ -359,7 +359,7 @@ namespace Umbraco.Core.Runtime { _state.DetermineRuntimeLevel(databaseFactory); - profilingLogger.Debug("Runtime level: {RuntimeLevel} - {RuntimeLevelReason}", _state.Level, _state.Reason); + profilingLogger.Debug("Runtime level: {RuntimeLevel} - {RuntimeLevelReason}", _state.Level, _state.Reason); if (_state.Level == RuntimeLevel.Upgrade) { diff --git a/src/Umbraco.Core/Runtime/MainDom.cs b/src/Umbraco.Core/Runtime/MainDom.cs index 71842905dd..d784560f2c 100644 --- a/src/Umbraco.Core/Runtime/MainDom.cs +++ b/src/Umbraco.Core/Runtime/MainDom.cs @@ -101,14 +101,14 @@ namespace Umbraco.Core.Runtime lock (_locko) { - _logger.Debug("Signaled ({Signaled}) ({SignalSource})", _signaled ? "again" : "first", source); + _logger.Debug("Signaled ({Signaled}) ({SignalSource})", _signaled ? "again" : "first", source); if (_signaled) return; if (_isMainDom == false) return; // probably not needed _signaled = true; try { - _logger.Info("Stopping ({SignalSource})", source); + _logger.Info("Stopping ({SignalSource})", source); foreach (var callback in _callbacks.OrderBy(x => x.Key).Select(x => x.Value)) { try @@ -122,14 +122,14 @@ namespace Umbraco.Core.Runtime } } - _logger.Debug("Stopped ({SignalSource})", source); + _logger.Debug("Stopped ({SignalSource})", source); } finally { // in any case... _isMainDom = false; _mainDomLock.Dispose(); - _logger.Info("Released ({SignalSource})", source); + _logger.Info("Released ({SignalSource})", source); } } @@ -179,7 +179,14 @@ namespace Umbraco.Core.Runtime _listenTask = _mainDomLock.ListenAsync(); _listenCompleteTask = _listenTask.ContinueWith(t => { - _logger.Debug("Listening task completed with {TaskStatus}", _listenTask.Status); + if (_listenTask.Exception != null) + { + _logger.Warn("Listening task completed with {TaskStatus}, Exception: {Exception}", _listenTask.Status, _listenTask.Exception); + } + else + { + _logger.Debug("Listening task completed with {TaskStatus}", _listenTask.Status); + } OnSignal("signal"); }, TaskScheduler.Default); // Must explicitly specify this, see https://blog.stephencleary.com/2013/10/continuewith-is-dangerous-too.html diff --git a/src/Umbraco.Core/Runtime/SqlMainDomLock.cs b/src/Umbraco.Core/Runtime/SqlMainDomLock.cs index f58b279a8d..12359c96d1 100644 --- a/src/Umbraco.Core/Runtime/SqlMainDomLock.cs +++ b/src/Umbraco.Core/Runtime/SqlMainDomLock.cs @@ -1,5 +1,6 @@ using NPoco; using System; +using System.Configuration; using System.Data; using System.Data.SqlClient; using System.Diagnostics; @@ -7,6 +8,7 @@ using System.Linq; using System.Security.Cryptography; using System.Threading; using System.Threading.Tasks; +using Umbraco.Core.Configuration; using Umbraco.Core.Logging; using Umbraco.Core.Persistence; using Umbraco.Core.Persistence.Dtos; @@ -18,6 +20,7 @@ namespace Umbraco.Core.Runtime { internal class SqlMainDomLock : IMainDomLock { + private readonly TimeSpan _lockTimeout; private string _lockId; private const string MainDomKeyPrefix = "Umbraco.Core.Runtime.SqlMainDom"; private const string UpdatedSuffix = "_updated"; @@ -40,6 +43,8 @@ namespace Umbraco.Core.Runtime Constants.System.UmbracoConnectionName, _logger, new Lazy(() => new MapperCollection(Enumerable.Empty()))); + + _lockTimeout = TimeSpan.FromMilliseconds(GlobalSettings.GetSqlWriteLockTimeoutFromConfigFile(logger)); } public async Task AcquireLockAsync(int millisecondsTimeout) @@ -81,7 +86,7 @@ namespace Umbraco.Core.Runtime // wait to get a write lock _sqlServerSyntax.WriteLock(db, TimeSpan.FromMilliseconds(millisecondsTimeout), Constants.Locks.MainDom); } - catch(SqlException ex) + catch (SqlException ex) { if (IsLockTimeoutException(ex)) { @@ -100,7 +105,7 @@ namespace Umbraco.Core.Runtime // if we've inserted, then there was no MainDom so we can instantly acquire InsertLockRecord(_lockId, db); // so update with our appdomain id - _logger.Debug("Acquired with ID {LockId}", _lockId); + _logger.Debug("Acquired with ID {LockId}", _lockId); return true; } @@ -121,7 +126,7 @@ namespace Umbraco.Core.Runtime } - return await WaitForExistingAsync(tempId, millisecondsTimeout); + return await WaitForExistingAsync(tempId, millisecondsTimeout).ConfigureAwait(false); } public Task ListenAsync() @@ -134,13 +139,15 @@ namespace Umbraco.Core.Runtime // Create a long running task (dedicated thread) // to poll to check if we are still the MainDom registered in the DB - return Task.Factory.StartNew( - ListeningLoop, - _cancellationTokenSource.Token, - TaskCreationOptions.LongRunning, - // Must explicitly specify this, see https://blog.stephencleary.com/2013/10/continuewith-is-dangerous-too.html - TaskScheduler.Default); - + using (ExecutionContext.SuppressFlow()) + { + return Task.Factory.StartNew( + ListeningLoop, + _cancellationTokenSource.Token, + TaskCreationOptions.LongRunning, + // Must explicitly specify this, see https://blog.stephencleary.com/2013/10/continuewith-is-dangerous-too.html + TaskScheduler.Default); + } } /// @@ -198,7 +205,7 @@ namespace Umbraco.Core.Runtime db.BeginTransaction(IsolationLevel.ReadCommitted); // get a read lock - _sqlServerSyntax.ReadLock(db, Constants.Locks.MainDom); + _sqlServerSyntax.ReadLock(db, _lockTimeout, Constants.Locks.MainDom); if (!IsMainDomValue(_lockId, db)) { @@ -222,11 +229,29 @@ namespace Umbraco.Core.Runtime } finally { - db?.CompleteTransaction(); - db?.Dispose(); + // Even if any of the above fail like BeginTransaction, or even a query after the + // Transaction is started, the calls below will not throw. I've tried all sorts of + // combinations to see if I can make this throw but I can't. In any case, we'll be + // extra safe and try/catch/log + try + { + db?.CompleteTransaction(); + } + catch (Exception ex) + { + _logger.Error(ex, "Unexpected error completing transaction."); + } + + try + { + db?.Dispose(); + } + catch (Exception ex) + { + _logger.Error(ex, "Unexpected error completing disposing."); + } } } - } } @@ -240,37 +265,40 @@ namespace Umbraco.Core.Runtime { var updatedTempId = tempId + UpdatedSuffix; - return Task.Run(() => + using (ExecutionContext.SuppressFlow()) { - try + return Task.Run(() => { - using var db = _dbFactory.CreateDatabase(); - - var watch = new Stopwatch(); - watch.Start(); - while (true) + try { - // poll very often, we need to take over as fast as we can - // local testing shows the actual query to be executed from client/server is approx 300ms but would change depending on environment/IO - Thread.Sleep(1000); + using var db = _dbFactory.CreateDatabase(); - var acquired = TryAcquire(db, tempId, updatedTempId); - if (acquired.HasValue) - return acquired.Value; - - if (watch.ElapsedMilliseconds >= millisecondsTimeout) + var watch = new Stopwatch(); + watch.Start(); + while (true) { - return AcquireWhenMaxWaitTimeElapsed(db); + // poll very often, we need to take over as fast as we can + // local testing shows the actual query to be executed from client/server is approx 300ms but would change depending on environment/IO + Thread.Sleep(1000); + + var acquired = TryAcquire(db, tempId, updatedTempId); + if (acquired.HasValue) + return acquired.Value; + + if (watch.ElapsedMilliseconds >= millisecondsTimeout) + { + return AcquireWhenMaxWaitTimeElapsed(db); + } } } - } - catch (Exception ex) - { - _logger.Error(ex, "An error occurred trying to acquire and waiting for existing SqlMainDomLock to shutdown"); - return false; - } + catch (Exception ex) + { + _logger.Error(ex, "An error occurred trying to acquire and waiting for existing SqlMainDomLock to shutdown"); + return false; + } - }, _cancellationTokenSource.Token); + }, _cancellationTokenSource.Token); + } } private bool? TryAcquire(IUmbracoDatabase db, string tempId, string updatedTempId) @@ -284,7 +312,7 @@ namespace Umbraco.Core.Runtime { transaction = db.GetTransaction(IsolationLevel.ReadCommitted); // get a read lock - _sqlServerSyntax.ReadLock(db, Constants.Locks.MainDom); + _sqlServerSyntax.ReadLock(db, _lockTimeout, Constants.Locks.MainDom); // the row var mainDomRows = db.Fetch("SELECT * FROM umbracoKeyValue WHERE [key] = @key", new { key = MainDomKey }); @@ -296,11 +324,11 @@ namespace Umbraco.Core.Runtime // which indicates that we // can acquire it and it has shutdown. - _sqlServerSyntax.WriteLock(db, Constants.Locks.MainDom); + _sqlServerSyntax.WriteLock(db, _lockTimeout, Constants.Locks.MainDom); // so now we update the row with our appdomain id InsertLockRecord(_lockId, db); - _logger.Debug("Acquired with ID {LockId}", _lockId); + _logger.Debug("Acquired with ID {LockId}", _lockId); return true; } else if (mainDomRows.Count == 1 && !mainDomRows[0].Value.StartsWith(tempId)) @@ -355,11 +383,11 @@ namespace Umbraco.Core.Runtime { transaction = db.GetTransaction(IsolationLevel.ReadCommitted); - _sqlServerSyntax.WriteLock(db, Constants.Locks.MainDom); + _sqlServerSyntax.WriteLock(db, _lockTimeout, Constants.Locks.MainDom); // so now we update the row with our appdomain id InsertLockRecord(_lockId, db); - _logger.Debug("Acquired with ID {LockId}", _lockId); + _logger.Debug("Acquired with ID {LockId}", _lockId); return true; } catch (Exception ex) @@ -438,7 +466,7 @@ namespace Umbraco.Core.Runtime db.BeginTransaction(IsolationLevel.ReadCommitted); // get a write lock - _sqlServerSyntax.WriteLock(db, Constants.Locks.MainDom); + _sqlServerSyntax.WriteLock(db, _lockTimeout, Constants.Locks.MainDom); // When we are disposed, it means we have released the MainDom lock // and called all MainDom release callbacks, in this case diff --git a/src/Umbraco.Core/RuntimeState.cs b/src/Umbraco.Core/RuntimeState.cs index c7a1a18d44..bfc125e44f 100644 --- a/src/Umbraco.Core/RuntimeState.cs +++ b/src/Umbraco.Core/RuntimeState.cs @@ -144,13 +144,13 @@ namespace Umbraco.Core { // there *is* a local version, but it does not match the code version // need to upgrade - _logger.Debug("Local version '{LocalVersion}' < code version '{CodeVersion}', need to upgrade Umbraco.", localVersion, codeVersion); + _logger.Debug("Local version '{LocalVersion}' < code version '{CodeVersion}', need to upgrade Umbraco.", localVersion, codeVersion); Level = RuntimeLevel.Upgrade; Reason = RuntimeLevelReason.UpgradeOldVersion; } else if (localVersion > codeVersion) { - _logger.Warn("Local version '{LocalVersion}' > code version '{CodeVersion}', downgrading is not supported.", localVersion, codeVersion); + _logger.Warn("Local version '{LocalVersion}' > code version '{CodeVersion}', downgrading is not supported.", localVersion, codeVersion); // in fact, this is bad enough that we want to throw Reason = RuntimeLevelReason.BootFailedCannotDowngrade; @@ -290,7 +290,7 @@ namespace Umbraco.Core CurrentMigrationState = KeyValueService.GetValue(database, stateValueKey); FinalMigrationState = upgrader.Plan.FinalState; - _logger.Debug("Final upgrade state is {FinalMigrationState}, database contains {DatabaseState}", FinalMigrationState, CurrentMigrationState ?? ""); + _logger.Debug("Final upgrade state is {FinalMigrationState}, database contains {DatabaseState}", FinalMigrationState, CurrentMigrationState ?? ""); return CurrentMigrationState != FinalMigrationState; } diff --git a/src/Umbraco.Core/Scoping/IScope.cs b/src/Umbraco.Core/Scoping/IScope.cs index de4eef0a08..0c38031558 100644 --- a/src/Umbraco.Core/Scoping/IScope.cs +++ b/src/Umbraco.Core/Scoping/IScope.cs @@ -5,6 +5,24 @@ using Umbraco.Core.Persistence; namespace Umbraco.Core.Scoping { + // TODO: This is for backward compat - Merge this in netcore + public interface IScope2 : IScope + { + /// + /// Write-locks some lock objects. + /// + /// The database timeout in milliseconds + /// The lock object identifier. + void WriteLock(TimeSpan timeout, int lockId); + + /// + /// Read-locks some lock objects. + /// + /// The database timeout in milliseconds + /// The lock object identifier. + void ReadLock(TimeSpan timeout, int lockId); + } + /// /// Represents a scope. /// diff --git a/src/Umbraco.Core/Scoping/Scope.cs b/src/Umbraco.Core/Scoping/Scope.cs index 84273e23da..4c058cbdb7 100644 --- a/src/Umbraco.Core/Scoping/Scope.cs +++ b/src/Umbraco.Core/Scoping/Scope.cs @@ -1,11 +1,14 @@ using System; +using System.Collections.Generic; using System.Data; +using System.Text; using Umbraco.Core.Cache; using Umbraco.Core.Composing; using Umbraco.Core.Events; using Umbraco.Core.IO; using Umbraco.Core.Logging; using Umbraco.Core.Persistence; +using Umbraco.Core.Persistence.SqlSyntax; namespace Umbraco.Core.Scoping { @@ -13,7 +16,7 @@ namespace Umbraco.Core.Scoping /// Implements . /// /// Not thread-safe obviously. - internal class Scope : IScope + internal class Scope : IScope2 { private readonly ScopeProvider _scopeProvider; private readonly ILogger _logger; @@ -33,6 +36,12 @@ namespace Umbraco.Core.Scoping private ICompletable _fscope; private IEventDispatcher _eventDispatcher; + private object _dictionaryLocker; + private HashSet _readLocks; + private HashSet _writeLocks; + internal Dictionary> ReadLocks; + internal Dictionary> WriteLocks; + // initializes a new scope private Scope(ScopeProvider scopeProvider, ILogger logger, FileSystems fileSystems, Scope parent, ScopeContext scopeContext, bool detachable, @@ -57,6 +66,8 @@ namespace Umbraco.Core.Scoping Detachable = detachable; + _dictionaryLocker = new object(); + #if DEBUG_SCOPES _scopeProvider.RegisterScope(this); Console.WriteLine("create " + InstanceId.ToString("N").Substring(0, 8)); @@ -310,7 +321,7 @@ namespace Umbraco.Core.Scoping if (completed.HasValue == false || completed.Value == false) { if (LogUncompletedScopes) - _logger.Debug("Uncompleted Child Scope at\r\n {StackTrace}", Environment.StackTrace); + _logger.Debug("Uncompleted Child Scope at\r\n {StackTrace}", Environment.StackTrace); _completed = false; } @@ -332,6 +343,8 @@ namespace Umbraco.Core.Scoping if (this != _scopeProvider.AmbientScope) { + var failedMessage = $"The {nameof(Scope)} {this.InstanceId} being disposed is not the Ambient {nameof(Scope)} {(_scopeProvider.AmbientScope?.InstanceId.ToString() ?? "NULL")}. This typically indicates that a child {nameof(Scope)} was not disposed, or flowed to a child thread that was not awaited, or concurrent threads are accessing the same {nameof(Scope)} (Ambient context) which is not supported. If using Task.Run (or similar) as a fire and forget tasks or to run threads in parallel you must suppress execution context flow with ExecutionContext.SuppressFlow() and ExecutionContext.RestoreFlow()."; + #if DEBUG_SCOPES var ambient = _scopeProvider.AmbientScope; _logger.Debug("Dispose error (" + (ambient == null ? "no" : "other") + " ambient)"); @@ -343,10 +356,24 @@ namespace Umbraco.Core.Scoping + "- ambient ctor ->\r\n" + ambientInfos.CtorStack + "\r\n" + "- dispose ctor ->\r\n" + disposeInfos.CtorStack + "\r\n"); #else - throw new InvalidOperationException("Not the ambient scope."); + throw new InvalidOperationException(failedMessage); #endif } + // Decrement the lock counters on the parent if any. + ClearLocks(InstanceId); + if (ParentScope is null) + { + // We're the parent scope, make sure that locks of all scopes has been cleared + // Since we're only reading we don't have to be in a lock + if (ReadLocks?.Count > 0 || WriteLocks?.Count > 0) + { + var exception = new InvalidOperationException($"All scopes has not been disposed from parent scope: {InstanceId}, see log for more details."); + _logger.Error(exception, GenerateUnclearedScopesLogMessage()); + throw exception; + } + } + var parent = ParentScope; _scopeProvider.AmbientScope = parent; // might be null = this is how scopes are removed from context objects @@ -366,6 +393,42 @@ namespace Umbraco.Core.Scoping GC.SuppressFinalize(this); } + /// + /// Generates a log message with all scopes that hasn't cleared their locks, including how many, and what locks they have requested. + /// + /// Log message. + private string GenerateUnclearedScopesLogMessage() + { + // Dump the dicts into a message for the locks. + StringBuilder builder = new StringBuilder(); + builder.AppendLine($"Lock counters aren't empty, suggesting a scope hasn't been properly disposed, parent id: {InstanceId}"); + WriteLockDictionaryToString(ReadLocks, builder, "read locks"); + WriteLockDictionaryToString(WriteLocks, builder, "write locks"); + return builder.ToString(); + } + + /// + /// Writes a locks dictionary to a for logging purposes. + /// + /// Lock dictionary to report on. + /// String builder to write to. + /// The name to report the dictionary as. + private void WriteLockDictionaryToString(Dictionary> dict, StringBuilder builder, string dictName) + { + if (dict?.Count > 0) + { + builder.AppendLine($"Remaining {dictName}:"); + foreach (var instance in dict) + { + builder.AppendLine($"Scope {instance.Key}"); + foreach (var lockCounter in instance.Value) + { + builder.AppendLine($"\tLock ID: {lockCounter.Key} - times requested: {lockCounter.Value}"); + } + } + } + } + private void DisposeLastScope() { // figure out completed @@ -485,10 +548,210 @@ namespace Umbraco.Core.Scoping private static bool LogUncompletedScopes => (_logUncompletedScopes ?? (_logUncompletedScopes = Current.Configs.CoreDebug().LogUncompletedScopes)).Value; - /// - public void ReadLock(params int[] lockIds) => Database.SqlContext.SqlSyntax.ReadLock(Database, lockIds); + /// + /// Increment the counter of a locks dictionary, either ReadLocks or WriteLocks, + /// for a specific scope instance and lock identifier. Must be called within a lock. + /// + /// Lock ID to increment. + /// Instance ID of the scope requesting the lock. + /// Reference to the dictionary to increment on + private void IncrementLock(int lockId, Guid instanceId, ref Dictionary> locks) + { + // Since we've already checked that we're the parent in the WriteLockInner method, we don't need to check again. + // If it's the very first time a lock has been requested the WriteLocks dict hasn't been instantiated yet. + locks ??= new Dictionary>(); + + // Try and get the dict associated with the scope id. + var locksDictFound = locks.TryGetValue(instanceId, out var locksDict); + if (locksDictFound) + { + locksDict.TryGetValue(lockId, out var value); + locksDict[lockId] = value + 1; + } + else + { + // The scope hasn't requested a lock yet, so we have to create a dict for it. + locks.Add(instanceId, new Dictionary()); + locks[instanceId][lockId] = 1; + } + } + + /// + /// Clears all lock counters for a given scope instance, signalling that the scope has been disposed. + /// + /// Instance ID of the scope to clear. + private void ClearLocks(Guid instanceId) + { + if (ParentScope is not null) + { + ParentScope.ClearLocks(instanceId); + } + else + { + lock (_dictionaryLocker) + { + ReadLocks?.Remove(instanceId); + WriteLocks?.Remove(instanceId); + } + } + } /// - public void WriteLock(params int[] lockIds) => Database.SqlContext.SqlSyntax.WriteLock(Database, lockIds); + public void ReadLock(params int[] lockIds) => ReadLockInner(InstanceId, null, lockIds); + + /// + public void ReadLock(TimeSpan timeout, int lockId) => ReadLockInner(InstanceId, timeout, lockId); + + /// + public void WriteLock(params int[] lockIds) => WriteLockInner(InstanceId, null, lockIds); + + /// + public void WriteLock(TimeSpan timeout, int lockId) => WriteLockInner(InstanceId, timeout, lockId); + + /// + /// Handles acquiring a read lock, will delegate it to the parent if there are any. + /// + /// Instance ID of the requesting scope. + /// Optional database timeout in milliseconds. + /// Array of lock object identifiers. + private void ReadLockInner(Guid instanceId, TimeSpan? timeout = null, params int[] lockIds) + { + if (ParentScope is not null) + { + // If we have a parent we delegate lock creation to parent. + ParentScope.ReadLockInner(instanceId, timeout, lockIds); + } + else + { + // We are the outermost scope, handle the lock request. + LockInner(instanceId, ref ReadLocks, ref _readLocks, ObtainReadLock, ObtainTimeoutReadLock, timeout, lockIds); + } + } + + /// + /// Handles acquiring a write lock with a specified timeout, will delegate it to the parent if there are any. + /// + /// Instance ID of the requesting scope. + /// Optional database timeout in milliseconds. + /// Array of lock object identifiers. + private void WriteLockInner(Guid instanceId, TimeSpan? timeout = null, params int[] lockIds) + { + if (ParentScope is not null) + { + // If we have a parent we delegate lock creation to parent. + ParentScope.WriteLockInner(instanceId, timeout, lockIds); + } + else + { + // We are the outermost scope, handle the lock request. + LockInner(instanceId, ref WriteLocks, ref _writeLocks, ObtainWriteLock, ObtainTimeoutWriteLock, timeout, lockIds); + } + } + + /// + /// Handles acquiring a lock, this should only be called from the outermost scope. + /// + /// Instance ID of the scope requesting the lock. + /// Reference to the applicable locks dictionary (ReadLocks or WriteLocks). + /// Reference to the applicable locks hashset (_readLocks or _writeLocks). + /// Delegate used to request the lock from the database without a timeout. + /// Delegate used to request the lock from the database with a timeout. + /// Optional timeout parameter to specify a timeout. + /// Lock identifiers to lock on. + private void LockInner(Guid instanceId, ref Dictionary> locks, ref HashSet locksSet, + Action obtainLock, Action obtainLockTimeout, TimeSpan? timeout = null, + params int[] lockIds) + { + lock (_dictionaryLocker) + { + locksSet ??= new HashSet(); + foreach (var lockId in lockIds) + { + // Only acquire the lock if we haven't done so yet. + if (!locksSet.Contains(lockId)) + { + IncrementLock(lockId, instanceId, ref locks); + locksSet.Add(lockId); + try + { + if (timeout is null) + { + // We just want an ordinary lock. + obtainLock(lockId); + } + else + { + // We want a lock with a custom timeout + obtainLockTimeout(lockId, timeout.Value); + } + } + catch + { + // Something went wrong and we didn't get the lock + // Since we at this point have determined that we haven't got any lock with an ID of LockID, it's safe to completely remove it instead of decrementing. + locks[instanceId].Remove(lockId); + // It needs to be removed from the HashSet as well, because that's how we determine to acquire a lock. + locksSet.Remove(lockId); + throw; + } + } + else + { + // We already have a lock, but need to update the dictionary for debugging purposes. + IncrementLock(lockId, instanceId, ref locks); + } + } + } + } + + /// + /// Obtains an ordinary read lock. + /// + /// Lock object identifier to lock. + private void ObtainReadLock(int lockId) + { + Database.SqlContext.SqlSyntax.ReadLock(Database, lockId); + } + + /// + /// Obtains a read lock with a custom timeout. + /// + /// Lock object identifier to lock. + /// TimeSpan specifying the timout period. + private void ObtainTimeoutReadLock(int lockId, TimeSpan timeout) + { + var syntax2 = Database.SqlContext.SqlSyntax as ISqlSyntaxProvider2; + if (syntax2 is null) + { + throw new InvalidOperationException($"{Database.SqlContext.SqlSyntax.GetType()} is not of type {typeof(ISqlSyntaxProvider2)}"); + } + + syntax2.ReadLock(Database, timeout, lockId); + } + + /// + /// Obtains an ordinary write lock. + /// + /// Lock object identifier to lock. + private void ObtainWriteLock(int lockId) + { + Database.SqlContext.SqlSyntax.WriteLock(Database, lockId); + } + + /// + /// Obtains a write lock with a custom timeout. + /// + /// Lock object identifier to lock. + /// TimeSpan specifying the timout period. + private void ObtainTimeoutWriteLock(int lockId, TimeSpan timeout) + { + var syntax2 = Database.SqlContext.SqlSyntax as ISqlSyntaxProvider2; + if (syntax2 is null) + { + throw new InvalidOperationException($"{Database.SqlContext.SqlSyntax.GetType()} is not of type {typeof(ISqlSyntaxProvider2)}"); + } + + syntax2.WriteLock(Database, timeout, lockId); + } } } diff --git a/src/Umbraco.Core/Scoping/ScopeProvider.cs b/src/Umbraco.Core/Scoping/ScopeProvider.cs index 3c0fa94327..a1cc128181 100644 --- a/src/Umbraco.Core/Scoping/ScopeProvider.cs +++ b/src/Umbraco.Core/Scoping/ScopeProvider.cs @@ -1,6 +1,7 @@ using System; using System.Collections; using System.Collections.Generic; +using System.ComponentModel; using System.Data; using System.Runtime.Remoting.Messaging; using System.Web; @@ -117,7 +118,7 @@ namespace Umbraco.Core.Scoping } // hard to inject into a static method :( - Current.Logger.Warn("Missed {TypeName} Object {ObjectKey}", typeof(T).Name, objectKey.ToString("N").Substring(0, 8)); + Current.Logger.Warn("Missed {TypeName} Object {ObjectKey}", typeof(T).Name, objectKey.ToString("N").Substring(0, 8)); #if DEBUG_SCOPES //Current.Logger.Debug("At:\r\n" + Head(Environment.StackTrace, 24)); #endif @@ -240,6 +241,9 @@ namespace Umbraco.Core.Scoping var value = GetHttpContextObject(ContextItemKey, false); return value ?? GetCallContextObject(ContextItemKey); } + + [Obsolete("This setter is not used and will be removed in future versions")] + [EditorBrowsable(EditorBrowsableState.Never)] set { // clear both diff --git a/src/Umbraco.Core/Security/BackOfficeUserStore.cs b/src/Umbraco.Core/Security/BackOfficeUserStore.cs index 7df328b5b7..fe1673bca6 100644 --- a/src/Umbraco.Core/Security/BackOfficeUserStore.cs +++ b/src/Umbraco.Core/Security/BackOfficeUserStore.cs @@ -5,6 +5,8 @@ using System.Linq; using System.Threading.Tasks; using System.Web.Security; using Microsoft.AspNet.Identity; +using Umbraco.Core.Cache; +using Umbraco.Core.Composing; using Umbraco.Core.Configuration; using Umbraco.Core.Mapping; using Umbraco.Core.Models; @@ -38,20 +40,26 @@ namespace Umbraco.Core.Security private readonly IExternalLoginService _externalLoginService; private readonly IGlobalSettings _globalSettings; private readonly UmbracoMapper _mapper; + private readonly AppCaches _appCaches; private bool _disposed = false; + [Obsolete("Use the constructor specifying all dependencies")] public BackOfficeUserStore(IUserService userService, IMemberTypeService memberTypeService, IEntityService entityService, IExternalLoginService externalLoginService, IGlobalSettings globalSettings, MembershipProviderBase usersMembershipProvider, UmbracoMapper mapper) + : this(userService, memberTypeService, entityService, externalLoginService, globalSettings, usersMembershipProvider, mapper, Current.AppCaches) { } + + public BackOfficeUserStore(IUserService userService, IMemberTypeService memberTypeService, IEntityService entityService, IExternalLoginService externalLoginService, IGlobalSettings globalSettings, MembershipProviderBase usersMembershipProvider, UmbracoMapper mapper, AppCaches appCaches) { + if (userService == null) throw new ArgumentNullException("userService"); + if (usersMembershipProvider == null) throw new ArgumentNullException("usersMembershipProvider"); + if (externalLoginService == null) throw new ArgumentNullException("externalLoginService"); + _userService = userService; _memberTypeService = memberTypeService; _entityService = entityService; _externalLoginService = externalLoginService; - _globalSettings = globalSettings; - if (userService == null) throw new ArgumentNullException("userService"); - if (usersMembershipProvider == null) throw new ArgumentNullException("usersMembershipProvider"); - if (externalLoginService == null) throw new ArgumentNullException("externalLoginService"); + _globalSettings = globalSettings; _mapper = mapper; - + _appCaches = appCaches; _userService = userService; _externalLoginService = externalLoginService; @@ -775,8 +783,8 @@ namespace Umbraco.Core.Security } //we should re-set the calculated start nodes - identityUser.CalculatedMediaStartNodeIds = user.CalculateMediaStartNodeIds(_entityService); - identityUser.CalculatedContentStartNodeIds = user.CalculateContentStartNodeIds(_entityService); + identityUser.CalculatedMediaStartNodeIds = user.CalculateMediaStartNodeIds(_entityService, _appCaches); + identityUser.CalculatedContentStartNodeIds = user.CalculateContentStartNodeIds(_entityService, _appCaches); //reset all changes identityUser.ResetDirtyProperties(false); diff --git a/src/Umbraco.Core/Security/ContentPermissionsHelper.cs b/src/Umbraco.Core/Security/ContentPermissionsHelper.cs index 1a329fcdcb..8c3a138f6a 100644 --- a/src/Umbraco.Core/Security/ContentPermissionsHelper.cs +++ b/src/Umbraco.Core/Security/ContentPermissionsHelper.cs @@ -4,6 +4,7 @@ using System.Globalization; using System.Linq; using System.Text; using System.Threading.Tasks; +using Umbraco.Core.Cache; using Umbraco.Core.Models; using Umbraco.Core.Models.Entities; using Umbraco.Core.Models.Membership; @@ -25,6 +26,7 @@ namespace Umbraco.Core.Security IUser user, IUserService userService, IEntityService entityService, + AppCaches appCaches, params char[] permissionsToCheck) { if (user == null) throw new ArgumentNullException("user"); @@ -33,7 +35,7 @@ namespace Umbraco.Core.Security if (content == null) return ContentAccess.NotFound; - var hasPathAccess = user.HasPathAccess(content, entityService); + var hasPathAccess = user.HasPathAccess(content, entityService, appCaches); if (hasPathAccess == false) return ContentAccess.Denied; @@ -52,6 +54,7 @@ namespace Umbraco.Core.Security IUser user, IUserService userService, IEntityService entityService, + AppCaches appCaches, params char[] permissionsToCheck) { if (user == null) throw new ArgumentNullException("user"); @@ -60,7 +63,7 @@ namespace Umbraco.Core.Security if (entity == null) return ContentAccess.NotFound; - var hasPathAccess = user.HasContentPathAccess(entity, entityService); + var hasPathAccess = user.HasContentPathAccess(entity, entityService, appCaches); if (hasPathAccess == false) return ContentAccess.Denied; @@ -89,6 +92,7 @@ namespace Umbraco.Core.Security IUser user, IUserService userService, IEntityService entityService, + AppCaches appCaches, out IUmbracoEntity entity, params char[] permissionsToCheck) { @@ -100,16 +104,16 @@ namespace Umbraco.Core.Security entity = null; if (nodeId == Constants.System.Root) - hasPathAccess = user.HasContentRootAccess(entityService); + hasPathAccess = user.HasContentRootAccess(entityService, appCaches); else if (nodeId == Constants.System.RecycleBinContent) - hasPathAccess = user.HasContentBinAccess(entityService); + hasPathAccess = user.HasContentBinAccess(entityService, appCaches); if (hasPathAccess.HasValue) return hasPathAccess.Value ? ContentAccess.Granted : ContentAccess.Denied; entity = entityService.Get(nodeId, UmbracoObjectTypes.Document); if (entity == null) return ContentAccess.NotFound; - hasPathAccess = user.HasContentPathAccess(entity, entityService); + hasPathAccess = user.HasContentPathAccess(entity, entityService, appCaches); if (hasPathAccess == false) return ContentAccess.Denied; @@ -140,6 +144,7 @@ namespace Umbraco.Core.Security IUserService userService, IContentService contentService, IEntityService entityService, + AppCaches appCaches, out IContent contentItem, params char[] permissionsToCheck) { @@ -152,16 +157,16 @@ namespace Umbraco.Core.Security contentItem = null; if (nodeId == Constants.System.Root) - hasPathAccess = user.HasContentRootAccess(entityService); + hasPathAccess = user.HasContentRootAccess(entityService, appCaches); else if (nodeId == Constants.System.RecycleBinContent) - hasPathAccess = user.HasContentBinAccess(entityService); + hasPathAccess = user.HasContentBinAccess(entityService, appCaches); if (hasPathAccess.HasValue) return hasPathAccess.Value ? ContentAccess.Granted : ContentAccess.Denied; contentItem = contentService.GetById(nodeId); if (contentItem == null) return ContentAccess.NotFound; - hasPathAccess = user.HasPathAccess(contentItem, entityService); + hasPathAccess = user.HasPathAccess(contentItem, entityService, appCaches); if (hasPathAccess == false) return ContentAccess.Denied; diff --git a/src/Umbraco.Core/Serialization/NoTypeConverterJsonConverter.cs b/src/Umbraco.Core/Serialization/NoTypeConverterJsonConverter.cs index b06ee870de..ab64d5b368 100644 --- a/src/Umbraco.Core/Serialization/NoTypeConverterJsonConverter.cs +++ b/src/Umbraco.Core/Serialization/NoTypeConverterJsonConverter.cs @@ -19,6 +19,7 @@ namespace Umbraco.Core.Serialization internal class NoTypeConverterJsonConverter : JsonConverter { static readonly IContractResolver resolver = new NoTypeConverterContractResolver(); + private static readonly JsonSerializerSettings JsonSerializerSettings = new JsonSerializerSettings { ContractResolver = resolver }; private class NoTypeConverterContractResolver : DefaultContractResolver { @@ -41,12 +42,12 @@ namespace Umbraco.Core.Serialization public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) { - return JsonSerializer.CreateDefault(new JsonSerializerSettings { ContractResolver = resolver }).Deserialize(reader, objectType); + return JsonSerializer.CreateDefault(JsonSerializerSettings).Deserialize(reader, objectType); } public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) { - JsonSerializer.CreateDefault(new JsonSerializerSettings { ContractResolver = resolver }).Serialize(writer, value); + JsonSerializer.CreateDefault(JsonSerializerSettings).Serialize(writer, value); } } } diff --git a/src/Umbraco.Core/Services/Changes/TreeChange.cs b/src/Umbraco.Core/Services/Changes/TreeChange.cs index 81c9b67c3f..605cde87a2 100644 --- a/src/Umbraco.Core/Services/Changes/TreeChange.cs +++ b/src/Umbraco.Core/Services/Changes/TreeChange.cs @@ -3,7 +3,7 @@ using System.Linq; namespace Umbraco.Core.Services.Changes { - internal class TreeChange + public class TreeChange { public TreeChange(TItem changedItem, TreeChangeTypes changeTypes) { diff --git a/src/Umbraco.Core/Services/IIconService.cs b/src/Umbraco.Core/Services/IIconService.cs index 963edb22a5..1e1ae38002 100644 --- a/src/Umbraco.Core/Services/IIconService.cs +++ b/src/Umbraco.Core/Services/IIconService.cs @@ -1,4 +1,6 @@ +using System; using System.Collections.Generic; +using System.ComponentModel; using Umbraco.Core.Models; namespace Umbraco.Core.Services @@ -6,7 +8,7 @@ namespace Umbraco.Core.Services public interface IIconService { /// - /// Gets an IconModel containing the icon name and SvgString according to an icon name found at the global icons path + /// Gets the svg string for the icon name found at the global icons path /// /// /// @@ -15,7 +17,15 @@ namespace Umbraco.Core.Services /// /// Gets a list of all svg icons found at at the global icons path. /// - /// + /// A list of + [Obsolete("This method should not be used - use GetIcons instead")] + [EditorBrowsable(EditorBrowsableState.Never)] IList GetAllIcons(); + + /// + /// Gets a list of all svg icons found at at the global icons path. + /// + /// + IReadOnlyDictionary GetIcons(); } } diff --git a/src/Umbraco.Core/Services/Implement/ContentService.cs b/src/Umbraco.Core/Services/Implement/ContentService.cs index a809b83f23..e5363d0e2b 100644 --- a/src/Umbraco.Core/Services/Implement/ContentService.cs +++ b/src/Umbraco.Core/Services/Implement/ContentService.cs @@ -1415,7 +1415,7 @@ namespace Umbraco.Core.Services.Implement var result = CommitDocumentChangesInternal(scope, d, saveEventArgs, allLangs.Value, d.WriterId); if (result.Success == false) - Logger.Error(null, "Failed to publish document id={DocumentId}, reason={Reason}.", d.Id, result.Result); + Logger.Error(null, "Failed to publish document id={DocumentId}, reason={Reason}.", d.Id, result.Result); results.Add(result); } @@ -1425,7 +1425,7 @@ namespace Umbraco.Core.Services.Implement d.ContentSchedule.Clear(ContentScheduleAction.Expire, date); var result = Unpublish(d, userId: d.WriterId); if (result.Success == false) - Logger.Error(null, "Failed to unpublish document id={DocumentId}, reason={Reason}.", d.Id, result.Result); + Logger.Error(null, "Failed to unpublish document id={DocumentId}, reason={Reason}.", d.Id, result.Result); results.Add(result); } } @@ -1479,7 +1479,7 @@ namespace Umbraco.Core.Services.Implement var impact = CultureImpact.Explicit(culture, IsDefaultCulture(allLangs.Value, culture)); var tryPublish = d.PublishCulture(impact) && _propertyValidationService.Value.IsPropertyDataValid(d, out invalidProperties, impact); if (invalidProperties != null && invalidProperties.Length > 0) - Logger.Warn("Scheduled publishing will fail for document {DocumentId} and culture {Culture} because of invalid properties {InvalidProperties}", + Logger.Warn("Scheduled publishing will fail for document {DocumentId} and culture {Culture} because of invalid properties {InvalidProperties}", d.Id, culture, string.Join(",", invalidProperties.Select(x => x.Alias))); publishing &= tryPublish; //set the culture to be published @@ -1496,7 +1496,7 @@ namespace Umbraco.Core.Services.Implement result = CommitDocumentChangesInternal(scope, d, saveEventArgs, allLangs.Value, d.WriterId); if (result.Success == false) - Logger.Error(null, "Failed to publish document id={DocumentId}, reason={Reason}.", d.Id, result.Result); + Logger.Error(null, "Failed to publish document id={DocumentId}, reason={Reason}.", d.Id, result.Result); results.Add(result); } @@ -1510,7 +1510,7 @@ namespace Umbraco.Core.Services.Implement : SaveAndPublish(d, userId: d.WriterId); if (result.Success == false) - Logger.Error(null, "Failed to publish document id={DocumentId}, reason={Reason}.", d.Id, result.Result); + Logger.Error(null, "Failed to publish document id={DocumentId}, reason={Reason}.", d.Id, result.Result); results.Add(result); } @@ -2606,7 +2606,7 @@ namespace Umbraco.Core.Services.Implement /// /// Occurs after change. /// - internal static event TypedEventHandler.EventArgs> TreeChanged; + public static event TypedEventHandler.EventArgs> TreeChanged; /// /// Occurs after a blueprint has been saved. @@ -2640,7 +2640,7 @@ namespace Umbraco.Core.Services.Implement // raise Publishing event if (scope.Events.DispatchCancelable(Publishing, this, savingEventArgs.ToContentPublishingEventArgs())) { - Logger.Info("Document {ContentName} (id={ContentId}) cannot be published: {Reason}", content.Name, content.Id, "publishing was cancelled"); + Logger.Info("Document {ContentName} (id={ContentId}) cannot be published: {Reason}", content.Name, content.Id, "publishing was cancelled"); return new PublishResult(PublishResultType.FailedPublishCancelledByEvent, evtMsgs, content); } @@ -2692,7 +2692,7 @@ namespace Umbraco.Core.Services.Implement // either because it is 'publishing' or because it already has a published version if (content.PublishedState != PublishedState.Publishing && content.PublishedVersionId == 0) { - Logger.Info("Document {ContentName} (id={ContentId}) cannot be published: {Reason}", content.Name, content.Id, "document does not have published values"); + Logger.Info("Document {ContentName} (id={ContentId}) cannot be published: {Reason}", content.Name, content.Id, "document does not have published values"); return new PublishResult(PublishResultType.FailedPublishNothingToPublish, evtMsgs, content); } @@ -2705,20 +2705,20 @@ namespace Umbraco.Core.Services.Implement { case ContentStatus.Expired: if (!variesByCulture) - Logger.Info("Document {ContentName} (id={ContentId}) cannot be published: {Reason}", content.Name, content.Id, "document has expired"); + Logger.Info("Document {ContentName} (id={ContentId}) cannot be published: {Reason}", content.Name, content.Id, "document has expired"); else Logger.Info("Document {ContentName} (id={ContentId}) culture {Culture} cannot be published: {Reason}", content.Name, content.Id, culture, "document culture has expired"); return new PublishResult(!variesByCulture ? PublishResultType.FailedPublishHasExpired : PublishResultType.FailedPublishCultureHasExpired, evtMsgs, content); case ContentStatus.AwaitingRelease: if (!variesByCulture) - Logger.Info("Document {ContentName} (id={ContentId}) cannot be published: {Reason}", content.Name, content.Id, "document is awaiting release"); + Logger.Info("Document {ContentName} (id={ContentId}) cannot be published: {Reason}", content.Name, content.Id, "document is awaiting release"); else Logger.Info("Document {ContentName} (id={ContentId}) culture {Culture} cannot be published: {Reason}", content.Name, content.Id, culture, "document is culture awaiting release"); return new PublishResult(!variesByCulture ? PublishResultType.FailedPublishAwaitingRelease : PublishResultType.FailedPublishCultureAwaitingRelease, evtMsgs, content); case ContentStatus.Trashed: - Logger.Info("Document {ContentName} (id={ContentId}) cannot be published: {Reason}", content.Name, content.Id, "document is trashed"); + Logger.Info("Document {ContentName} (id={ContentId}) cannot be published: {Reason}", content.Name, content.Id, "document is trashed"); return new PublishResult(PublishResultType.FailedPublishIsTrashed, evtMsgs, content); } } @@ -2731,7 +2731,7 @@ namespace Umbraco.Core.Services.Implement var pathIsOk = content.ParentId == Constants.System.Root || IsPathPublished(GetParent(content)); if (!pathIsOk) { - Logger.Info("Document {ContentName} (id={ContentId}) cannot be published: {Reason}", content.Name, content.Id, "parent is not published"); + Logger.Info("Document {ContentName} (id={ContentId}) cannot be published: {Reason}", content.Name, content.Id, "parent is not published"); return new PublishResult(PublishResultType.FailedPublishPathNotPublished, evtMsgs, content); } } @@ -2768,11 +2768,11 @@ namespace Umbraco.Core.Services.Implement return new PublishResult(PublishResultType.FailedPublishNothingToPublish, evtMsgs, content); if (culturesUnpublishing.Count > 0) - Logger.Info("Document {ContentName} (id={ContentId}) cultures: {Cultures} have been unpublished.", + Logger.Info("Document {ContentName} (id={ContentId}) cultures: {Cultures} have been unpublished.", content.Name, content.Id, string.Join(",", culturesUnpublishing)); if (culturesPublishing.Count > 0) - Logger.Info("Document {ContentName} (id={ContentId}) cultures: {Cultures} have been published.", + Logger.Info("Document {ContentName} (id={ContentId}) cultures: {Cultures} have been published.", content.Name, content.Id, string.Join(",", culturesPublishing)); if (culturesUnpublishing.Count > 0 && culturesPublishing.Count > 0) @@ -2784,7 +2784,7 @@ namespace Umbraco.Core.Services.Implement return new PublishResult(PublishResultType.SuccessPublishCulture, evtMsgs, content); } - Logger.Info("Document {ContentName} (id={ContentId}) has been published.", content.Name, content.Id); + Logger.Info("Document {ContentName} (id={ContentId}) has been published.", content.Name, content.Id); return new PublishResult(evtMsgs, content); } @@ -2800,7 +2800,7 @@ namespace Umbraco.Core.Services.Implement // raise Unpublishing event if (scope.Events.DispatchCancelable(Unpublishing, this, new PublishEventArgs(content, evtMsgs))) { - Logger.Info("Document {ContentName} (id={ContentId}) cannot be unpublished: unpublishing was cancelled.", content.Name, content.Id); + Logger.Info("Document {ContentName} (id={ContentId}) cannot be unpublished: unpublishing was cancelled.", content.Name, content.Id); return new PublishResult(PublishResultType.FailedUnpublishCancelledByEvent, evtMsgs, content); } @@ -2832,12 +2832,12 @@ namespace Umbraco.Core.Services.Implement foreach (var p in pastReleases) content.ContentSchedule.Remove(p); if (pastReleases.Count > 0) - Logger.Info("Document {ContentName} (id={ContentId}) had its release date removed, because it was unpublished.", content.Name, content.Id); + Logger.Info("Document {ContentName} (id={ContentId}) had its release date removed, because it was unpublished.", content.Name, content.Id); // change state to unpublishing content.PublishedState = PublishedState.Unpublishing; - Logger.Info("Document {ContentName} (id={ContentId}) has been unpublished.", content.Name, content.Id); + Logger.Info("Document {ContentName} (id={ContentId}) has been unpublished.", content.Name, content.Id); return attempt; } @@ -3169,7 +3169,7 @@ namespace Umbraco.Core.Services.Implement if (rollbackSaveResult.Success == false) { //Log the error/warning - Logger.Error("User '{UserId}' was unable to rollback content '{ContentId}' to version '{VersionId}'", userId, id, versionId); + Logger.Error("User '{UserId}' was unable to rollback content '{ContentId}' to version '{VersionId}'", userId, id, versionId); } else { @@ -3178,7 +3178,7 @@ namespace Umbraco.Core.Services.Implement scope.Events.Dispatch(RolledBack, this, rollbackEventArgs); //Logging & Audit message - Logger.Info("User '{UserId}' rolled back content '{ContentId}' to version '{VersionId}'", userId, id, versionId); + Logger.Info("User '{UserId}' rolled back content '{ContentId}' to version '{VersionId}'", userId, id, versionId); Audit(AuditType.RollBack, userId, id, $"Content '{content.Name}' was rolled back to version '{versionId}'"); } diff --git a/src/Umbraco.Core/Services/Implement/EntityXmlSerializer.cs b/src/Umbraco.Core/Services/Implement/EntityXmlSerializer.cs index 4d7694d1b8..8cfcaa1182 100644 --- a/src/Umbraco.Core/Services/Implement/EntityXmlSerializer.cs +++ b/src/Umbraco.Core/Services/Implement/EntityXmlSerializer.cs @@ -77,7 +77,7 @@ namespace Umbraco.Core.Services.Implement var children = _contentService.GetPagedChildren(content.Id, page++, pageSize, out total); SerializeChildren(children, xml, published); } - + } return xml; @@ -488,7 +488,7 @@ namespace Umbraco.Core.Services.Implement new XElement("Alias", propertyType.Alias), new XElement("Key", propertyType.Key), new XElement("Type", propertyType.PropertyEditorAlias), - new XElement("Definition", definition.Key), + new XElement("Definition", definition.Key), new XElement("Tab", propertyGroup == null ? "" : propertyGroup.Name), new XElement("SortOrder", propertyType.SortOrder), new XElement("Mandatory", propertyType.Mandatory.ToString()), @@ -553,6 +553,13 @@ namespace Umbraco.Core.Services.Implement new XAttribute("path", contentBase.Path), new XAttribute("isDoc", "")); + + // Add culture specific node names + foreach (var culture in contentBase.AvailableCultures) + { + xml.Add(new XAttribute("nodeName-" + culture, contentBase.GetCultureName(culture))); + } + foreach (var property in contentBase.Properties) xml.Add(SerializeProperty(property, published)); diff --git a/src/Umbraco.Core/Services/Implement/LocalizedTextService.cs b/src/Umbraco.Core/Services/Implement/LocalizedTextService.cs index f5a85dda7e..1d9da91bf8 100644 --- a/src/Umbraco.Core/Services/Implement/LocalizedTextService.cs +++ b/src/Umbraco.Core/Services/Implement/LocalizedTextService.cs @@ -251,7 +251,7 @@ namespace Umbraco.Core.Services.Implement { if (_dictionarySource.ContainsKey(culture) == false) { - _logger.Warn("The culture specified {Culture} was not found in any configured sources for this service", culture); + _logger.Warn("The culture specified {Culture} was not found in any configured sources for this service", culture); return "[" + key + "]"; } diff --git a/src/Umbraco.Core/Services/Implement/LocalizedTextServiceFileSources.cs b/src/Umbraco.Core/Services/Implement/LocalizedTextServiceFileSources.cs index 86913071fd..e25fbed509 100644 --- a/src/Umbraco.Core/Services/Implement/LocalizedTextServiceFileSources.cs +++ b/src/Umbraco.Core/Services/Implement/LocalizedTextServiceFileSources.cs @@ -88,7 +88,7 @@ namespace Umbraco.Core.Services.Implement } catch (CultureNotFoundException) { - Current.Logger.Warn("The culture {CultureValue} found in the file {CultureFile} is not a valid culture", cultureVal, fileInfo.FullName); + Current.Logger.Warn("The culture {CultureValue} found in the file {CultureFile} is not a valid culture", cultureVal, fileInfo.FullName); //If the culture in the file is invalid, we'll just hope the file name is a valid culture below, otherwise // an exception will be thrown. } @@ -125,7 +125,7 @@ namespace Umbraco.Core.Services.Implement if (fileSourceFolder.Exists == false) { - Current.Logger.Warn("The folder does not exist: {FileSourceFolder}, therefore no sources will be discovered", fileSourceFolder.FullName); + Current.Logger.Warn("The folder does not exist: {FileSourceFolder}, therefore no sources will be discovered", fileSourceFolder.FullName); } else { @@ -203,7 +203,7 @@ namespace Umbraco.Core.Services.Implement } catch (Exception ex) { - _logger.Error(ex, "Could not load file into XML {File}", supplementaryFile.File.FullName); + _logger.Error(ex, "Could not load file into XML {File}", supplementaryFile.File.FullName); continue; } diff --git a/src/Umbraco.Core/Services/Implement/MediaService.cs b/src/Umbraco.Core/Services/Implement/MediaService.cs index ac9c83458d..ec84c0738e 100644 --- a/src/Umbraco.Core/Services/Implement/MediaService.cs +++ b/src/Umbraco.Core/Services/Implement/MediaService.cs @@ -1279,7 +1279,7 @@ namespace Umbraco.Core.Services.Implement /// /// Occurs after change. /// - internal static event TypedEventHandler.EventArgs> TreeChanged; + public static event TypedEventHandler.EventArgs> TreeChanged; #endregion diff --git a/src/Umbraco.Core/Services/Implement/NotificationService.cs b/src/Umbraco.Core/Services/Implement/NotificationService.cs index 3c5f91e932..b6e42520f0 100644 --- a/src/Umbraco.Core/Services/Implement/NotificationService.cs +++ b/src/Umbraco.Core/Services/Implement/NotificationService.cs @@ -517,7 +517,7 @@ namespace Umbraco.Core.Services.Implement try { if (Sendmail != null) Sendmail(s, request.Mail, _logger); else s.Send(request.Mail); - _logger.Debug("Notification '{Action}' sent to {Username} ({Email})", request.Action, request.UserName, request.Email); + _logger.Debug("Notification '{Action}' sent to {Username} ({Email})", request.Action, request.UserName, request.Email); } catch (Exception ex) { diff --git a/src/Umbraco.Core/Services/Implement/PublicAccessService.cs b/src/Umbraco.Core/Services/Implement/PublicAccessService.cs index 4e3cd96012..fa9f2c55f1 100644 --- a/src/Umbraco.Core/Services/Implement/PublicAccessService.cs +++ b/src/Umbraco.Core/Services/Implement/PublicAccessService.cs @@ -62,12 +62,10 @@ namespace Umbraco.Core.Services.Implement //start with the deepest id ids.Reverse(); - using (var scope = ScopeProvider.CreateScope()) + using (var scope = ScopeProvider.CreateScope(autoComplete: true)) { //This will retrieve from cache! - var entries = _publicAccessRepository.GetMany().ToArray(); - - scope.Complete(); + var entries = _publicAccessRepository.GetMany().ToList(); foreach (var id in ids) { var found = entries.FirstOrDefault(x => x.ProtectedNodeId == id); diff --git a/src/Umbraco.Core/StringExtensions.cs b/src/Umbraco.Core/StringExtensions.cs index 80ef81f36d..0e16c2c852 100644 --- a/src/Umbraco.Core/StringExtensions.cs +++ b/src/Umbraco.Core/StringExtensions.cs @@ -22,7 +22,7 @@ namespace Umbraco.Core /// public static class StringExtensions { - + private const char DefaultEscapedStringEscapeChar = '\\'; private static readonly char[] ToCSharpHexDigitLower = "0123456789abcdef".ToCharArray(); private static readonly char[] ToCSharpEscapeChars; @@ -1490,5 +1490,44 @@ namespace Umbraco.Core /// public static string NullOrWhiteSpaceAsNull(this string text) => string.IsNullOrWhiteSpace(text) ? null : text; + + /// + /// Splits a string with an escape character that allows for the split character to exist in a string + /// + /// The string to split + /// The character to split on + /// The character which can be used to escape the character to split on + /// The string split into substrings delimited by the split character + public static IEnumerable EscapedSplit(this string value, char splitChar, char escapeChar = DefaultEscapedStringEscapeChar) + { + if (value == null) yield break; + + var sb = new StringBuilder(value.Length); + var escaped = false; + + foreach (var chr in value.ToCharArray()) + { + if (escaped) + { + escaped = false; + sb.Append(chr); + } + else if (chr == splitChar) + { + yield return sb.ToString(); + sb.Clear(); + } + else if (chr == escapeChar) + { + escaped = true; + } + else + { + sb.Append(chr); + } + } + + yield return sb.ToString(); + } } } diff --git a/src/Umbraco.Core/Sync/DatabaseServerMessenger.cs b/src/Umbraco.Core/Sync/DatabaseServerMessenger.cs index 7442169b44..49b0d23862 100644 --- a/src/Umbraco.Core/Sync/DatabaseServerMessenger.cs +++ b/src/Umbraco.Core/Sync/DatabaseServerMessenger.cs @@ -192,7 +192,7 @@ namespace Umbraco.Core.Sync if (count > Options.MaxProcessingInstructionCount) { //too many instructions, proceed to cold boot - Logger.Warn( + Logger.Warn( "The instruction count ({InstructionCount}) exceeds the specified MaxProcessingInstructionCount ({MaxProcessingInstructionCount})." + " The server will skip existing instructions, rebuild its caches and indexes entirely, adjust its last synced Id" + " to the latest found in the database and maintain cache updates based on that Id.", @@ -352,7 +352,7 @@ namespace Umbraco.Core.Sync } catch (JsonException ex) { - Logger.Error(ex, "Failed to deserialize instructions ({DtoId}: '{DtoInstructions}').", + Logger.Error(ex, "Failed to deserialize instructions ({DtoId}: '{DtoInstructions}').", dto.Id, dto.Instructions); @@ -410,7 +410,7 @@ namespace Umbraco.Core.Sync //} catch (Exception ex) { - Logger.Error( + Logger.Error ( ex, "DISTRIBUTED CACHE IS NOT UPDATED. Failed to execute instructions ({DtoId}: '{DtoInstructions}'). Instruction is being skipped/ignored", dto.Id, diff --git a/src/Umbraco.Core/Sync/ServerMessengerBase.cs b/src/Umbraco.Core/Sync/ServerMessengerBase.cs index bbf00c3a6b..1b13b49434 100644 --- a/src/Umbraco.Core/Sync/ServerMessengerBase.cs +++ b/src/Umbraco.Core/Sync/ServerMessengerBase.cs @@ -157,7 +157,7 @@ namespace Umbraco.Core.Sync { if (refresher == null) throw new ArgumentNullException(nameof(refresher)); - Current.Logger.Debug("Invoking refresher {RefresherType} on local server for message type RefreshByPayload", refresher.GetType()); + Current.Logger.Debug("Invoking refresher {RefresherType} on local server for message type RefreshByPayload", refresher.GetType()); var payloadRefresher = refresher as IPayloadCacheRefresher; if (payloadRefresher == null) @@ -179,7 +179,7 @@ namespace Umbraco.Core.Sync { if (refresher == null) throw new ArgumentNullException(nameof(refresher)); - Current.Logger.Debug("Invoking refresher {RefresherType} on local server for message type {MessageType}", refresher.GetType(), messageType); + Current.Logger.Debug("Invoking refresher {RefresherType} on local server for message type {MessageType}", refresher.GetType(), messageType); switch (messageType) { @@ -240,7 +240,7 @@ namespace Umbraco.Core.Sync { if (refresher == null) throw new ArgumentNullException(nameof(refresher)); - Current.Logger.Debug("Invoking refresher {RefresherType} on local server for message type {MessageType}", refresher.GetType(), messageType); + Current.Logger.Debug("Invoking refresher {RefresherType} on local server for message type {MessageType}", refresher.GetType(), messageType); var typedRefresher = refresher as ICacheRefresher; diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index 080df560e4..2ea5292d73 100755 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -131,8 +131,11 @@ + + + + - @@ -157,6 +160,7 @@ + @@ -257,7 +261,6 @@ - diff --git a/src/Umbraco.Core/UriExtensions.cs b/src/Umbraco.Core/UriExtensions.cs index 60d9cd6ead..96e24e89db 100644 --- a/src/Umbraco.Core/UriExtensions.cs +++ b/src/Umbraco.Core/UriExtensions.cs @@ -4,6 +4,7 @@ using System.Linq; using Umbraco.Core.Composing; using Umbraco.Core.Configuration; using Umbraco.Core.IO; +using Umbraco.Core.Logging; namespace Umbraco.Core { @@ -154,7 +155,7 @@ namespace Umbraco.Core } catch (ArgumentException) { - Current.Logger.Debug(typeof(UriExtensions), "Failed to determine if request was client side (invalid chars in path \"{Path}\"?)", url.LocalPath); + Current.Logger.Debug(typeof(UriExtensions), "Failed to determine if request was client side (invalid chars in path \"{Path}\"?)", url.LocalPath); return false; } } diff --git a/src/Umbraco.Examine/ContentValueSetValidator.cs b/src/Umbraco.Examine/ContentValueSetValidator.cs index 9555566c53..f702e8197d 100644 --- a/src/Umbraco.Examine/ContentValueSetValidator.cs +++ b/src/Umbraco.Examine/ContentValueSetValidator.cs @@ -4,7 +4,9 @@ using System.Linq; using Examine; using Examine.LuceneEngine.Providers; using Umbraco.Core; +using Umbraco.Core.Composing; using Umbraco.Core.Models; +using Umbraco.Core.Scoping; using Umbraco.Core.Services; namespace Umbraco.Examine @@ -15,9 +17,9 @@ namespace Umbraco.Examine public class ContentValueSetValidator : ValueSetValidator, IContentValueSetValidator { private readonly IPublicAccessService _publicAccessService; - + private readonly IScopeProvider _scopeProvider; private const string PathKey = "path"; - private static readonly IEnumerable ValidCategories = new[] {IndexTypes.Content, IndexTypes.Media}; + private static readonly IEnumerable ValidCategories = new[] { IndexTypes.Content, IndexTypes.Media }; protected override IEnumerable ValidIndexCategories => ValidCategories; public bool PublishedValuesOnly { get; } @@ -53,25 +55,38 @@ namespace Umbraco.Examine public bool ValidateProtectedContent(string path, string category) { - if (category == IndexTypes.Content - && !SupportProtectedContent - //if the service is null we can't look this up so we'll return false - && (_publicAccessService == null || _publicAccessService.IsProtected(path))) + if (category == IndexTypes.Content && !SupportProtectedContent) { - return false; + //if the service is null we can't look this up so we'll return false + if (_publicAccessService == null || _scopeProvider == null) + { + return false; + } + + // explicit scope since we may be in a background thread + using (_scopeProvider.CreateScope(autoComplete: true)) + { + if (_publicAccessService.IsProtected(path)) + { + return false; + } + } } return true; } + // used for tests public ContentValueSetValidator(bool publishedValuesOnly, int? parentId = null, IEnumerable includeItemTypes = null, IEnumerable excludeItemTypes = null) - : this(publishedValuesOnly, true, null, parentId, includeItemTypes, excludeItemTypes) + : this(publishedValuesOnly, true, null, null, parentId, includeItemTypes, excludeItemTypes) { } public ContentValueSetValidator(bool publishedValuesOnly, bool supportProtectedContent, - IPublicAccessService publicAccessService, int? parentId = null, + IPublicAccessService publicAccessService, + IScopeProvider scopeProvider, + int? parentId = null, IEnumerable includeItemTypes = null, IEnumerable excludeItemTypes = null) : base(includeItemTypes, excludeItemTypes, null, null) { @@ -79,6 +94,16 @@ namespace Umbraco.Examine SupportProtectedContent = supportProtectedContent; ParentId = parentId; _publicAccessService = publicAccessService; + _scopeProvider = scopeProvider; + } + + [Obsolete("Use the ctor with all parameters instead")] + public ContentValueSetValidator(bool publishedValuesOnly, bool supportProtectedContent, + IPublicAccessService publicAccessService, int? parentId = null, + IEnumerable includeItemTypes = null, IEnumerable excludeItemTypes = null) + : this(publishedValuesOnly, supportProtectedContent, publicAccessService, Current.ScopeProvider, + parentId, includeItemTypes, excludeItemTypes) + { } public override ValueSetValidationResult Validate(ValueSet valueSet) @@ -103,7 +128,7 @@ namespace Umbraco.Examine && variesByCulture.Count > 0 && variesByCulture[0].Equals("y")) { //so this valueset is for a content that varies by culture, now check for non-published cultures and remove those values - foreach(var publishField in valueSet.Values.Where(x => x.Key.StartsWith($"{UmbracoExamineIndex.PublishedFieldName}_")).ToList()) + foreach (var publishField in valueSet.Values.Where(x => x.Key.StartsWith($"{UmbracoExamineIndex.PublishedFieldName}_")).ToList()) { if (publishField.Value.Count <= 0 || !publishField.Value[0].Equals("y")) { @@ -134,7 +159,7 @@ namespace Umbraco.Examine || !ValidateProtectedContent(path, valueSet.Category)) return ValueSetValidationResult.Filtered; - return isFiltered ? ValueSetValidationResult.Filtered: ValueSetValidationResult.Valid; + return isFiltered ? ValueSetValidationResult.Filtered : ValueSetValidationResult.Valid; } } } diff --git a/src/Umbraco.Examine/IndexRebuilder.cs b/src/Umbraco.Examine/IndexRebuilder.cs index b14ff25c57..1f953c8cbd 100644 --- a/src/Umbraco.Examine/IndexRebuilder.cs +++ b/src/Umbraco.Examine/IndexRebuilder.cs @@ -69,7 +69,7 @@ namespace Umbraco.Examine } catch (Exception e) { - _logger.Error(e, "Index populating failed for populator {Populator}", populator.GetType()); + _logger.Error(e, "Index populating failed for populator {Populator}", populator.GetType()); } } } diff --git a/src/Umbraco.Examine/Umbraco.Examine.csproj b/src/Umbraco.Examine/Umbraco.Examine.csproj index 0e0ee62139..517edf354c 100644 --- a/src/Umbraco.Examine/Umbraco.Examine.csproj +++ b/src/Umbraco.Examine/Umbraco.Examine.csproj @@ -49,7 +49,7 @@ - + 1.0.0-beta2-19324-01 runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/Umbraco.Examine/UmbracoContentIndex.cs b/src/Umbraco.Examine/UmbracoContentIndex.cs index 88033b1407..2da35201b2 100644 --- a/src/Umbraco.Examine/UmbracoContentIndex.cs +++ b/src/Umbraco.Examine/UmbracoContentIndex.cs @@ -133,7 +133,7 @@ namespace Umbraco.Examine var filtered = c.NativeQuery(rawQuery); var results = filtered.Execute(); - ProfilingLogger.Debug(GetType(), "DeleteFromIndex with query: {Query} (found {TotalItems} results)", rawQuery, results.TotalItemCount); + ProfilingLogger.Debug(GetType(), "DeleteFromIndex with query: {Query} (found {TotalItems} results)", rawQuery, results.TotalItemCount); //need to queue a delete item for each one found QueueIndexOperation(results.Select(r => new IndexOperation(new ValueSet(r.Id), IndexOperationType.Delete))); diff --git a/src/Umbraco.Examine/UmbracoExamineIndex.cs b/src/Umbraco.Examine/UmbracoExamineIndex.cs index e1dd77b994..511d78db92 100644 --- a/src/Umbraco.Examine/UmbracoExamineIndex.cs +++ b/src/Umbraco.Examine/UmbracoExamineIndex.cs @@ -24,8 +24,8 @@ namespace Umbraco.Examine // note // wrapping all operations that end up calling base.SafelyProcessQueueItems in a safe call // context because they will fork a thread/task/whatever which should *not* capture our - // call context (and the database it can contain)! ideally we should be able to override - // SafelyProcessQueueItems but that's not possible in the current version of Examine. + // call context (and the database it can contain)! + // TODO: FIX Examine to not flow the ExecutionContext so callers don't need to worry about this! /// /// Used to store the path of a content object @@ -99,6 +99,9 @@ namespace Umbraco.Examine { if (CanInitialize()) { + // Use SafeCallContext to prevent the current CallContext flow to child + // tasks executed in the base class so we don't leak Scopes. + // TODO: See notes at the top of this class using (new SafeCallContext()) { base.PerformDeleteFromIndex(itemIds, onComplete); @@ -106,6 +109,20 @@ namespace Umbraco.Examine } } + protected override void PerformIndexItems(IEnumerable values, Action onComplete) + { + if (CanInitialize()) + { + // Use SafeCallContext to prevent the current CallContext flow to child + // tasks executed in the base class so we don't leak Scopes. + // TODO: See notes at the top of this class + using (new SafeCallContext()) + { + base.PerformIndexItems(values, onComplete); + } + } + } + /// /// Returns true if the Umbraco application is in a state that we can initialize the examine indexes /// @@ -159,7 +176,7 @@ namespace Umbraco.Examine /// protected override void AddDocument(Document doc, ValueSet valueSet, IndexWriter writer) { - ProfilingLogger.Debug(GetType(), + ProfilingLogger.Debug(GetType(), "Write lucene doc id:{DocumentId}, category:{DocumentCategory}, type:{DocumentItemType}", valueSet.Id, valueSet.Category, diff --git a/src/Umbraco.ModelsBuilder.Embedded/Building/TextBuilder.cs b/src/Umbraco.ModelsBuilder.Embedded/Building/TextBuilder.cs index e59900bdc4..148d0869d4 100644 --- a/src/Umbraco.ModelsBuilder.Embedded/Building/TextBuilder.cs +++ b/src/Umbraco.ModelsBuilder.Embedded/Building/TextBuilder.cs @@ -246,7 +246,7 @@ namespace Umbraco.ModelsBuilder.Embedded.Building WriteGeneratedCodeAttribute(sb, "\t\t"); sb.AppendFormat("\t\t[ImplementPropertyType(\"{0}\")]\n", property.Alias); - sb.Append("\t\tpublic "); + sb.Append("\t\tpublic virtual "); WriteClrType(sb, property.ClrTypeName); sb.AppendFormat(" {0} => ", @@ -307,14 +307,14 @@ namespace Umbraco.ModelsBuilder.Embedded.Building if (mixinStatic) { - sb.Append("\t\tpublic "); + sb.Append("\t\tpublic virtual "); WriteClrType(sb, property.ClrTypeName); sb.AppendFormat(" {0} => {1}(this);\n", property.ClrName, MixinStaticGetterName(property.ClrName)); } else { - sb.Append("\t\tpublic "); + sb.Append("\t\tpublic virtual "); WriteClrType(sb, property.ClrTypeName); sb.AppendFormat(" {0} => this.Value", property.ClrName); diff --git a/src/Umbraco.ModelsBuilder.Embedded/Compose/ModelsBuilderComposer.cs b/src/Umbraco.ModelsBuilder.Embedded/Compose/ModelsBuilderComposer.cs index 45c4de5d2a..fee9b6f62e 100644 --- a/src/Umbraco.ModelsBuilder.Embedded/Compose/ModelsBuilderComposer.cs +++ b/src/Umbraco.ModelsBuilder.Embedded/Compose/ModelsBuilderComposer.cs @@ -1,4 +1,5 @@ -using System.Linq; +using System; +using System.Linq; using System.Reflection; using Umbraco.Core; using Umbraco.Core.Logging; @@ -20,14 +21,11 @@ namespace Umbraco.ModelsBuilder.Embedded.Compose { public void Compose(Composition composition) { - var isLegacyModelsBuilderInstalled = IsLegacyModelsBuilderInstalled(); - - composition.Configs.Add(() => new ModelsBuilderConfig()); - if (isLegacyModelsBuilderInstalled) + if (IsExternalModelsBuilderInstalled() == true) { - ComposeForLegacyModelsBuilder(composition); + ComposeForExternalModelsBuilder(composition); return; } @@ -45,22 +43,35 @@ namespace Umbraco.ModelsBuilder.Embedded.Compose ComposeForDefaultModelsFactory(composition); } - private static bool IsLegacyModelsBuilderInstalled() + private static bool IsExternalModelsBuilderInstalled() { - Assembly legacyMbAssembly = null; + var assemblyNames = new[] + { + "Umbraco.ModelsBuilder", + "ModelsBuilder.Umbraco" + }; + try { - legacyMbAssembly = Assembly.Load("Umbraco.ModelsBuilder"); + foreach (var name in assemblyNames) + { + var assembly = Assembly.Load(name); + + if (assembly != null) + { + return true; + } + } } - catch (System.Exception) + catch (Exception) { //swallow exception, DLL must not be there } - return legacyMbAssembly != null; + return false; } - private void ComposeForLegacyModelsBuilder(Composition composition) + private void ComposeForExternalModelsBuilder(Composition composition) { composition.Logger.Info("ModelsBuilder.Embedded is disabled, the external ModelsBuilder was detected."); composition.Components().Append(); diff --git a/src/Umbraco.ModelsBuilder.Embedded/LiveModelsProvider.cs b/src/Umbraco.ModelsBuilder.Embedded/LiveModelsProvider.cs index 333181f27c..1010d1db19 100644 --- a/src/Umbraco.ModelsBuilder.Embedded/LiveModelsProvider.cs +++ b/src/Umbraco.ModelsBuilder.Embedded/LiveModelsProvider.cs @@ -91,7 +91,7 @@ namespace Umbraco.ModelsBuilder.Embedded catch (Exception e) { _mbErrors.Report("Failed to build Live models.", e); - _logger.Error("Failed to generate models.", e); + _logger.Error(e, "Failed to generate models."); } finally { diff --git a/src/Umbraco.ModelsBuilder.Embedded/PureLiveModelFactory.cs b/src/Umbraco.ModelsBuilder.Embedded/PureLiveModelFactory.cs index 912d0e3363..8ef99383a4 100644 --- a/src/Umbraco.ModelsBuilder.Embedded/PureLiveModelFactory.cs +++ b/src/Umbraco.ModelsBuilder.Embedded/PureLiveModelFactory.cs @@ -308,7 +308,7 @@ namespace Umbraco.ModelsBuilder.Embedded { try { - _logger.Error("Failed to build models.", e); + _logger.Error(e, "Failed to build models."); _logger.Warn("Running without models."); // be explicit _errors.Report("Failed to build PureLive models.", e); } diff --git a/src/Umbraco.TestData/LoadTestComponent.cs b/src/Umbraco.TestData/LoadTestComponent.cs new file mode 100644 index 0000000000..97c006520d --- /dev/null +++ b/src/Umbraco.TestData/LoadTestComponent.cs @@ -0,0 +1,35 @@ +using System.Web.Mvc; +using System.Web.Routing; +using Umbraco.Core.Composing; +using System.Configuration; + +// see https://github.com/Shazwazza/UmbracoScripts/tree/master/src/LoadTesting + +namespace Umbraco.TestData +{ + public class LoadTestComponent : IComponent + { + public void Initialize() + { + if (ConfigurationManager.AppSettings["Umbraco.TestData.Enabled"] != "true") + return; + + + + RouteTable.Routes.MapRoute( + name: "LoadTest", + url: "LoadTest/{action}", + defaults: new + { + controller = "LoadTest", + action = "Index" + }, + namespaces: new[] { "Umbraco.TestData" } + ); + } + + public void Terminate() + { + } + } +} diff --git a/src/Umbraco.TestData/LoadTestComposer.cs b/src/Umbraco.TestData/LoadTestComposer.cs new file mode 100644 index 0000000000..2c5e404642 --- /dev/null +++ b/src/Umbraco.TestData/LoadTestComposer.cs @@ -0,0 +1,29 @@ +using Umbraco.Core.Composing; +using System.Configuration; +using Umbraco.Web.PublishedCache.NuCache; + +// see https://github.com/Shazwazza/UmbracoScripts/tree/master/src/LoadTesting + +namespace Umbraco.TestData +{ + public class LoadTestComposer : ComponentComposer, IUserComposer + { + public override void Compose(Composition composition) + { + base.Compose(composition); + + if (ConfigurationManager.AppSettings["Umbraco.TestData.Enabled"] != "true") + return; + + composition.Register(typeof(LoadTestController), Lifetime.Request); + + if (ConfigurationManager.AppSettings["Umbraco.TestData.IgnoreLocalDb"] == "true") + { + composition.Register(factory => new PublishedSnapshotServiceOptions + { + IgnoreLocalDb = true + }); + } + } + } +} diff --git a/src/Umbraco.TestData/LoadTestController.cs b/src/Umbraco.TestData/LoadTestController.cs index 97665dd084..8e1faf56d1 100644 --- a/src/Umbraco.TestData/LoadTestController.cs +++ b/src/Umbraco.TestData/LoadTestController.cs @@ -6,10 +6,9 @@ using Umbraco.Core.Services; using Umbraco.Core.Models; using System.Web; using System.Web.Hosting; -using System.Web.Routing; using System.Diagnostics; -using Umbraco.Core.Composing; -using System.Configuration; +using Umbraco.Core.IO; +using System.IO; // see https://github.com/Shazwazza/UmbracoScripts/tree/master/src/LoadTesting @@ -261,6 +260,15 @@ namespace Umbraco.TestData HttpRuntime.UnloadAppDomain(); } + public ActionResult ColdBootRestart() + { + Directory.Delete(IOHelper.MapPath("~/App_Data/TEMP/DistCache"), true); + + DoRestart(); + + return Content("Cold Boot Restarted."); + } + public ActionResult Restart() { DoRestart(); @@ -331,41 +339,4 @@ namespace Umbraco.TestData return t; } } - - public class TestComponent : IComponent - { - public void Initialize() - { - if (ConfigurationManager.AppSettings["Umbraco.TestData.Enabled"] != "true") - return; - - RouteTable.Routes.MapRoute( - name: "LoadTest", - url: "LoadTest/{action}", - defaults: new - { - controller = "LoadTest", - action = "Index" - }, - namespaces: new[] { "Umbraco.TestData" } - ); - } - - public void Terminate() - { - } - } - - public class TestComposer : ComponentComposer, IUserComposer - { - public override void Compose(Composition composition) - { - base.Compose(composition); - - if (ConfigurationManager.AppSettings["Umbraco.TestData.Enabled"] != "true") - return; - - composition.Register(typeof(LoadTestController), Lifetime.Request); - } - } } diff --git a/src/Umbraco.TestData/Umbraco.TestData.csproj b/src/Umbraco.TestData/Umbraco.TestData.csproj index a3753cc17b..113d209ef4 100644 --- a/src/Umbraco.TestData/Umbraco.TestData.csproj +++ b/src/Umbraco.TestData/Umbraco.TestData.csproj @@ -41,6 +41,8 @@ + + diff --git a/src/Umbraco.Tests.AcceptanceTest/cypress/integration/Content/content.ts b/src/Umbraco.Tests.AcceptanceTest/cypress/integration/Content/content.ts index 22f1f883d0..0cec374c5d 100644 --- a/src/Umbraco.Tests.AcceptanceTest/cypress/integration/Content/content.ts +++ b/src/Umbraco.Tests.AcceptanceTest/cypress/integration/Content/content.ts @@ -1,5 +1,13 @@ /// -import { DocumentTypeBuilder, ContentBuilder, AliasHelper } from 'umbraco-cypress-testhelpers'; +import { + DocumentTypeBuilder, + ContentBuilder, + AliasHelper, + GridDataTypeBuilder, + PartialViewMacroBuilder, + MacroBuilder +} from 'umbraco-cypress-testhelpers'; + context('Content', () => { beforeEach(() => { @@ -14,6 +22,23 @@ context('Content', () => { cy.get('.umb-tree-item__inner').should('exist', {timeout: 10000}); } + function createSimpleMacro(name){ + const insertMacro = new PartialViewMacroBuilder() + .withName(name) + .withContent(`@inherits Umbraco.Web.Macros.PartialViewMacroPage +

Acceptance test

`) + .build(); + + const macroWithPartial = new MacroBuilder() + .withName(name) + .withPartialViewMacro(insertMacro) + .withRenderInEditor() + .withUseInEditor() + .build(); + + cy.saveMacroWithPartial(macroWithPartial); + } + it('Copy content', () => { const rootDocTypeName = "Test document type"; const childDocTypeName = "Child test document type"; @@ -574,7 +599,7 @@ context('Content', () => { // Create content with content picker cy.get('.umb-tree-root-link').rightclick(); - cy.get('.-opens-dialog > .umb-action-link').click(); + cy.get('[data-element="action-create"]').click(); cy.get('[data-element="action-create-' + pickerDocTypeAlias + '"] > .umb-action-link').click(); // Fill out content cy.umbracoEditorHeaderName('ContentPickerContent'); @@ -596,4 +621,181 @@ context('Content', () => { cy.umbracoEnsureTemplateNameNotExists(pickerDocTypeName); cy.umbracoEnsureDocumentTypeNameNotExists(pickedDocTypeName); }); + + it('Content with macro in RTE', () => { + const viewMacroName = 'Content with macro in RTE'; + const partialFileName = viewMacroName + '.cshtml'; + + cy.umbracoEnsureMacroNameNotExists(viewMacroName); + cy.umbracoEnsurePartialViewMacroFileNameNotExists(partialFileName); + cy.umbracoEnsureDocumentTypeNameNotExists(viewMacroName); + cy.umbracoEnsureTemplateNameNotExists(viewMacroName); + cy.deleteAllContent(); + + // First thing first we got to create the macro we will be inserting + createSimpleMacro(viewMacroName); + + // Now we need to create a document type with a rich text editor where we can insert the macro + // The document type must have a template as well in order to ensure that the content is displayed correctly + const alias = AliasHelper.toAlias(viewMacroName); + const docType = new DocumentTypeBuilder() + .withName(viewMacroName) + .withAlias(alias) + .withAllowAsRoot(true) + .withDefaultTemplate(alias) + .addGroup() + .addRichTextProperty() + .withAlias('text') + .done() + .done() + .build(); + + cy.saveDocumentType(docType).then((generatedDocType) => { + // Might as wel initally create the content here, the less GUI work during the test the better + const contentNode = new ContentBuilder() + .withContentTypeAlias(generatedDocType["alias"]) + .withAction('saveNew') + .addVariant() + .withName(viewMacroName) + .withSave(true) + .done() + .build(); + + cy.saveContent(contentNode); + }); + + // Edit the macro template in order to have something to verify on when rendered. + cy.editTemplate(viewMacroName, `@inherits Umbraco.Web.Mvc.UmbracoViewPage +@using ContentModels = Umbraco.Web.PublishedModels; +@{ + Layout = null; +} +@{ + if (Model.HasValue("text")){ + @(Model.Value("text")) + } +} `); + + // Enter content + refreshContentTree(); + cy.umbracoTreeItem("content", [viewMacroName]).click(); + + // Insert macro + cy.get('#mceu_13-button').click(); + cy.get('.umb-card-grid-item').contains(viewMacroName).click(); + + // Save and publish + cy.umbracoButtonByLabelKey('buttons_saveAndPublish').click(); + cy.umbracoSuccessNotification().should('be.visible'); + + // Ensure that the view gets rendered correctly + const expected = `

Acceptance test

 

`; + cy.umbracoVerifyRenderedViewContent('/', expected, true).should('be.true'); + + // Cleanup + cy.umbracoEnsureMacroNameNotExists(viewMacroName); + cy.umbracoEnsurePartialViewMacroFileNameNotExists(partialFileName); + cy.umbracoEnsureDocumentTypeNameNotExists(viewMacroName); + cy.umbracoEnsureTemplateNameNotExists(viewMacroName); + }); + + it('Content with macro in grid', () => { + const name = 'Content with macro in grid'; + const macroName = 'Grid macro'; + const macroFileName = macroName + '.cshtml'; + + cy.umbracoEnsureDataTypeNameNotExists(name); + cy.umbracoEnsureDocumentTypeNameNotExists(name); + cy.umbracoEnsureTemplateNameNotExists(name); + cy.umbracoEnsureMacroNameNotExists(macroName); + cy.umbracoEnsurePartialViewMacroFileNameNotExists(macroFileName); + cy.deleteAllContent(); + + createSimpleMacro(macroName); + + const grid = new GridDataTypeBuilder() + .withName(name) + .withDefaultGrid() + .build(); + + const alias = AliasHelper.toAlias(name); + // Save grid and get the ID + cy.saveDataType(grid).then((dataType) => { + // Create a document type using the data type + const docType = new DocumentTypeBuilder() + .withName(name) + .withAlias(alias) + .withAllowAsRoot(true) + .withDefaultTemplate(alias) + .addGroup() + .addCustomProperty(dataType['id']) + .withAlias('grid') + .done() + .done() + .build(); + + cy.saveDocumentType(docType).then((generatedDocType) => { + const contentNode = new ContentBuilder() + .withContentTypeAlias(generatedDocType["alias"]) + .addVariant() + .withName(name) + .withSave(true) + .done() + .build(); + + cy.saveContent(contentNode); + }); + }); + + // Edit the template to allow us to verify the rendered view + cy.editTemplate(name, `@inherits Umbraco.Web.Mvc.UmbracoViewPage +@using ContentModels = Umbraco.Web.PublishedModels; +@{ + Layout = null; +} +@Html.GetGridHtml(Model, "grid")`); + + // Act + // Enter content + refreshContentTree(); + cy.umbracoTreeItem("content", [name]).click(); + // Click add + cy.get(':nth-child(2) > .preview-row > .preview-col > .preview-cell').click(); // Choose 1 column layout. + cy.get('.umb-column > .templates-preview > :nth-child(2) > small').click(); // Choose headline + cy.get('.umb-cell-placeholder').click(); + // Click macro + cy.get(':nth-child(4) > .umb-card-grid-item > :nth-child(1)').click(); + // Select the macro + cy.get('.umb-card-grid-item').contains(macroName).click(); + + // Save and publish + cy.umbracoButtonByLabelKey('buttons_saveAndPublish').click(); + cy.umbracoSuccessNotification().should('be.visible'); + + const expected = ` +
+
+
+
+
+
+
+

Acceptance test

+
+
+
+
+
+
+
` + + cy.umbracoVerifyRenderedViewContent('/', expected, true).should('be.true'); + + // Clean + cy.umbracoEnsureDataTypeNameNotExists(name); + cy.umbracoEnsureDocumentTypeNameNotExists(name); + cy.umbracoEnsureTemplateNameNotExists(name); + cy.umbracoEnsureMacroNameNotExists(macroName); + cy.umbracoEnsurePartialViewMacroFileNameNotExists(macroFileName); + }); }); diff --git a/src/Umbraco.Tests.AcceptanceTest/cypress/integration/Settings/languages.ts b/src/Umbraco.Tests.AcceptanceTest/cypress/integration/Settings/languages.ts index 49bcf94943..336e5793d9 100644 --- a/src/Umbraco.Tests.AcceptanceTest/cypress/integration/Settings/languages.ts +++ b/src/Umbraco.Tests.AcceptanceTest/cypress/integration/Settings/languages.ts @@ -6,7 +6,10 @@ context('Languages', () => { }); it('Add language', () => { - const name = "Kyrgyz (Kyrgyzstan)"; // Must be an option in the select box + // For some reason the languages to chose fom seems to be translated differently than normal, as an example: + // My system is set to EN (US), but most languages are translated into Danish for some reason + // Aghem seems untranslated though? + const name = "Aghem"; // Must be an option in the select box cy.umbracoEnsureLanguageNameNotExists(name); diff --git a/src/Umbraco.Tests.AcceptanceTest/cypress/integration/Settings/templates.ts b/src/Umbraco.Tests.AcceptanceTest/cypress/integration/Settings/templates.ts index c586384af7..65d03e5a78 100644 --- a/src/Umbraco.Tests.AcceptanceTest/cypress/integration/Settings/templates.ts +++ b/src/Umbraco.Tests.AcceptanceTest/cypress/integration/Settings/templates.ts @@ -23,14 +23,18 @@ context('Templates', () => { cy.umbracoEnsureTemplateNameNotExists(name); createTemplate(); + // We have to wait for the ace editor to load, because when the editor is loading it will "steal" the focus briefly, + // which causes the save event to fire if we've added something to the header field, causing errors. + cy.wait(500); + //Type name cy.umbracoEditorHeaderName(name); // Save // We must drop focus for the auto save event to occur. cy.get('.btn-success').focus(); // And then wait for the auto save event to finish by finding the page in the tree view. - // This is a bit of a roundabout way to find items in a treev view since we dont use umbracoTreeItem - // but we must be able to wait for the save evnent to finish, and we can't do that with umbracoTreeItem + // This is a bit of a roundabout way to find items in a tree view since we dont use umbracoTreeItem + // but we must be able to wait for the save event to finish, and we can't do that with umbracoTreeItem cy.get('[data-element="tree-item-templates"] > :nth-child(2) > .umb-animated > .umb-tree-item__inner > .umb-tree-item__label') .contains(name).should('be.visible', { timeout: 10000 }); // Now that the auto save event has finished we can save diff --git a/src/Umbraco.Tests.AcceptanceTest/cypress/integration/Tour/backofficeTour.ts b/src/Umbraco.Tests.AcceptanceTest/cypress/integration/Tour/backofficeTour.ts index 9bc1fff488..d3950d7d19 100644 --- a/src/Umbraco.Tests.AcceptanceTest/cypress/integration/Tour/backofficeTour.ts +++ b/src/Umbraco.Tests.AcceptanceTest/cypress/integration/Tour/backofficeTour.ts @@ -49,7 +49,7 @@ function resetTourData() { { "alias": "umbIntroIntroduction", "completed": false, - "disabled": false + "disabled": true }; cy.getCookie('UMB-XSRF-TOKEN', { log: false }).then((token) => { diff --git a/src/Umbraco.Tests.AcceptanceTest/package.json b/src/Umbraco.Tests.AcceptanceTest/package.json index 378fe719fc..caf75638e6 100644 --- a/src/Umbraco.Tests.AcceptanceTest/package.json +++ b/src/Umbraco.Tests.AcceptanceTest/package.json @@ -7,10 +7,10 @@ }, "devDependencies": { "cross-env": "^7.0.2", - "cypress": "^6.0.1", + "cypress": "^6.8.0", "ncp": "^2.0.0", "prompt": "^1.0.0", - "umbraco-cypress-testhelpers": "^1.0.0-beta-52" + "umbraco-cypress-testhelpers": "^1.0.0-beta-53" }, "dependencies": { "typescript": "^3.9.2" diff --git a/src/Umbraco.Tests.Benchmarks/JsonSerializerSettingsBenchmarks.cs b/src/Umbraco.Tests.Benchmarks/JsonSerializerSettingsBenchmarks.cs new file mode 100644 index 0000000000..7f419547bd --- /dev/null +++ b/src/Umbraco.Tests.Benchmarks/JsonSerializerSettingsBenchmarks.cs @@ -0,0 +1,69 @@ +using BenchmarkDotNet.Attributes; +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Umbraco.Tests.Benchmarks.Config; + +namespace Umbraco.Tests.Benchmarks +{ + [QuickRunConfig] + [MemoryDiagnoser] + public class JsonSerializerSettingsBenchmarks + { + [Benchmark] + public void SerializerSettingsInstantiation() + { + int instances = 1000; + for (int i = 0; i < instances; i++) + { + new JsonSerializerSettings(); + } + } + + [Benchmark(Baseline =true)] + public void SerializerSettingsSingleInstantiation() + { + new JsonSerializerSettings(); + } + +// // * Summary * + +// BenchmarkDotNet=v0.11.3, OS=Windows 10.0.18362 +//Intel Core i5-8265U CPU 1.60GHz(Kaby Lake R), 1 CPU, 8 logical and 4 physical cores +// [Host] : .NET Framework 4.7.2 (CLR 4.0.30319.42000), 32bit LegacyJIT-v4.8.4250.0 +// Job-JIATTD : .NET Framework 4.7.2 (CLR 4.0.30319.42000), 32bit LegacyJIT-v4.8.4250.0 + +//IterationCount=3 IterationTime=100.0000 ms LaunchCount = 1 +//WarmupCount=3 + +// Method | Mean | Error | StdDev | Ratio | RatioSD | Gen 0/1k Op | Gen 1/1k Op | Gen 2/1k Op | Allocated Memory/Op | +//-------------------------------------- |-------------:|-------------:|------------:|-------:|--------:|------------:|------------:|------------:|--------------------:| +// SerializerSettingsInstantiation | 29,120.48 ns | 5,532.424 ns | 303.2508 ns | 997.84 | 23.66 | 73.8122 | - | - | 232346 B | +// SerializerSettingsSingleInstantiation | 29.19 ns | 8.089 ns | 0.4434 ns | 1.00 | 0.00 | 0.0738 | - | - | 232 B | + +//// * Warnings * +//MinIterationTime +// JsonSerializerSettingsBenchmarks.SerializerSettingsSingleInstantiation: IterationCount= 3, IterationTime= 100.0000 ms, LaunchCount= 1, WarmupCount= 3->MinIterationTime = 96.2493 ms which is very small. It's recommended to increase it. + +//// * Legends * +// Mean : Arithmetic mean of all measurements +// Error : Half of 99.9% confidence interval +// StdDev : Standard deviation of all measurements +// Ratio : Mean of the ratio distribution ([Current]/[Baseline]) +// RatioSD : Standard deviation of the ratio distribution([Current]/[Baseline]) +// Gen 0/1k Op : GC Generation 0 collects per 1k Operations +// Gen 1/1k Op : GC Generation 1 collects per 1k Operations +// Gen 2/1k Op : GC Generation 2 collects per 1k Operations +// Allocated Memory/Op : Allocated memory per single operation(managed only, inclusive, 1KB = 1024B) +// 1 ns : 1 Nanosecond(0.000000001 sec) + +//// * Diagnostic Output - MemoryDiagnoser * + + +// // ***** BenchmarkRunner: End ***** +// Run time: 00:00:04 (4.88 sec), executed benchmarks: 2 + } +} diff --git a/src/Umbraco.Tests.Benchmarks/LoggerAllocationBenchmark.cs b/src/Umbraco.Tests.Benchmarks/LoggerAllocationBenchmark.cs new file mode 100644 index 0000000000..818eeec7f1 --- /dev/null +++ b/src/Umbraco.Tests.Benchmarks/LoggerAllocationBenchmark.cs @@ -0,0 +1,58 @@ +using BenchmarkDotNet.Attributes; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Umbraco.Core.Logging; +using Umbraco.Tests.Benchmarks.Config; + +namespace Umbraco.Tests.Benchmarks +{ + [QuickRunWithMemoryDiagnoserConfig] + public class LoggerAllocationBenchmark + { + private string rawQuery = ""; + private int totalItemCount; + [Benchmark(Baseline = true)] + public void Baseline() + { + for (int i = 0; i < 1000; i++) + { + OriginalDebugSignature(GetType(), "DeleteFromIndex with query: {Query} (found {TotalItems} results)", rawQuery, totalItemCount); + } + + } + + [Benchmark()] + public void NewOverload2() + { + for (int i = 0; i < 1000; i++) + { + NewDebugSignature(GetType(), "DeleteFromIndex with query: {Query} (found {TotalItems} results)", rawQuery, totalItemCount); + } + } + public void OriginalDebugSignature(Type reporting, string messageTemplate, params object[] propertyValues) + { + + } + + public void NewDebugSignature(Type reporting, string messageTemplate, T1 param1, T2 param2) + { + + } + + // BenchmarkDotNet=v0.11.3, OS=Windows 10.0.18362 + //Intel Core i5-8265U CPU 1.60GHz(Kaby Lake R), 1 CPU, 8 logical and 4 physical cores + // [Host] : .NET Framework 4.7.2 (CLR 4.0.30319.42000), 32bit LegacyJIT-v4.8.4180.0 + // Job-JIATTD : .NET Framework 4.7.2 (CLR 4.0.30319.42000), 32bit LegacyJIT-v4.8.4180.0 + + //IterationCount=3 IterationTime=100.0000 ms LaunchCount = 1 + //WarmupCount=3 + + // Method | Mean | Error | StdDev | Ratio | Gen 0/1k Op | Gen 1/1k Op | Gen 2/1k Op | Allocated Memory/Op | + //------------- |----------:|----------:|----------:|------:|------------:|------------:|------------:|--------------------:| + // Baseline | 14.599 us | 1.0882 us | 0.0596 us | 1.00 | 10.0420 | - | - | 32048 B | + // NewOverload2 | 1.775 us | 0.4056 us | 0.0222 us | 0.12 | - | - | - | - | + } +} diff --git a/src/Umbraco.Tests.Benchmarks/Umbraco.Tests.Benchmarks.csproj b/src/Umbraco.Tests.Benchmarks/Umbraco.Tests.Benchmarks.csproj index 4bb7faa22d..39cd7b7b95 100644 --- a/src/Umbraco.Tests.Benchmarks/Umbraco.Tests.Benchmarks.csproj +++ b/src/Umbraco.Tests.Benchmarks/Umbraco.Tests.Benchmarks.csproj @@ -53,8 +53,10 @@ + + diff --git a/src/Umbraco.Tests/Composing/TypeLoaderTests.cs b/src/Umbraco.Tests/Composing/TypeLoaderTests.cs index 9cd4f39c17..b0c57b685b 100644 --- a/src/Umbraco.Tests/Composing/TypeLoaderTests.cs +++ b/src/Umbraco.Tests/Composing/TypeLoaderTests.cs @@ -268,7 +268,7 @@ AnotherContentFinder public void GetDataEditors() { var types = _typeLoader.GetDataEditors(); - Assert.AreEqual(39, types.Count()); + Assert.AreEqual(40, types.Count()); } /// diff --git a/src/Umbraco.Tests/Configurations/UmbracoSettings/ContentElementTests.cs b/src/Umbraco.Tests/Configurations/UmbracoSettings/ContentElementTests.cs index 33df3caaad..498c12101c 100644 --- a/src/Umbraco.Tests/Configurations/UmbracoSettings/ContentElementTests.cs +++ b/src/Umbraco.Tests/Configurations/UmbracoSettings/ContentElementTests.cs @@ -83,7 +83,7 @@ namespace Umbraco.Tests.Configurations.UmbracoSettings [Test] public void DisallowedUploadFiles() { - Assert.IsTrue(SettingsSection.Content.DisallowedUploadFiles.All(x => "ashx,aspx,ascx,config,cshtml,vbhtml,asmx,air,axd".Split(',').Contains(x))); + Assert.IsTrue(SettingsSection.Content.DisallowedUploadFiles.All(x => "ashx,aspx,ascx,config,cshtml,vbhtml,asmx,air,axd,xamlx".Split(',').Contains(x))); } [Test] diff --git a/src/Umbraco.Tests/Configurations/UmbracoSettings/umbracoSettings.config b/src/Umbraco.Tests/Configurations/UmbracoSettings/umbracoSettings.config index 8cbb799d88..d802cfc7ad 100644 --- a/src/Umbraco.Tests/Configurations/UmbracoSettings/umbracoSettings.config +++ b/src/Umbraco.Tests/Configurations/UmbracoSettings/umbracoSettings.config @@ -18,9 +18,9 @@ umbracoBytes2 umbracoExtension2 - + - + inline - + - ashx,aspx,ascx,config,cshtml,vbhtml,asmx,air,axd + ashx,aspx,ascx,config,cshtml,vbhtml,asmx,air,axd,xamlx - jpg,png,gif + jpg,png,gif @@ -108,7 +108,7 @@ 1440 - + - -
-
Content of tab 1
-
-
-
Content of tab 2
-
-
+ + + +
+
Content of tab 1
+
+
+
Content of tab 2
+
+
+
@@ -37,7 +40,7 @@ Use this directive to render a tabs navigation. (function () { "use strict"; - function Controller() { + function Controller(eventsService) { var vm = this; @@ -62,7 +65,7 @@ Use this directive to render a tabs navigation. selectedTab.active = true; }; - eventsService.on("tab.tabChange", function(name, args){ + eventsService.on("app.tabChange", function(name, args){ console.log("args", args); }); diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/tree/umbtree.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/tree/umbtree.directive.js index e27ae11e6c..7868f79809 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/tree/umbtree.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/tree/umbtree.directive.js @@ -348,7 +348,7 @@ function umbTreeDirective($q, $rootScope, treeService, notificationsService, use }; $scope.selectEnabledNodeClass = node => - node && node.selected ? 'icon umb-tree-icon sprTree icon-check green temporary' : ''; + node && node.selected ? 'icon sprTree icon-check green temporary' : '-hidden'; /* helper to force reloading children of a tree node */ $scope.loadChildren = (node, forceReload) => loadChildren(node, forceReload); diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/tree/umbtreesearchbox.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/tree/umbtreesearchbox.directive.js index 3e2e7e362e..2ae17fdc6b 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/tree/umbtreesearchbox.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/tree/umbtreesearchbox.directive.js @@ -15,6 +15,7 @@ function treeSearchBox(localizationService, searchService, $q) { datatypeKey: "@", hideSearchCallback: "=", searchCallback: "=", + inputId: "@", autoFocus: "=" }, restrict: "E", // restrict to an element diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbicon.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbicon.directive.js index 87d976f6d9..73a9617aee 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbicon.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbicon.directive.js @@ -75,9 +75,8 @@ Icon with additional attribute. It can be treated like any other dom element iconHelper.getIcon(icon) .then(data => { - if (data !== null && data.svgString !== undefined) { + if (data && data.svgString) { // Watch source SVG string - //icon.svgString.$$unwrapTrustedValue(); scope.svgString = data.svgString; } }); diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbpagination.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbpagination.directive.js index b49d47b979..f939eb5e46 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbpagination.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbpagination.directive.js @@ -16,6 +16,7 @@ Use this directive to generate a pagination. total-pages="vm.pagination.totalPages" on-next="vm.nextPage" on-prev="vm.prevPage" + on-change="vm.changePage" on-go-to-page="vm.goToPage"> @@ -34,10 +35,11 @@ Use this directive to generate a pagination. vm.pagination = { pageNumber: 1, totalPages: 10 - } + }; vm.nextPage = nextPage; vm.prevPage = prevPage; + vm.changePage = changePage; vm.goToPage = goToPage; function nextPage(pageNumber) { @@ -51,6 +53,12 @@ Use this directive to generate a pagination. console.log(pageNumber); alert("prevpage"); } + + function changePage(pageNumber) { + // do magic here + console.log(pageNumber); + alert("changepage"); + } function goToPage(pageNumber) { // do magic here @@ -81,6 +89,11 @@ Use this directive to generate a pagination.
  • pageNumber: The page number
+@param {callback=} onChange (binding): Callback method when changing page. +

The callback returns:

+
    +
  • pageNumber: The page number
  • +
**/ (function() { @@ -175,9 +188,7 @@ Use this directive to generate a pagination. scope.onGoToPage(scope.pageNumber); } if (scope.onChange) { - if (scope.onChange) { - scope.onChange({ "pageNumber": scope.pageNumber }); - } + scope.onChange({ "pageNumber": scope.pageNumber }); } }; diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/upload/umbfileupload.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/upload/umbfileupload.directive.js index 3581aed9e0..60882a372f 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/upload/umbfileupload.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/upload/umbfileupload.directive.js @@ -20,11 +20,7 @@ function umbFileUpload() { el.val(''); }); - el.on('drag dragstart dragend dragover dragenter dragleave drop', function (e) { - e.preventDefault(); - e.stopPropagation(); - }) - .on('dragover dragenter', function () { + el.on('dragover dragenter', function () { scope.$emit("isDragover", { value: true }); }) .on('dragleave dragend drop', function () { diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/users/changepassword.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/users/changepassword.directive.js index 8cbdabbf75..a9961a7579 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/users/changepassword.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/users/changepassword.directive.js @@ -11,6 +11,7 @@ vm.cancelChange = cancelChange; vm.showOldPass = showOldPass; vm.showCancelBtn = showCancelBtn; + vm.newPasswordKeyUp = newPasswordKeyUp; var unsubscribe = []; @@ -55,6 +56,11 @@ vm.config.minPasswordLength = 0; } + // Check non-alpha pwd settings for tooltip display + if (vm.config.minNonAlphaNumericChars === undefined) { + vm.config.minNonAlphaNumericChars = 0; + } + //set the model defaults if (!Utilities.isObject(vm.passwordValues)) { //if it's not an object then just create a new one @@ -152,6 +158,9 @@ return vm.config.disableToggle !== true && vm.config.hasPassword; }; + function newPasswordKeyUp(event) { + vm.passwordVal = event.target.value; + } } var component = { diff --git a/src/Umbraco.Web.UI.Client/src/common/mocks/services/localization.mocks.js b/src/Umbraco.Web.UI.Client/src/common/mocks/services/localization.mocks.js index ea7f3a6d4c..9a05e3cd7f 100644 --- a/src/Umbraco.Web.UI.Client/src/common/mocks/services/localization.mocks.js +++ b/src/Umbraco.Web.UI.Client/src/common/mocks/services/localization.mocks.js @@ -193,7 +193,7 @@ angular.module('umbraco.mocks'). "defaultdialogs_recycleBinDeleting": "The items in the recycle bin are now being deleted. Please do not close this window while this operation takes place", "defaultdialogs_recycleBinIsEmpty": "The recycle bin is now empty", "defaultdialogs_recycleBinWarning": "When items are deleted from the recycle bin, they will be gone forever", - "defaultdialogs_regexSearchError": "regexlib.com's webservice is currently experiencing some problems, which we have no control over. We are very sorry for this inconvenience.", + "defaultdialogs_regexSearchError": "regexlib.com's webservice is currently experiencing some problems, which we have no control over. We are very sorry for this inconvenience.", "defaultdialogs_regexSearchHelp": "Search for a regular expression to add validation to a form field. Exemple: 'email, 'zip-code' 'url'", "defaultdialogs_removeMacro": "Remove Macro", "defaultdialogs_requiredField": "Required Field", @@ -355,7 +355,7 @@ angular.module('umbraco.mocks'). "installer_databaseHeader": "Database configuration", "installer_databaseInstall": " Press the install button to install the Umbraco %0% database ", "installer_databaseInstallDone": "Umbraco %0% has now been copied to your database. Press Next to proceed.", - "installer_databaseNotFound": "

Database not found! Please check that the information in the 'connection string' of the \"web.config\" file is correct.

To proceed, please edit the 'web.config' file (using Visual Studio or your favourite text editor), scroll to the bottom, add the connection string for your database in the key named 'UmbracoDbDSN' and save the file.

Click the retry button when done.
More information on editing web.config here.

", + "installer_databaseNotFound": "

Database not found! Please check that the information in the 'connection string' of the \"web.config\" file is correct.

To proceed, please edit the 'web.config' file (using Visual Studio or your favourite text editor), scroll to the bottom, add the connection string for your database in the key named 'UmbracoDbDSN' and save the file.

Click the retry button when done.
More information on editing web.config here.

", "installer_databaseText": "To complete this step, you must know some information regarding your database server ('connection string').
Please contact your ISP if necessary. If you're installing on a local machine or server you might need information from your system administrator.", "installer_databaseUpgrade": "

Press the upgrade button to upgrade your database to Umbraco %0%

Don't worry - no content will be deleted and everything will continue working afterwards!

", "installer_databaseUpgradeDone": "Your database has been upgraded to the final version %0%.
Press Next to proceed. ", @@ -420,7 +420,7 @@ angular.module('umbraco.mocks'). "login_greeting6": "Happy friendly Friday", "login_greeting7": "Happy shiny Saturday", "login_instruction": "Log in below:", - "login_bottomText": "

© 2001 - %0%
Umbraco.org

", + "login_bottomText": "

© 2001 - %0%
Umbraco.org

", "main_dashboard": "Dashboard", "main_sections": "Sections", "main_tree": "Content", 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 01b6360d07..a3a5b1946d 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 @@ -432,13 +432,15 @@ function contentTypeResource($q, $http, umbRequestHelper, umbDataFormatter, loca throw "args.id cannot be null"; } + var promise = localizationService.localize("contentType_moveFailed"); + return umbRequestHelper.resourcePromise( $http.post(umbRequestHelper.getApiUrl("contentTypeApiBaseUrl", "PostMove"), { parentId: args.parentId, id: args.id }, { responseType: 'text' }), - 'Failed to move content'); + promise); }, /** @@ -475,13 +477,15 @@ function contentTypeResource($q, $http, umbRequestHelper, umbDataFormatter, loca throw "args.id cannot be null"; } + var promise = localizationService.localize("contentType_copyFailed"); + return umbRequestHelper.resourcePromise( $http.post(umbRequestHelper.getApiUrl("contentTypeApiBaseUrl", "PostCopy"), { parentId: args.parentId, id: args.id }, { responseType: 'text' }), - 'Failed to copy content'); + promise); }, /** @@ -511,41 +515,8 @@ function contentTypeResource($q, $http, umbRequestHelper, umbDataFormatter, loca $http.post(umbRequestHelper.getApiUrl("contentTypeApiBaseUrl", "PostCreateContainer", { parentId: parentId, name: encodeURIComponent(name) })), 'Failed to create a folder under parent id ' + parentId); - }, - /** - * @ngdoc method - * @name umbraco.resources.contentTypeResource#createCollection - * @methodOf umbraco.resources.contentTypeResource - * - * @description - * Create a collection of a content types - * - * ##usage - *
-        * contentTypeResource.createCollection(1244,"testcollectionname",true,"collectionItemName",true,"icon-name","icon-name")
-        *    .then(function() {
-        *       Do stuff..
-        *    });
-        * 
- * - * @param {Int} parentId the ID of the parent content type underneath which to create the collection - * @param {String} collectionName the name of the collection - * @param {Boolean} collectionCreateTemplate true/false to specify whether to create a default template for the collection - * @param {String} collectionItemName the name of the collection item - * @param {Boolean} collectionItemCreateTemplate true/false to specify whether to create a default template for the collection item - * @param {String} collectionIcon the icon for the collection - * @param {String} collectionItemIcon the icon for the collection item - * @returns {Promise} resourcePromise object. - * - */ - createCollection: function (parentId, collectionName, collectionCreateTemplate, collectionItemName, collectionItemCreateTemplate, collectionIcon, collectionItemIcon) { - - return umbRequestHelper.resourcePromise( - $http.post(umbRequestHelper.getApiUrl("contentTypeApiBaseUrl", "PostCreateCollection", { parentId: parentId, collectionName: collectionName, collectionCreateTemplate: collectionCreateTemplate, collectionItemName: collectionItemName, collectionItemCreateTemplate: collectionItemCreateTemplate, collectionIcon: collectionIcon, collectionItemIcon: collectionItemIcon})), - 'Failed to create collection under ' + parentId); - - }, + }, /** * @ngdoc method 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 d194ae2c73..e3fab86067 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 @@ -208,7 +208,7 @@ function mediaTypeResource($q, $http, umbRequestHelper, umbDataFormatter, locali throw "args.id cannot be null"; } - var promise = localizationService.localize("media_moveFailed"); + var promise = localizationService.localize("mediaType_moveFailed"); return umbRequestHelper.resourcePromise( $http.post(umbRequestHelper.getApiUrl("mediaTypeApiBaseUrl", "PostMove"), @@ -230,7 +230,7 @@ function mediaTypeResource($q, $http, umbRequestHelper, umbDataFormatter, locali throw "args.id cannot be null"; } - var promise = localizationService.localize("media_copyFailed"); + var promise = localizationService.localize("mediaType_copyFailed"); return umbRequestHelper.resourcePromise( $http.post(umbRequestHelper.getApiUrl("mediaTypeApiBaseUrl", "PostCopy"), diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/membertype.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/membertype.resource.js index 2314fa6d6c..bf02d9618e 100644 --- a/src/Umbraco.Web.UI.Client/src/common/resources/membertype.resource.js +++ b/src/Umbraco.Web.UI.Client/src/common/resources/membertype.resource.js @@ -3,7 +3,7 @@ * @name umbraco.resources.memberTypeResource * @description Loads in data for member types **/ -function memberTypeResource($q, $http, umbRequestHelper, umbDataFormatter) { +function memberTypeResource($q, $http, umbRequestHelper, umbDataFormatter, localizationService) { return { @@ -102,8 +102,29 @@ function memberTypeResource($q, $http, umbRequestHelper, umbDataFormatter) { return umbRequestHelper.resourcePromise( $http.post(umbRequestHelper.getApiUrl("memberTypeApiBaseUrl", "PostSave"), saveModel), 'Failed to save data for member type id ' + contentType.id); - } + }, + copy: function (args) { + if (!args) { + throw "args cannot be null"; + } + if (!args.parentId) { + throw "args.parentId cannot be null"; + } + if (!args.id) { + throw "args.id cannot be null"; + } + + var promise = localizationService.localize("memberType_copyFailed"); + + return umbRequestHelper.resourcePromise( + $http.post(umbRequestHelper.getApiUrl("memberTypeApiBaseUrl", "PostCopy"), + { + parentId: args.parentId, + id: args.id + }, { responseType: 'text' }), + promise); + } }; } angular.module('umbraco.resources').factory('memberTypeResource', memberTypeResource); 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 b301960eab..8524b960c6 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 @@ -759,6 +759,59 @@ function contentEditingHelper(fileManager, $q, $location, $routeParams, editorSt //don't add a browser history for this $location.replace(); return true; + }, + + /** + * @ngdoc function + * @name umbraco.services.contentEditingHelper#sortVariants + * @methodOf umbraco.services.contentEditingHelper + * @function + * + * @description + * Sorts the variants so default language is shown first. Mandatory languages are shown next and all other underneath. Both Mandatory and non mandatory languages are + * sorted in the following groups 'Published', 'Draft', 'Not Created'. Within each of those groups the variants are + * sorted by the language display name. + * + */ + sortVariants: function (a, b) { + const statesOrder = {'PublishedPendingChanges':1, 'Published': 1, 'Draft': 2, 'NotCreated': 3}; + const compareDefault = (a,b) => (!a.language.isDefault ? 1 : -1) - (!b.language.isDefault ? 1 : -1); + + // Make sure mandatory variants goes on top, unless they are published, cause then they already goes to the top and then we want to mix them with other published variants. + const compareMandatory = (a,b) => (a.state === 'PublishedPendingChanges' || a.state === 'Published') ? 0 : (!a.language.isMandatory ? 1 : -1) - (!b.language.isMandatory ? 1 : -1); + const compareState = (a, b) => (statesOrder[a.state] || 99) - (statesOrder[b.state] || 99); + const compareName = (a, b) => a.displayName.localeCompare(b.displayName); + + return compareDefault(a, b) || compareMandatory(a, b) || compareState(a, b) || compareName(a, b); + }, + + /** + * @ngdoc function + * @name umbraco.services.contentEditingHelper#getSortedVariantsAndSegments + * @methodOf umbraco.services.contentEditingHelper + * @function + * + * @description + * Returns an array of variants and segments sorted by the rules in the sortVariants method. + * A variant language is followed by its segments in the array. If a segment doesn't have a parent variant it is + * added to the end of the array. + * + */ + getSortedVariantsAndSegments: function (variantsAndSegments) { + const sortedVariants = variantsAndSegments.filter(variant => !variant.segment).sort(this.sortVariants); + let segments = variantsAndSegments.filter(variant => variant.segment); + let sortedAvailableVariants = []; + + sortedVariants.forEach((variant) => { + const sortedMatchedSegments = segments.filter(segment => segment.language.culture === variant.language.culture).sort(this.sortVariants); + segments = segments.filter(segment => segment.language.culture !== variant.language.culture); + sortedAvailableVariants = [...sortedAvailableVariants, ...[variant], ...sortedMatchedSegments]; + }) + + // if we have segments without a parent language variant we need to add the remaining segments to the array + sortedAvailableVariants = [...sortedAvailableVariants, ...segments.sort(this.sortVariants)]; + + return sortedAvailableVariants; } }; } diff --git a/src/Umbraco.Web.UI.Client/src/common/services/editor.service.js b/src/Umbraco.Web.UI.Client/src/common/services/editor.service.js index e512e52643..b0a2465f42 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/editor.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/editor.service.js @@ -179,8 +179,7 @@ When building a custom infinite editor view you can use the same components as a } else { focus(); } - }); - + }); /** * @ngdoc method @@ -188,7 +187,7 @@ When building a custom infinite editor view you can use the same components as a * @methodOf umbraco.services.editorService * * @description - * Method to return all open editors + * Method to return all open editors. */ function getEditors() { return editors; @@ -200,7 +199,7 @@ When building a custom infinite editor view you can use the same components as a * @methodOf umbraco.services.editorService * * @description - * Method to return the number of open editors + * Method to return the number of open editors. */ function getNumberOfEditors() { return editors.length; @@ -251,8 +250,8 @@ When building a custom infinite editor view you can use the same components as a * Method to open a new editor in infinite editing. * * @param {object} editor rendering options. - * @param {string} editor.view Path to view. - * @param {string} editor.size Sets the size of the editor ("small" || "medium"). If nothing is set it will use full width. + * @param {string} editor.view URL to view. + * @param {string} editor.size Sets the size of the editor (`small` or `medium`). If nothing is set it will use full width. */ function open(editor) { @@ -287,7 +286,7 @@ When building a custom infinite editor view you can use the same components as a * @methodOf umbraco.services.editorService * * @description - * Method to close the latest opened editor + * Method to close the latest opened editor. */ function close() { @@ -322,7 +321,7 @@ When building a custom infinite editor view you can use the same components as a * @methodOf umbraco.services.editorService * * @description - * Method to close all open editors + * Method to close all open editors. */ function closeAll() { @@ -343,6 +342,7 @@ When building a custom infinite editor view you can use the same components as a * * @description * Opens a content editor in infinite editing, the submit callback returns the updated content item. + * * @param {object} editor rendering options. * @param {string} editor.id The id of the content item. * @param {boolean} editor.create Create new content item. @@ -352,7 +352,6 @@ When building a custom infinite editor view you can use the same components as a * @param {string} editor.documentTypeAlias If editor.create is true, provide document type alias for the creation of the content item. * @param {boolean} editor.allowSaveAndClose If editor is being used in infinite editing allows the editor to close when the save action is performed. * @param {boolean} editor.allowPublishAndClose If editor is being used in infinite editing allows the editor to close when the publish action is performed. - * * @returns {object} editor object */ function contentEditor(editor) { @@ -396,7 +395,6 @@ When building a custom infinite editor view you can use the same components as a * @param {boolean} editor.multiPicker Pick one or multiple items. * @param {function} editor.submit Callback function when the submit button is clicked. Returns the editor model object. * @param {function} editor.close Callback function when the close button is clicked. - * * @returns {object} editor object */ function contentTypePicker(editor) { @@ -419,7 +417,6 @@ When building a custom infinite editor view you can use the same components as a * @param {boolean} editor.multiPicker Pick one or multiple items. * @param {function} editor.submit Callback function when the submit button is clicked. Returns the editor model object. * @param {function} editor.close Callback function when the close button is clicked. - * * @returns {object} editor object. */ function mediaTypePicker(editor) { @@ -459,13 +456,14 @@ When building a custom infinite editor view you can use the same components as a * * @description * Opens a copy editor in infinite editing, the submit callback returns an array of selected items. + * + * @param {object} editor rendering options. * @param {string} editor.section The node entity type. * @param {string} editor.currentNode The current node id. - * @param {function} editor.submit Saves, submits, and closes the editor. - * @param {function} editor.close Closes the editor. + * @param {function} editor.submit Callback function when the submit button is clicked. Returns the editor model object. + * @param {function} editor.close Callback function when the close button is clicked. * @returns {object} editor object. */ - function copy(editor) { editor.view = "views/common/infiniteeditors/copy/copy.html"; if (!editor.size) editor.size = "small"; @@ -479,13 +477,14 @@ When building a custom infinite editor view you can use the same components as a * * @description * Opens a move editor in infinite editing. + * + * @param {object} editor rendering options. * @param {string} editor.section The node entity type. * @param {string} editor.currentNode The current node id. - * @param {function} editor.submit Saves, submits, and closes the editor. - * @param {function} editor.close Closes the editor. + * @param {function} editor.submit Callback function when the submit button is clicked. Returns the editor model object. + * @param {function} editor.close Callback function when the close button is clicked. * @returns {object} editor object. */ - function move(editor) { editor.view = "views/common/infiniteeditors/move/move.html"; if (!editor.size) editor.size = "small"; @@ -499,11 +498,12 @@ When building a custom infinite editor view you can use the same components as a * * @description * Opens an embed editor in infinite editing. - * @param {function} editor.submit Saves, submits, and closes the editor. - * @param {function} editor.close Closes the editor. + * + * @param {object} editor rendering options. + * @param {function} editor.submit Callback function when the submit button is clicked. Returns the editor model object. + * @param {function} editor.close Callback function when the close button is clicked. * @returns {object} editor object. */ - function embed(editor) { editor.view = "views/common/infiniteeditors/embed/embed.html"; if (!editor.size) editor.size = "small"; @@ -517,12 +517,13 @@ When building a custom infinite editor view you can use the same components as a * * @description * Opens a rollback editor in infinite editing. + * + * @param {object} editor rendering options. * @param {string} editor.node The node to rollback. - * @param {function} editor.submit Saves, submits, and closes the editor. - * @param {function} editor.close Closes the editor. + * @param {function} editor.submit Callback function when the submit button is clicked. Returns the editor model object. + * @param {function} editor.close Callback function when the close button is clicked. * @returns {object} editor object. */ - function rollback(editor) { editor.view = "views/common/infiniteeditors/rollback/rollback.html"; if (!editor.size) editor.size = "medium"; @@ -536,11 +537,12 @@ When building a custom infinite editor view you can use the same components as a * * @description * Opens an embed editor in infinite editing. + * * @param {object} editor rendering options. * @param {string} editor.icon The icon class. * @param {string} editor.color The color class. - * @param {function} editor.submit Saves, submits, and closes the editor. - * @param {function} editor.close Closes the editor. + * @param {function} editor.submit Callback function when the submit button is clicked. Returns the editor model object. + * @param {function} editor.close Callback function when the close button is clicked. * @returns {object} editor object. */ function linkPicker(editor) { @@ -556,11 +558,12 @@ When building a custom infinite editor view you can use the same components as a * * @description * Opens a media editor in infinite editing, the submit callback returns the updated media item. + * * @param {object} editor rendering options. * @param {string} editor.id The id of the media item. * @param {boolean} editor.create Create new media item. - * @param {function} editor.submit Saves, submits, and closes the editor. - * @param {function} editor.close Closes the editor. + * @param {function} editor.submit Callback function when the submit button is clicked. Returns the editor model object. + * @param {function} editor.close Callback function when the close button is clicked. * @returns {object} editor object. */ function mediaEditor(editor) { @@ -575,6 +578,7 @@ When building a custom infinite editor view you can use the same components as a * * @description * Opens a media picker in infinite editing, the submit callback returns an array of selected media items. + * * @param {object} editor rendering options. * @param {number} editor.startNodeId Set the startnode of the picker (optional). * @param {boolean} editor.multiPicker Pick one or multiple items. @@ -582,8 +586,8 @@ When building a custom infinite editor view you can use the same components as a * @param {boolean} editor.disableFolderSelect Disable folder selection. * @param {boolean} editor.disableFocalPoint Disable focal point editor for selected media. * @param {array} editor.updatedMediaNodes A list of ids for media items that have been updated through the media picker. - * @param {function} editor.submit Submits the editor. - * @param {function} editor.close Closes the editor. + * @param {function} editor.submit Callback function when the submit button is clicked. Returns the editor model object. + * @param {function} editor.close Callback function when the close button is clicked. * @returns {object} editor object. */ function mediaPicker(editor) { @@ -593,6 +597,24 @@ When building a custom infinite editor view you can use the same components as a open(editor); } + /** + * @ngdoc method + * @name umbraco.services.editorService#mediaCropDetails + * @methodOf umbraco.services.editorService + * + * @description + * Opens the media crop details editor in infinite editing, the submit callback returns the updated media object. + * + * @param {object} editor rendering options. + * @param {function} editor.submit Callback function when the submit button is clicked. Returns the editor model object. + * @param {function} editor.close Callback function when the close button is clicked. + * @returns {object} editor object. + */ + function mediaCropDetails(editor) { + editor.view = "views/common/infiniteeditors/mediapicker/overlays/mediacropdetails.html"; + open(editor); + } + /** * @ngdoc method * @name umbraco.services.editorService#iconPicker @@ -600,11 +622,12 @@ When building a custom infinite editor view you can use the same components as a * * @description * Opens an icon picker in infinite editing, the submit callback returns the selected icon. + * * @param {object} editor rendering options. - * @param {string} editor.icon The CSS class representing the icon - eg. "icon-autofill". - * @param {string} editor.color The CSS class representing the color - eg. "color-red". - * @param {callback} editor.submit Submits the editor. - * @param {callback} editor.close Closes the editor. + * @param {string} editor.icon The CSS class representing the icon - eg. `icon-autofill. + * @param {string} editor.color The CSS class representing the color - eg. color-red. + * @param {function} editor.submit Callback function when the submit button is clicked. Returns the editor model object. + * @param {function} editor.close Callback function when the close button is clicked. * @returns {object} editor object. */ function iconPicker(editor) { @@ -620,14 +643,15 @@ When building a custom infinite editor view you can use the same components as a * * @description * Opens the document type editor in infinite editing, the submit callback returns the alias of the saved document type. + * * @param {object} editor rendering options. * @param {number} editor.id Indicates the ID of the document type to be edited. Alternatively the ID may be set to `-1` in combination with `create` being set to `true` to open the document type editor for creating a new document type. * @param {boolean} editor.create Set to `true` to open the document type editor for creating a new document type. * @param {boolean} editor.noTemplate If `true` and in combination with `create` being set to `true`, the document type editor will not create a corresponding template by default. This is similar to selecting the "Document Type without a template" in the Create dialog. * @param {boolean} editor.isElement If `true` and in combination with `create` being set to `true`, the "Is an Element type" option will be selected by default in the document type editor. * @param {boolean} editor.allowVaryByCulture If `true` and in combination with `create`, the "Allow varying by culture" option will be selected by default in the document type editor. - * @param {function} editor.submit Submits the editor. - * @param {function} editor.close Closes the editor. + * @param {function} editor.submit Callback function when the submit button is clicked. Returns the editor model object. + * @param {function} editor.close Callback function when the close button is clicked. * @returns {object} editor object. */ function documentTypeEditor(editor) { @@ -642,10 +666,11 @@ When building a custom infinite editor view you can use the same components as a * * @description * Opens the media type editor in infinite editing, the submit callback returns the saved media type. + * * @param {object} editor rendering options. - * @param {function} editor.submit Submits the editor. - * @param {function} editor.close Closes the editor. - * @returns {object} editor object + * @param {function} editor.submit Callback function when the submit button is clicked. Returns the editor model object. + * @param {function} editor.close Callback function when the close button is clicked. + * @returns {object} editor object. */ function mediaTypeEditor(editor) { editor.view = "views/mediatypes/edit.html"; @@ -659,9 +684,10 @@ When building a custom infinite editor view you can use the same components as a * * @description * Opens the member type editor in infinite editing, the submit callback returns the saved member type. + * * @param {object} editor rendering options. - * @param {function} editor.submit Submits the editor. - * @param {function} editor.close Closes the editor. + * @param {function} editor.submit Callback function when the submit button is clicked. Returns the editor model object. + * @param {function} editor.close Callback function when the close button is clicked. * @returns {object} editor object. */ function memberTypeEditor(editor) { @@ -676,9 +702,10 @@ When building a custom infinite editor view you can use the same components as a * * @description * Opens the query builder in infinite editing, the submit callback returns the generated query. + * * @param {object} editor rendering options. - * @param {function} editor.submit Submits the editor. - * @param {function} editor.close Closes the editor. + * @param {function} editor.submit Callback function when the submit button is clicked. Returns the editor model object. + * @param {function} editor.close Callback function when the close button is clicked. * @returns {object} editor object. */ function queryBuilder(editor) { @@ -693,12 +720,13 @@ When building a custom infinite editor view you can use the same components as a * * @description * Opens the query builder in infinite editing, the submit callback returns the generted query. + * * @param {object} editor rendering options. * @param {string} options.section tree section to display. * @param {string} options.treeAlias specific tree to display. * @param {boolean} options.multiPicker should the tree pick one or multiple items before returning. - * @param {function} editor.submit Submits the editor. - * @param {function} editor.close Closes the editor. + * @param {function} editor.submit Callback function when the submit button is clicked. Returns the editor model object. + * @param {function} editor.close Callback function when the close button is clicked. * @returns {object} editor object. */ function treePicker(editor) { @@ -714,9 +742,10 @@ When building a custom infinite editor view you can use the same components as a * * @description * Opens the an editor to set node permissions. + * * @param {object} editor rendering options. - * @param {function} editor.submit Submits the editor. - * @param {function} editor.close Closes the editor. + * @param {function} editor.submit Callback function when the submit button is clicked. Returns the editor model object. + * @param {function} editor.close Callback function when the close button is clicked. * @returns {object} editor object. */ function nodePermissions(editor) { @@ -732,9 +761,10 @@ When building a custom infinite editor view you can use the same components as a * * @description * Open an editor to insert code snippets into the code editor. + * * @param {object} editor rendering options. - * @param {function} editor.submit Submits the editor. - * @param {function} editor.close Closes the editor. + * @param {function} editor.submit Callback function when the submit button is clicked. Returns the editor model object. + * @param {function} editor.close Callback function when the close button is clicked. * @returns {object} editor object. */ function insertCodeSnippet(editor) { @@ -750,6 +780,7 @@ When building a custom infinite editor view you can use the same components as a * * @description * Opens the user group picker in infinite editing, the submit callback returns an array of the selected user groups. + * * @param {object} editor rendering options. * @param {function} editor.submit Submits the editor. * @param {function} editor.close Closes the editor. @@ -767,10 +798,11 @@ When building a custom infinite editor view you can use the same components as a * @methodOf umbraco.services.editorService * * @description - * Opens the user group picker in infinite editing, the submit callback returns the saved user group + * Opens the user group picker in infinite editing, the submit callback returns the saved user group. + * * @param {object} editor rendering options. - * @param {function} editor.submit Submits the editor. - * @param {function} editor.close Closes the editor. + * @param {function} editor.submit Callback function when the submit button is clicked. Returns the editor model object. + * @param {function} editor.close Callback function when the close button is clicked. * @returns {object} editor object. */ function userGroupEditor(editor) { @@ -785,10 +817,11 @@ When building a custom infinite editor view you can use the same components as a * * @description * Opens the template editor in infinite editing, the submit callback returns the saved template. + * * @param {object} editor rendering options. * @param {string} editor.id The template id. - * @param {function} editor.submit Submits the editor. - * @param {function} editor.close Closes the editor. + * @param {function} editor.submit Callback function when the submit button is clicked. Returns the editor model object. + * @param {function} editor.close Callback function when the close button is clicked. * @returns {object} editor object. */ function templateEditor(editor) { @@ -803,9 +836,10 @@ When building a custom infinite editor view you can use the same components as a * * @description * Opens the section picker in infinite editing, the submit callback returns an array of the selected sections. + * * @param {object} editor rendering options. - * @param {function} editor.submit Submits the editor. - * @param {function} editor.close Closes the editor. + * @param {function} editor.submit Callback function when the submit button is clicked. Returns the editor model object. + * @param {function} editor.close Callback function when the close button is clicked. * @returns {object} editor object. */ function sectionPicker(editor) { @@ -821,9 +855,10 @@ When building a custom infinite editor view you can use the same components as a * * @description * Opens the insert field editor in infinite editing, the submit callback returns the code snippet. + * * @param {object} editor rendering options. - * @param {function} editor.submit Submits the editor. - * @param {function} editor.close Closes the editor. + * @param {function} editor.submit Callback function when the submit button is clicked. Returns the editor model object. + * @param {function} editor.close Callback function when the close button is clicked. * @returns {object} editor object. */ function insertField(editor) { @@ -839,9 +874,10 @@ When building a custom infinite editor view you can use the same components as a * * @description * Opens the template sections editor in infinite editing, the submit callback returns the type to insert. + * * @param {object} editor rendering options. - * @param {function} editor.submit Submits the editor. - * @param {function} editor.close Closes the editor. + * @param {function} editor.submit Callback function when the submit button is clicked. Returns the editor model object. + * @param {function} editor.close Callback function when the close button is clicked. * @returns {object} editor object. */ function templateSections(editor) { @@ -857,9 +893,10 @@ When building a custom infinite editor view you can use the same components as a * * @description * Opens the section picker in infinite editing, the submit callback returns an array of the selected users. + * * @param {object} editor rendering options. - * @param {function} editor.submit Submits the editor. - * @param {function} editor.close Closes the editor. + * @param {function} editor.submit Callback function when the submit button is clicked. Returns the editor model object. + * @param {function} editor.close Callback function when the close button is clicked. * @returns {object} editor object. */ function userPicker(editor) { @@ -880,8 +917,8 @@ When building a custom infinite editor view you can use the same components as a * @param {array} editor.availableItems Array of available items. * @param {array} editor.selectedItems Array of selected items. When passed in the selected items will be filtered from the available items. * @param {boolean} editor.filter Set to false to hide the filter. - * @param {function} editor.submit Submits the editor. - * @param {function} editor.close Closes the editor. + * @param {function} editor.submit Callback function when the submit button is clicked. Returns the editor model object. + * @param {function} editor.close Callback function when the close button is clicked. * @returns {object} editor object. */ function itemPicker(editor) { @@ -898,8 +935,9 @@ When building a custom infinite editor view you can use the same components as a * @description * Opens a macro picker in infinite editing, the submit callback returns an array of the selected items. * - * @param {function} editor.submit Submits the editor. - * @param {function} editor.close Closes the editor. + * @param {object} editor rendering options. + * @param {function} editor.submit Callback function when the submit button is clicked. Returns the editor model object. + * @param {function} editor.close Callback function when the close button is clicked. * @returns {object} editor object. */ function macroPicker(editor) { @@ -918,8 +956,8 @@ When building a custom infinite editor view you can use the same components as a * * @param {object} editor rendering options. * @param {object} editor.multiPicker Pick one or multiple items. - * @param {function} editor.submit Submits the editor. - * @param {function} editor.close Closes the editor. + * @param {function} editor.submit Callback function when the submit button is clicked. Returns the editor model object. + * @param {function} editor.close Callback function when the close button is clicked. * @returns {object} editor object. */ function memberGroupPicker(editor) { @@ -940,7 +978,6 @@ When building a custom infinite editor view you can use the same components as a * @param {boolean} editor.multiPicker Pick one or multiple items. * @param {function} editor.submit Callback function when the submit button is clicked. Returns the editor model object. * @param {function} editor.close Callback function when the close button is clicked. - * * @returns {object} editor object. */ function memberPicker(editor) { @@ -958,13 +995,13 @@ When building a custom infinite editor view you can use the same components as a * * @description * Opens a member editor in infinite editing, the submit callback returns the updated member. + * * @param {object} editor rendering options. * @param {string} editor.id The id (GUID) of the member. * @param {boolean} editor.create Create new member. * @param {function} editor.submit Callback function when the submit button is clicked. Returns the editor model object. * @param {function} editor.close Callback function when the close button is clicked. - * @param {string} editor.doctype If editor.create is true, provide member type for the creation of the member. - * + * @param {string} editor.doctype If `editor.create` is `true`, provide member type for the creation of the member. * @returns {object} editor object. */ function memberEditor(editor) { @@ -982,7 +1019,6 @@ When building a custom infinite editor view you can use the same components as a * @description * Internal method to keep track of keyboard shortcuts registered * to each editor so they can be rebound when an editor closes. - * */ function unbindKeyboardShortcuts() { const shortcuts = Utilities.copy(keyboardService.keyboardEvent); @@ -1002,7 +1038,6 @@ When building a custom infinite editor view you can use the same components as a * * @description * Internal method to rebind keyboard shortcuts for the editor in focus. - * */ function rebindKeyboardShortcuts() { // find the shortcuts from the previous editor @@ -1055,7 +1090,8 @@ When building a custom infinite editor view you can use the same components as a macroPicker: macroPicker, memberGroupPicker: memberGroupPicker, memberPicker: memberPicker, - memberEditor: memberEditor + memberEditor: memberEditor, + mediaCropDetails }; return service; diff --git a/src/Umbraco.Web.UI.Client/src/common/services/formhelper.service.js b/src/Umbraco.Web.UI.Client/src/common/services/formhelper.service.js index bd6bbcc5b3..773aa85f6f 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/formhelper.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/formhelper.service.js @@ -46,7 +46,7 @@ function formHelper(angularHelper, serverValidationManager, notificationsService args.scope.$broadcast("formSubmitting", { scope: args.scope, action: args.action }); this.focusOnFirstError(currentForm); - + // Some property editors need to perform an action after all property editors have reacted to the formSubmitting. args.scope.$broadcast("formSubmittingFinalPhase", { scope: args.scope, action: args.action }); @@ -80,7 +80,7 @@ function formHelper(angularHelper, serverValidationManager, notificationsService * * @description * Called by submitForm when a form has been submitted, it will fire a focus on the first found invalid umb-property it finds in the form.. - * + * * @param {object} form Pass in a form object. */ focusOnFirstError: function(form) { @@ -89,9 +89,9 @@ function formHelper(angularHelper, serverValidationManager, notificationsService if(firstInvalidNgForm.length !== 0) { var focusableFields = [...firstInvalidNgForm.find("umb-range-slider .noUi-handle,input,textarea,select,button")]; - if(focusableFields.length !== 0) { + if(focusableFields.length !== 0) { var firstErrorEl = focusableFields.find(el => el.type !== "hidden" && el.hasAttribute("readonly") === false); - if(firstErrorEl.length !== 0) { + if(firstErrorEl !== undefined) { firstErrorEl.focus(); } } diff --git a/src/Umbraco.Web.UI.Client/src/common/services/iconhelper.service.js b/src/Umbraco.Web.UI.Client/src/common/services/iconhelper.service.js index f26763bd14..f3f5deb695 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/iconhelper.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/iconhelper.service.js @@ -3,9 +3,9 @@ * @name umbraco.services.iconHelper * @description A helper service for dealing with icons, mostly dealing with legacy tree icons **/ -function iconHelper($http, $q, $sce, $timeout, umbRequestHelper) { +function iconHelper($http, $q, $sce, $timeout) { - var converter = [ + const converter = [ { oldIcon: ".sprNew", newIcon: "add" }, { oldIcon: ".sprDelete", newIcon: "remove" }, { oldIcon: ".sprMove", newIcon: "enter" }, @@ -85,15 +85,61 @@ function iconHelper($http, $q, $sce, $timeout, umbRequestHelper) { { oldIcon: ".sprTreeDeveloperPython", newIcon: "icon-linux" } ]; - var collectedIcons; + let collectedIcons; - var imageConverter = [ - {oldImage: "contour.png", newIcon: "icon-umb-contour"} - ]; + let imageConverter = [ + {oldImage: "contour.png", newIcon: "icon-umb-contour"} + ]; - var iconCache = []; - var liveRequests = []; - var allIconsRequested = false; + const iconCache = []; + const promiseQueue = []; + let resourceLoadStatus = "none"; + + /** + * This is the same approach as use for loading the localized text json + * We don't want multiple requests for the icon collection, so need to track + * the current request state, and resolve the queued requests once the icons arrive + * Subsequent requests are returned immediately as the icons are cached into + */ + function init() { + const deferred = $q.defer(); + + if (resourceLoadStatus === "loaded") { + deferred.resolve(iconCache); + return deferred.promise; + } + + if (resourceLoadStatus === "loading") { + promiseQueue.push(deferred); + return deferred.promise; + } + + resourceLoadStatus = "loading"; + + $http({ method: "GET", url: Umbraco.Sys.ServerVariables.umbracoUrls.iconApiBaseUrl + 'GetIcons' }) + .then(function (response) { + resourceLoadStatus = "loaded"; + + for (const [key, value] of Object.entries(response.data.Data)) { + iconCache.push({name: key, svgString: $sce.trustAsHtml(value)}) + } + + deferred.resolve(iconCache); + + //ensure all other queued promises are resolved + for (let p in promiseQueue) { + promiseQueue[p].resolve(iconCache); + } + }, function (err) { + deferred.reject("Something broke"); + //ensure all other queued promises are resolved + for (let p in promiseQueue) { + promiseQueue[p].reject("Something broke"); + } + }); + + return deferred.promise; + } return { @@ -187,67 +233,12 @@ function iconHelper($http, $q, $sce, $timeout, umbRequestHelper) { /** Gets a single IconModel */ getIcon: function(iconName) { - return $q((resolve, reject) => { - var icon = this._getIconFromCache(iconName); - - if(icon !== undefined) { - resolve(icon); - } else { - var iconRequestPath = Umbraco.Sys.ServerVariables.umbracoUrls.iconApiBaseUrl + 'GetIcon?iconName=' + iconName; - - // If the current icon is being requested, wait a bit so that we don't have to make another http request and can instead get the icon from the cache. - // This is a bit rough and ready and could probably be improved used an event based system - if(liveRequests.indexOf(iconRequestPath) >= 0) { - setTimeout(() => { - resolve(this.getIcon(iconName)); - }, 10); - } else { - liveRequests.push(iconRequestPath); - // TODO - fix bug where Umbraco.Sys.ServerVariables.umbracoUrls.iconApiBaseUrl is undefinied when help icon - umbRequestHelper.resourcePromise( - $http.get(iconRequestPath) - ,'Failed to retrieve icon: ' + iconName) - .then(icon => { - if(icon) { - var trustedIcon = this.defineIcon(icon.Name, icon.SvgString); - - liveRequests = _.filter(liveRequests, iconRequestPath); - - resolve(trustedIcon); - } - }) - .catch(err => { - console.warn(err); - }); - }; - - } - }); + return init().then(icons => icons.find(i => i.name === iconName)); }, /** Gets all the available icons in the backoffice icon folder and returns them as an array of IconModels */ getAllIcons: function() { - return $q((resolve, reject) => { - if(allIconsRequested === false) { - allIconsRequested = true; - - umbRequestHelper.resourcePromise( - $http.get(Umbraco.Sys.ServerVariables.umbracoUrls.iconApiBaseUrl + 'GetAllIcons') - ,'Failed to retrieve icons') - .then(icons => { - icons.forEach(icon => { - this.defineIcon(icon.Name, icon.SvgString); - }); - - resolve(iconCache); - }) - .catch(err => { - console.warn(err); - });; - } else { - resolve(iconCache); - } - }); + return init().then(icons => icons); }, /** LEGACY - Return a list of icons from icon fonts, optionally filter them */ @@ -312,9 +303,8 @@ function iconHelper($http, $q, $sce, $timeout, umbRequestHelper) { }, /** Returns the cached icon or undefined */ - _getIconFromCache: function(iconName) { - return _.find(iconCache, {name: iconName}); - } + _getIconFromCache: iconName => iconCache.find(icon => icon.name === iconName) + }; } angular.module('umbraco.services').factory('iconHelper', iconHelper); diff --git a/src/Umbraco.Web.UI.Client/src/common/services/listviewhelper.service.js b/src/Umbraco.Web.UI.Client/src/common/services/listviewhelper.service.js index 14643dc9cd..f9ebba00ea 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/listviewhelper.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/listviewhelper.service.js @@ -572,13 +572,15 @@ * Method for opening an item in a list view for editing. * * @param {Object} item The item to edit + * @param {Object} scope The scope with options */ function editItem(item, scope) { + if (!item.editPath) { return; } - if (scope.options.useInfiniteEditor) + if (scope && scope.options && scope.options.useInfiniteEditor) { var editorModel = { id: item.id, diff --git a/src/Umbraco.Web.UI.Client/src/common/services/localization.service.js b/src/Umbraco.Web.UI.Client/src/common/services/localization.service.js index f8493ab39d..99162eaf53 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/localization.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/localization.service.js @@ -330,6 +330,7 @@ angular.module('umbraco.services') resourceFileLoadStatus = "none"; resourceLoadingPromise = []; }); + // return the local instance when called return service; diff --git a/src/Umbraco.Web.UI.Client/src/common/services/navigation.service.js b/src/Umbraco.Web.UI.Client/src/common/services/navigation.service.js index 3164c3ab19..c628e3a5b1 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/navigation.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/navigation.service.js @@ -30,6 +30,7 @@ function navigationService($routeParams, $location, $q, $injector, eventsService var element = $(args.element); element.addClass('above-backdrop'); }); + //A list of query strings defined that when changed will not cause a reload of the route var nonRoutingQueryStrings = ["mculture", "cculture", "csegment", "lq", "sr"]; diff --git a/src/Umbraco.Web.UI.Client/src/common/services/umbrequesthelper.service.js b/src/Umbraco.Web.UI.Client/src/common/services/umbrequesthelper.service.js index 0a4009264d..2b5447cdf6 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/umbrequesthelper.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/umbrequesthelper.service.js @@ -252,10 +252,10 @@ function umbRequestHelper($http, $q, notificationsService, eventsService, formHe //each item has a property alias and the file object, we'll ensure that the alias is suffixed to the key // so we know which property it belongs to on the server side var file = args.files[f]; - var fileKey = "file_" + file.alias + "_" + (file.culture ? file.culture : "") + "_" + (file.segment ? file.segment : ""); + var fileKey = "file_" + (file.alias || '').replace(/_/g, '\\_') + "_" + (file.culture ? file.culture.replace(/_/g, '\\_') : "") + "_" + (file.segment ? file.segment.replace(/_/g, '\\_') : ""); if (Utilities.isArray(file.metaData) && file.metaData.length > 0) { - fileKey += ("_" + file.metaData.join("_")); + fileKey += ("_" + _.map(file.metaData, x => ('' + x).replace(/_/g, '\\_')).join("_")); } formData.append(fileKey, file.file); } diff --git a/src/Umbraco.Web.UI.Client/src/installer/installer.service.js b/src/Umbraco.Web.UI.Client/src/installer/installer.service.js index 74858d652e..d3ab9e519c 100644 --- a/src/Umbraco.Web.UI.Client/src/installer/installer.service.js +++ b/src/Umbraco.Web.UI.Client/src/installer/installer.service.js @@ -19,8 +19,8 @@ angular.module("umbraco.install").factory('installerService', function ($rootSco "Over 500 000 websites are currently powered by Umbraco", "At least 2 people have named their cat 'Umbraco'", "On an average day more than 1000 people download Umbraco", - "umbraco.tv is the premier source of Umbraco video tutorials to get you started", - "You can find the world's friendliest CMS community at our.umbraco.com", + "umbraco.tv is the premier source of Umbraco video tutorials to get you started", + "You can find the world's friendliest CMS community at our.umbraco.com", "You can become a certified Umbraco developer by attending one of the official courses", "Umbraco works really well on tablets", "You have 100% control over your markup and design when crafting a website in Umbraco", @@ -30,7 +30,7 @@ angular.module("umbraco.install").factory('installerService', function ($rootSco "At least 4 people have the Umbraco logo tattooed on them", "'Umbraco' is the Danish name for an allen key", "Umbraco has been around since 2005, that's a looong time in IT", - "More than 700 people from all over the world meet each year in Denmark in May for our annual conference CodeGarden", + "More than 700 people from all over the world meet each year in Denmark in May for our annual conference CodeGarden", "While you are installing Umbraco someone else on the other side of the planet is probably doing it too", "You can extend Umbraco without modifying the source code using either JavaScript or C#", "Umbraco has been installed in more than 198 countries" diff --git a/src/Umbraco.Web.UI.Client/src/installer/steps/upgrade.html b/src/Umbraco.Web.UI.Client/src/installer/steps/upgrade.html index 472ceb7135..8ea69b3ee4 100644 --- a/src/Umbraco.Web.UI.Client/src/installer/steps/upgrade.html +++ b/src/Umbraco.Web.UI.Client/src/installer/steps/upgrade.html @@ -10,7 +10,7 @@ To compare versions and read a report of changes between versions, use the View Report button below.

- View Report + View Report

Simply click continue below to be guided through the rest of the upgrade. diff --git a/src/Umbraco.Web.UI.Client/src/less/application/umb-outline.less b/src/Umbraco.Web.UI.Client/src/less/application/umb-outline.less index 939366d5ac..1f1c2c0e72 100644 --- a/src/Umbraco.Web.UI.Client/src/less/application/umb-outline.less +++ b/src/Umbraco.Web.UI.Client/src/less/application/umb-outline.less @@ -15,6 +15,7 @@ right: 0; border-radius: 3px; box-shadow: 0 0 2px 0px @ui-outline, inset 0 0 2px 2px @ui-outline; + pointer-events: none; } } diff --git a/src/Umbraco.Web.UI.Client/src/less/buttons.less b/src/Umbraco.Web.UI.Client/src/less/buttons.less index c446a02424..d1a426f818 100644 --- a/src/Umbraco.Web.UI.Client/src/less/buttons.less +++ b/src/Umbraco.Web.UI.Client/src/less/buttons.less @@ -65,15 +65,22 @@ // -------------------------------------------------- .btn-reset { - padding: 0; - margin: 0; - border: none; + padding: 0; + margin: 0; + border: none; background: none; - color: currentColor; + color: currentColor; font-family: @baseFontFamily; font-size: @baseFontSize; line-height: @baseLineHeight; - cursor: pointer; + cursor: pointer; + + // Disabled state + &.disabled, + &[disabled], + &:disabled:hover { + cursor: default; + } } // Button Sizes diff --git a/src/Umbraco.Web.UI.Client/src/less/components/editor/umb-editor.less b/src/Umbraco.Web.UI.Client/src/less/components/editor/umb-editor.less index 19d6a1306e..274f74bbac 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/editor/umb-editor.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/editor/umb-editor.less @@ -34,6 +34,17 @@ &--level0 { transform: none; } + + &--small .umb-property { + .control-header { + float: none; + width: 100%; + } + + .controls { + margin-left: 0; + } + } } // use a loop to build the editor levels diff --git a/src/Umbraco.Web.UI.Client/src/less/components/editor/umb-variant-switcher.less b/src/Umbraco.Web.UI.Client/src/less/components/editor/umb-variant-switcher.less index 95625d9e73..ce9286e5f5 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/editor/umb-variant-switcher.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/editor/umb-variant-switcher.less @@ -69,7 +69,6 @@ button.umb-variant-switcher__toggle { .umb-variant-switcher__expand { color: @ui-action-discreet-type; - margin-top: 3px; margin-left: 5px; margin-right: -5px; transition: color 0.2s ease-in-out; @@ -196,10 +195,6 @@ button.umb-variant-switcher__toggle { .umb-variant-switcher__item.--current { color: @ui-light-active-type; - //background-color: @pinkExtraLight; - .umb-variant-switcher__name-wrapper { - border-left: 4px solid @ui-active; - } .umb-variant-switcher__name { //color: @ui-light-active-type; font-weight: 700; diff --git a/src/Umbraco.Web.UI.Client/src/less/components/tree/umb-tree.less b/src/Umbraco.Web.UI.Client/src/less/components/tree/umb-tree.less index 8234618393..a39a38fbde 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/tree/umb-tree.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/tree/umb-tree.less @@ -48,6 +48,8 @@ color: @gray-7; display: block; padding-left: 35px; + white-space: initial; + text-align: left; } } @@ -102,10 +104,9 @@ body.touch .umb-tree { .umb-button-ellipsis--hidden { opacity: 1; } - + .umb-tree-icon { color: @ui-option-type-hover; - flex-shrink: 0; } } } @@ -132,7 +133,6 @@ body.touch .umb-tree { .umb-tree .umb-search-group { position: inherit; display: inherit; - list-style: none; h6 { @@ -155,13 +155,17 @@ body.touch .umb-tree { &-link { display: block; + width: 100%; + text-align: left; } &-name { display: flex; - &__text { + &__text { margin: 1px 0 0; + overflow:hidden; + text-overflow: ellipsis; } } } @@ -336,9 +340,13 @@ body.touch .umb-tree { .umb-tree-icon { vertical-align: middle; margin: 0 13px 0 0; - //color: @gray-1; color: @ui-option-type; - font-size: 20px; + font-size: 20px; + + &.-hidden { + display: none; + visibility: hidden; + } &.blue { color: @blue; diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-content-grid.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-content-grid.less index 47fc8a10b9..c590421b97 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-content-grid.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-content-grid.less @@ -102,11 +102,11 @@ .umb-content-grid__details-label { font-weight: bold; - display: inline-block; + display: inline; } .umb-content-grid__details-value { - display: inline-block; + display: inline; word-break: break-word; margin-left: 3px; } diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-grid.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-grid.less index e1fc5573e5..281284a5ca 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-grid.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-grid.less @@ -401,6 +401,7 @@ text-align: center; text-align: -moz-center; width: 100%; + box-sizing: border-box; } .umb-grid .umb-editor-placeholder i { @@ -475,9 +476,19 @@ } } +// Control states +.umb-grid-media--controls { + display:none; + position: absolute; + top:0.5rem; + right:0.5rem; +} - - +.umb-grid .umb-row .umb-control.-active { + .umb-grid-media--controls { + display:flex; + } +} // Title bar and tools .umb-grid .umb-row-title-bar { diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-icon.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-icon.less index e08174e378..318ce0a563 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-icon.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-icon.less @@ -2,6 +2,7 @@ display: inline-block; width: 1em; height: 1em; + flex-shrink: 0; svg { width: 100%; diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-search-filter.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-search-filter.less index b96d3e8569..b38f5937c7 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-search-filter.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-search-filter.less @@ -21,11 +21,12 @@ html .umb-search-filter { // "icon-search" class it kept for backward compatibility .umb-icon, .icon-search { - color: #d8d7d9; + color: @gray-8; position: absolute; top: 0; bottom: 0; left: 10px; margin: auto 0; + pointer-events: none; } } diff --git a/src/Umbraco.Web.UI.Client/src/less/forms.less b/src/Umbraco.Web.UI.Client/src/less/forms.less index 90b2dbe37e..3782fca695 100644 --- a/src/Umbraco.Web.UI.Client/src/less/forms.less +++ b/src/Umbraco.Web.UI.Client/src/less/forms.less @@ -308,7 +308,14 @@ select[size] { input[type="file"], input[type="radio"], input[type="checkbox"] { - .umb-outline(); + &:focus { + border-color: @inputBorderFocus; + outline: 0; + + .tabbing-active & { + outline: 2px solid @ui-outline; + } + } } @@ -582,19 +589,21 @@ table.domains .help-inline { } } .add-on { - display: inline-block; + display: inline-flex; + align-items: center; + justify-content: center; width: auto; height: 22px; min-width: 18px; - padding: 5px 6px 3px 6px; + padding: 4px 6px; font-size: @baseFontSize; font-weight: normal; line-height: @baseLineHeight; text-align: center; - //text-shadow: 0 1px 0 @white; background-color: @white; border: 1px solid @inputBorder; color: @ui-option-type; + &:hover { border-color:@inputBorderFocus; color: @ui-option-type-hover; @@ -800,9 +809,12 @@ legend + .control-group { // adjustments for properties tab .form-horizontal .block-form .control-label { - display: block; - float: none; - width: 100%; + display: block; + float: none; + width: 100%; +} +.form-horizontal .block-form .controls { + margin-left: 0; } //make sure buttons are always on top diff --git a/src/Umbraco.Web.UI.Client/src/less/main.less b/src/Umbraco.Web.UI.Client/src/less/main.less index f9bd179caf..66afbfd73f 100644 --- a/src/Umbraco.Web.UI.Client/src/less/main.less +++ b/src/Umbraco.Web.UI.Client/src/less/main.less @@ -402,6 +402,10 @@ table thead button:focus{ /* UI interactions */ +.ui-sortable-handle { + cursor: move; +} + .umb-table tbody.ui-sortable tr { cursor:pointer; @@ -606,6 +610,9 @@ table thead button:focus{ display: inline; } +.relative { + position:relative; +} // Input label styles // @Simon: not sure where to put this part yet diff --git a/src/Umbraco.Web.UI.Client/src/less/panel.less b/src/Umbraco.Web.UI.Client/src/less/panel.less index a036267c85..cc87a0edf5 100644 --- a/src/Umbraco.Web.UI.Client/src/less/panel.less +++ b/src/Umbraco.Web.UI.Client/src/less/panel.less @@ -349,6 +349,7 @@ .umb-panel-header-icon { cursor: pointer; + font-size: 2rem; margin-right: 5px; margin-top: -6px; height: 50px; 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 0d8f270f1b..f5e652aa3d 100644 --- a/src/Umbraco.Web.UI.Client/src/less/property-editors.less +++ b/src/Umbraco.Web.UI.Client/src/less/property-editors.less @@ -838,22 +838,25 @@ // // Date/time picker // -------------------------------------------------- -.bootstrap-datetimepicker-widget .btn{padding: 0;} -.bootstrap-datetimepicker-widget .picker-switch .btn{ background: none; border: none;} -.umb-datepicker .input-append .add-on{cursor: pointer;} -.umb-datepicker .input-append .on-top { - border: 0 none; +.bootstrap-datetimepicker-widget .btn {padding: 0;} +.bootstrap-datetimepicker-widget .picker-switch .btn { background: none; border: none;} +.umb-datepicker .input-append .btn-clear { + border: none; position: absolute; margin-left: -31px; margin-top: 1px; - display: inline-block; - padding: 5px 6px 3px 6px; + display: inline-flex; + align-items: center; + justify-content: center; + height: 30px; + padding: 4px 6px; font-size: @baseFontSize; font-weight: normal; line-height: @baseLineHeight; text-align: center; background-color: @white; color: @ui-option-type; + &:hover { color: @ui-option-type-hover; } diff --git a/src/Umbraco.Web.UI.Client/src/navigation.controller.js b/src/Umbraco.Web.UI.Client/src/navigation.controller.js index a383c2d44a..9f5a6e46c6 100644 --- a/src/Umbraco.Web.UI.Client/src/navigation.controller.js +++ b/src/Umbraco.Web.UI.Client/src/navigation.controller.js @@ -325,6 +325,9 @@ function NavigationController($scope, $rootScope, $location, $log, $q, $routePar var queryParams = {}; if ($scope.selectedLanguage && $scope.selectedLanguage.culture) { queryParams["culture"] = $scope.selectedLanguage.culture; + if (!mainCulture) { + $location.search("mculture", $scope.selectedLanguage.culture); + } } var queryString = $.param(queryParams); //create the query string from the params object } diff --git a/src/Umbraco.Web.UI.Client/src/utilities.js b/src/Umbraco.Web.UI.Client/src/utilities.js index 64884b589b..01e18e4e1c 100644 --- a/src/Umbraco.Web.UI.Client/src/utilities.js +++ b/src/Umbraco.Web.UI.Client/src/utilities.js @@ -71,28 +71,29 @@ const isScope = obj => obj && obj.$evalAsync && obj.$watch; const toJsonReplacer = (key, value) => { - var val = value; + var val = value; if (typeof key === 'string' && key.charAt(0) === '$' && key.charAt(1) === '$') { - val = undefined; + val = undefined; } else if (isWindow(value)) { - val = '$WINDOW'; - } else if (value && window.document === value) { - val = '$DOCUMENT'; + val = '$WINDOW'; + } else if (value && window.document === value) { + val = '$DOCUMENT'; } else if (isScope(value)) { - val = '$SCOPE'; - } + val = '$SCOPE'; + } return val; - } + }; + /** * Equivalent to angular.toJson */ const toJson = (obj, pretty) => { if (isUndefined(obj)) return undefined; if (!isNumber(pretty)) { - pretty = pretty ? 2 : null; + pretty = pretty ? 2 : null; } return JSON.stringify(obj, toJsonReplacer, pretty); - } + }; /** * Equivalent to angular.fromJson @@ -102,7 +103,7 @@ return val; } return JSON.parse(val); - } + }; /** * Not equivalent to angular.forEach. But like the angularJS method this does not fail on null or undefined. @@ -112,7 +113,7 @@ return obj.forEach(iterator); } return obj; - } + }; let _utilities = { noop: noop, diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/blockeditor/blockeditor.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/blockeditor/blockeditor.controller.js index 88cda027a8..d3a87791f9 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/blockeditor/blockeditor.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/blockeditor/blockeditor.controller.js @@ -1,6 +1,7 @@ angular.module("umbraco") .controller("Umbraco.Editors.BlockEditorController", function ($scope, localizationService, formHelper, overlayService) { + var vm = this; vm.model = $scope.model; @@ -52,7 +53,7 @@ angular.module("umbraco") vm.saveButtonState = "error"; } } - } + }; vm.close = function () { if (vm.model && vm.model.close) { @@ -93,7 +94,7 @@ angular.module("umbraco") } } - } + }; } ); 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 2367771804..8fe5526c53 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 @@ -1,4 +1,4 @@ -

+
@@ -14,20 +14,12 @@ hide-description="true"> -
+ - + + -
- - -
- -
+ diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/blockpicker/blockpicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/blockpicker/blockpicker.controller.js index 2894e0bef4..90803a3765 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/blockpicker/blockpicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/blockpicker/blockpicker.controller.js @@ -1,11 +1,14 @@ angular.module("umbraco") .controller("Umbraco.Editors.BlockPickerController", function ($scope, localizationService) { - var vm = this; + var vm = this; vm.navigation = []; + vm.filter = { + searchTerm: '' + }; localizationService.localizeMany(["blockEditor_tabCreateEmpty", "blockEditor_tabClipboard"]).then( function (data) { @@ -28,33 +31,32 @@ angular.module("umbraco") vm.activeTab = vm.navigation[0]; } ); - - vm.onNavigationChanged = function(tab) { + vm.onNavigationChanged = function (tab) { vm.activeTab.active = false; vm.activeTab = tab; vm.activeTab.active = true; - } + }; - vm.clickClearClipboard = function() { + vm.clickClearClipboard = function () { vm.onNavigationChanged(vm.navigation[0]); vm.navigation[1].disabled = true;// disabled ws determined when creating the navigation, so we need to update it here. vm.model.clipboardItems = [];// This dialog is not connected via the clipboardService events, so we need to update manually. vm.model.clickClearClipboard(); - } + }; vm.model = $scope.model; - vm.selectItem = function(item, $event) { + vm.selectItem = function (item, $event) { vm.model.selectedItem = item; vm.model.submit($scope.model, $event); - } + }; - vm.close = function() { + vm.close = function () { if ($scope.model && $scope.model.close) { $scope.model.close($scope.model); } - } + }; } ); diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/blockpicker/blockpicker.html b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/blockpicker/blockpicker.html index 4b08d4e5fc..b72de0960d 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/blockpicker/blockpicker.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/blockpicker/blockpicker.html @@ -12,35 +12,35 @@ hide-description="true"> -
+ -
+
- -
+
+
-
+ diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/datatypesettings/datatypesettings.html b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/datatypesettings/datatypesettings.html index 428905020b..77cfc705a3 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/datatypesettings/datatypesettings.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/datatypesettings/datatypesettings.html @@ -36,13 +36,15 @@
- -
- - - - - + +
+
+ + + + +
+ diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/embed/embed.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/embed/embed.controller.js index c3d1312109..e29ba996c7 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/embed/embed.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/embed/embed.controller.js @@ -18,8 +18,11 @@ preview: "", success: false, info: "", - supportsDimensions: false - }; + a11yInfo: "", + supportsDimensions: false, + originalWidth: 360, + originalHeight: 240 + }; if ($scope.model.modify) { Utilities.extend($scope.model.embed, $scope.model.modify); @@ -32,8 +35,8 @@ vm.changeSize = changeSize; vm.submit = submit; vm.close = close; - - function onInit() { + + function onInit() { if (!$scope.model.title) { localizationService.localize("general_embed").then(function(value){ $scope.model.title = value; @@ -46,8 +49,8 @@ if ($scope.model.embed.url) { $scope.model.embed.show = true; $scope.model.embed.info = ""; + $scope.model.embed.a11yInfo = ""; $scope.model.embed.success = false; - vm.loading = true; $http({ @@ -67,6 +70,7 @@ //not supported $scope.model.embed.preview = ""; $scope.model.embed.info = "Not supported"; + $scope.model.embed.a11yInfo = $scope.model.embed.info; $scope.model.embed.success = false; $scope.model.embed.supportsDimensions = false; vm.trustedPreview = null; @@ -75,6 +79,7 @@ //error $scope.model.embed.preview = ""; $scope.model.embed.info = "Could not embed media - please ensure the URL is valid"; + $scope.model.embed.a11yInfo = $scope.model.embed.info; $scope.model.embed.success = false; $scope.model.embed.supportsDimensions = false; vm.trustedPreview = null; @@ -83,6 +88,8 @@ $scope.model.embed.success = true; $scope.model.embed.supportsDimensions = response.data.SupportsDimensions; $scope.model.embed.preview = response.data.Markup; + $scope.model.embed.info = ""; + $scope.model.embed.a11yInfo = "Retrieved URL"; vm.trustedPreview = $sce.trustAsHtml(response.data.Markup); break; } @@ -94,24 +101,28 @@ $scope.model.embed.supportsDimensions = false; $scope.model.embed.preview = ""; $scope.model.embed.info = "Could not embed media - please ensure the URL is valid"; - + $scope.model.embed.a11yInfo = $scope.model.embed.info; vm.loading = false; }); } else { $scope.model.embed.supportsDimensions = false; $scope.model.embed.preview = ""; $scope.model.embed.info = "Please enter a URL"; + $scope.model.embed.a11yInfo = $scope.model.embed.info; } } function changeSize(type) { - var width, height; + var width = parseInt($scope.model.embed.width, 10); + var height = parseInt($scope.model.embed.height, 10); + var originalWidth = parseInt($scope.model.embed.originalWidth, 10); + var originalHeight = parseInt($scope.model.embed.originalHeight, 10); + var resize = originalWidth !== width || originalHeight !== height; if ($scope.model.embed.constrain) { - width = parseInt($scope.model.embed.width, 10); - height = parseInt($scope.model.embed.height, 10); - if (type == 'width') { + + if (type === 'width') { origHeight = Math.round((width / origWidth) * height); $scope.model.embed.height = origHeight; } else { @@ -119,9 +130,12 @@ $scope.model.embed.width = origWidth; } } - if ($scope.model.embed.url !== "") { + $scope.model.embed.originalWidth = $scope.model.embed.width; + $scope.model.embed.originalHeight = $scope.model.embed.height; + if ($scope.model.embed.url !== "" && resize) { showPreview(); } + } function toggleConstrain() { @@ -138,7 +152,7 @@ if ($scope.model && $scope.model.close) { $scope.model.close(); } - } + } onInit(); } diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/embed/embed.html b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/embed/embed.html index 19cf9b2278..fd86f55c07 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/embed/embed.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/embed/embed.html @@ -15,7 +15,7 @@ - + -

+ +
- - + + - - + + - - + +
diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/iconpicker/iconpicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/iconpicker/iconpicker.controller.js index 59a9aed4bb..51c2c15898 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/iconpicker/iconpicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/iconpicker/iconpicker.controller.js @@ -10,6 +10,10 @@ function IconPickerController($scope, localizationService, iconHelper) { var vm = this; + vm.filter = { + searchTerm: '' + }; + vm.selectIcon = selectIcon; vm.selectColor = selectColor; vm.submit = submit; diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/iconpicker/iconpicker.html b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/iconpicker/iconpicker.html index 7368d2f39b..19fe679189 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/iconpicker/iconpicker.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/iconpicker/iconpicker.html @@ -20,7 +20,7 @@
    -
  • +
+ position="center"> There are no macros available to insert @@ -53,7 +53,7 @@
  • - + diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/mediapicker/mediapicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/mediapicker/mediapicker.controller.js index 7214e0b0ea..fec2e632c5 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/mediapicker/mediapicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/mediapicker/mediapicker.controller.js @@ -327,10 +327,10 @@ angular.module("umbraco") gotoFolder($scope.currentFolder).then(function () { $timeout(function () { if ($scope.multiPicker) { - var images = _.rest($scope.images, $scope.images.length - files.length); + var images = _.rest(_.sortBy($scope.images, 'id'), $scope.images.length - files.length); images.forEach(image => selectMedia(image)); } else { - var image = $scope.images[$scope.images.length - 1]; + var image = _.sortBy($scope.images, 'id')[$scope.images.length - 1]; clickHandler(image); } }); @@ -373,12 +373,11 @@ angular.module("umbraco") function openDetailsDialog() { const dialog = { - view: "views/common/infiniteeditors/mediapicker/overlays/mediacropdetails.html", size: "small", cropSize: $scope.cropSize, target: $scope.target, disableFocalPoint: $scope.disableFocalPoint, - submit: function (model) { + submit: function () { $scope.model.selection.push($scope.target); $scope.model.submit($scope.model); @@ -392,7 +391,7 @@ angular.module("umbraco") localizationService.localize("defaultdialogs_editSelectedMedia").then(value => { dialog.title = value; - editorService.open(dialog); + editorService.mediaCropDetails(dialog); }); }; diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/mediapicker/overlays/mediacropdetails.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/mediapicker/overlays/mediacropdetails.controller.js index 1c7b2a7520..c6927cbaa9 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/mediapicker/overlays/mediacropdetails.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/mediapicker/overlays/mediacropdetails.controller.js @@ -7,8 +7,9 @@ vm.submit = submit; vm.close = close; vm.hasCrops = cropSet() === true; - + vm.focalPointChanged = focalPointChanged; vm.disableFocalPoint = false; + if(typeof $scope.model.disableFocalPoint === "boolean") { vm.disableFocalPoint = $scope.model.disableFocalPoint } @@ -20,25 +21,17 @@ $scope.model.target.focalPoint = { left: .5, top: .5 }; } - vm.shouldShowUrl = shouldShowUrl; - vm.focalPointChanged = focalPointChanged; - if (!$scope.model.target.image) { $scope.model.target.image = $scope.model.target.url; } - function shouldShowUrl() { - if (!$scope.model.target) { - return false; - } - if ($scope.model.target.id) { - return false; - } - if ($scope.model.target.url && $scope.model.target.url.toLower().indexOf("blob:") === 0) { - return false; - } - return true; - } + if (!$scope.model.target + || $scope.model.target.id + || ($scope.model.target.url && $scope.model.target.url.toLowerCase().startsWith("blob:"))) { + vm.shouldShowUrl = false; + } else { + vm.shouldShowUrl = true; + } /** * Called when the umbImageGravity component updates the focal point value diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/mediapicker/overlays/mediacropdetails.html b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/mediapicker/overlays/mediacropdetails.html index da6e3f439c..de936da163 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/mediapicker/overlays/mediacropdetails.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/mediapicker/overlays/mediacropdetails.html @@ -10,7 +10,7 @@ -
    +
    @@ -24,6 +24,13 @@
    +
    +
    + +
    + +
    +
    diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/sectionpicker/sectionpicker.html b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/sectionpicker/sectionpicker.html index feb3b9ab53..452f4b2364 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/sectionpicker/sectionpicker.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/sectionpicker/sectionpicker.html @@ -19,10 +19,15 @@
      -
    • +
    • -
      diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/userpicker/userpicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/userpicker/userpicker.controller.js index a7021b2867..33d526c3cf 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/userpicker/userpicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/userpicker/userpicker.controller.js @@ -1,8 +1,8 @@ (function () { "use strict"; - function UserPickerController($scope, usersResource, localizationService, eventsService) { - + function UserPickerController($scope, entityResource, localizationService, eventsService) { + var vm = this; vm.users = []; @@ -102,17 +102,9 @@ vm.loading = true; // Get users - usersResource.getPagedResults(vm.usersOptions).then(function (users) { - - vm.users = users.items; - - vm.usersOptions.pageNumber = users.pageNumber; - vm.usersOptions.pageSize = users.pageSize; - vm.usersOptions.totalItems = users.totalItems; - vm.usersOptions.totalPages = users.totalPages; - + entityResource.getAll("User").then(function (data) { + vm.users = data; preSelect($scope.model.selection, vm.users); - vm.loading = false; }); } diff --git a/src/Umbraco.Web.UI.Client/src/views/components/application/umb-login.html b/src/Umbraco.Web.UI.Client/src/views/components/application/umb-login.html index 1e570b4af6..c4b6a4a2ed 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/application/umb-login.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/application/umb-login.html @@ -226,7 +226,8 @@
      - + +
      diff --git a/src/Umbraco.Web.UI.Client/src/views/components/application/umb-navigation.html b/src/Umbraco.Web.UI.Client/src/views/components/application/umb-navigation.html index 86e905183c..bb4b025410 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/application/umb-navigation.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/application/umb-navigation.html @@ -23,7 +23,7 @@ class="umb-language-picker__dropdown-item" ng-class="{'umb-language-picker__dropdown-item--current': language.active}" ng-click="selectLanguage(language)" - ng-repeat="language in languages | orderBy:'name'" + ng-repeat="language in languages" > Switch language to diff --git a/src/Umbraco.Web.UI.Client/src/views/components/content/umb-variant-content.html b/src/Umbraco.Web.UI.Client/src/views/components/content/umb-variant-content.html index 40a88ae6a6..9efc0e80bd 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/content/umb-variant-content.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/content/umb-variant-content.html @@ -36,7 +36,8 @@
      - + +
      diff --git a/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editor-content-header.html b/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editor-content-header.html index 687c8ef24e..6e33633512 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editor-content-header.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editor-content-header.html @@ -40,7 +40,7 @@ maxlength="255" /> - @@ -51,7 +51,7 @@ @@ -60,7 +60,11 @@
      Open in split view
      @@ -92,6 +96,7 @@
      diff --git a/src/Umbraco.Web.UI.Client/src/views/components/forms/umb-search-filter.html b/src/Umbraco.Web.UI.Client/src/views/components/forms/umb-search-filter.html index d6fde29090..ab21654f91 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/forms/umb-search-filter.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/forms/umb-search-filter.html @@ -2,27 +2,14 @@
      - - - +
      diff --git a/src/Umbraco.Web.UI.Client/src/views/components/tree/umb-tree-search-box.html b/src/Umbraco.Web.UI.Client/src/views/components/tree/umb-tree-search-box.html index 77498cd007..054472e4b6 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/tree/umb-tree-search-box.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/tree/umb-tree-search-box.html @@ -2,6 +2,7 @@ - - Sorry, we can not find what you are looking for. - - -
        -
      • -
          -
        • -
          - -
          + +

          Sorry, we can not find what you are looking for.

          +
          +

          1 item returned

          +

          {{results.length}} items returned

          +
            +
          • +
              +
            • +
              + +
              +
            • +
          • -
          -
        • -
        +
    diff --git a/src/Umbraco.Web.UI.Client/src/views/components/tree/umb-tree.html b/src/Umbraco.Web.UI.Client/src/views/components/tree/umb-tree.html index aa36c1ad08..6531114567 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/tree/umb-tree.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/tree/umb-tree.html @@ -4,7 +4,11 @@
    - + + {{tree.name}}
    @@ -38,7 +42,11 @@
    - + + {{group.name}}
    diff --git a/src/Umbraco.Web.UI.Client/src/views/components/users/change-password.html b/src/Umbraco.Web.UI.Client/src/views/components/users/change-password.html index 974f8d6b4e..273f56d256 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/users/change-password.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/users/change-password.html @@ -43,12 +43,14 @@ required val-server-field="password" ng-minlength="{{vm.config.minPasswordLength}}" - no-dirty-check /> + no-dirty-check + ng-keyup="vm.newPasswordKeyUp($event)"/> Required Minimum {{vm.config.minPasswordLength}} characters {{changePasswordForm.password.errorMsg}} + diff --git a/src/Umbraco.Web.UI.Client/src/views/content/overlays/publish.controller.js b/src/Umbraco.Web.UI.Client/src/views/content/overlays/publish.controller.js index b924793fcd..4e3496cde1 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/overlays/publish.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/content/overlays/publish.controller.js @@ -1,7 +1,7 @@ (function () { "use strict"; - function PublishController($scope, localizationService) { + function PublishController($scope, localizationService, contentEditingHelper) { var vm = this; vm.loading = true; @@ -143,25 +143,7 @@ }); if (vm.availableVariants.length !== 0) { - vm.availableVariants.sort((a, b) => { - if (a.language && b.language) { - if (a.language.name < b.language.name) { - return -1; - } - if (a.language.name > b.language.name) { - return 1; - } - } - if (a.segment && b.segment) { - if (a.segment < b.segment) { - return -1; - } - if (a.segment > b.segment) { - return 1; - } - } - return 0; - }); + vm.availableVariants = contentEditingHelper.getSortedVariantsAndSegments(vm.availableVariants); } $scope.model.disableSubmitButton = !canPublish(); diff --git a/src/Umbraco.Web.UI.Client/src/views/content/overlays/publish.html b/src/Umbraco.Web.UI.Client/src/views/content/overlays/publish.html index c3e0b90f5f..888b6ab0f2 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/overlays/publish.html +++ b/src/Umbraco.Web.UI.Client/src/views/content/overlays/publish.html @@ -30,7 +30,7 @@ - - + {{publishVariantSelectorForm.publishVariantSelector.errorMsg}} diff --git a/src/Umbraco.Web.UI.Client/src/views/content/overlays/publishdescendants.controller.js b/src/Umbraco.Web.UI.Client/src/views/content/overlays/publishdescendants.controller.js index f6fd6cb3cb..f9ad2eade8 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/overlays/publishdescendants.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/content/overlays/publishdescendants.controller.js @@ -1,7 +1,7 @@ (function () { "use strict"; - function PublishDescendantsController($scope, localizationService) { + function PublishDescendantsController($scope, localizationService, contentEditingHelper) { var vm = this; vm.includeUnpublished = $scope.model.includeUnpublished || false; @@ -38,25 +38,7 @@ if (vm.variants.length > 1) { - vm.displayVariants.sort((a, b) => { - if (a.language && b.language) { - if (a.language.name < b.language.name) { - return -1; - } - if (a.language.name > b.language.name) { - return 1; - } - } - if (a.segment && b.segment) { - if (a.segment < b.segment) { - return -1; - } - if (a.segment > b.segment) { - return 1; - } - } - return 0; - }); + vm.displayVariants = contentEditingHelper.getSortedVariantsAndSegments(vm.displayVariants); var active = vm.variants.find(v => v.active); diff --git a/src/Umbraco.Web.UI.Client/src/views/content/overlays/publishdescendants.html b/src/Umbraco.Web.UI.Client/src/views/content/overlays/publishdescendants.html index 61fd78a035..bb1d20c321 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/overlays/publishdescendants.html +++ b/src/Umbraco.Web.UI.Client/src/views/content/overlays/publishdescendants.html @@ -60,7 +60,7 @@ - - + {{publishVariantSelectorForm.publishVariantSelector.errorMsg}} diff --git a/src/Umbraco.Web.UI.Client/src/views/content/overlays/save.controller.js b/src/Umbraco.Web.UI.Client/src/views/content/overlays/save.controller.js index aa0d3797d4..bfde6afbe3 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/overlays/save.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/content/overlays/save.controller.js @@ -85,25 +85,7 @@ active.save = true; } - vm.availableVariants.sort((a, b) => { - if (a.language && b.language) { - if (a.language.name < b.language.name) { - return -1; - } - if (a.language.name > b.language.name) { - return 1; - } - } - if (a.segment && b.segment) { - if (a.segment < b.segment) { - return -1; - } - if (a.segment > b.segment) { - return 1; - } - } - return 0; - }); + vm.availableVariants = contentEditingHelper.getSortedVariantsAndSegments(vm.availableVariants); } else { //disable save button if we have nothing to save diff --git a/src/Umbraco.Web.UI.Client/src/views/content/overlays/save.html b/src/Umbraco.Web.UI.Client/src/views/content/overlays/save.html index 661dd4162e..fa9ab8c437 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/overlays/save.html +++ b/src/Umbraco.Web.UI.Client/src/views/content/overlays/save.html @@ -36,7 +36,7 @@ - - + {{saveVariantSelectorForm.saveVariantSelector.errorMsg}} diff --git a/src/Umbraco.Web.UI.Client/src/views/content/overlays/schedule.controller.js b/src/Umbraco.Web.UI.Client/src/views/content/overlays/schedule.controller.js index 8bf23ae6d5..2de526b503 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/overlays/schedule.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/content/overlays/schedule.controller.js @@ -1,7 +1,7 @@ (function () { "use strict"; - function ScheduleContentController($scope, $timeout, localizationService, dateHelper, userService) { + function ScheduleContentController($scope, $timeout, localizationService, dateHelper, userService, contentEditingHelper) { var vm = this; @@ -43,26 +43,7 @@ // Check for variants: if a node is invariant it will still have the default language in variants // so we have to check for length > 1 if (vm.variants.length > 1) { - - vm.displayVariants.sort((a, b) => { - if (a.language && b.language) { - if (a.language.name < b.language.name) { - return -1; - } - if (a.language.name > b.language.name) { - return 1; - } - } - if (a.segment && b.segment) { - if (a.segment < b.segment) { - return -1; - } - if (a.segment > b.segment) { - return 1; - } - } - return 0; - }); + vm.displayVariants = contentEditingHelper.getSortedVariantsAndSegments(vm.displayVariants); vm.variants.forEach(v => { if (v.active) { diff --git a/src/Umbraco.Web.UI.Client/src/views/content/overlays/schedule.html b/src/Umbraco.Web.UI.Client/src/views/content/overlays/schedule.html index e854f72717..563793862d 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/overlays/schedule.html +++ b/src/Umbraco.Web.UI.Client/src/views/content/overlays/schedule.html @@ -107,7 +107,7 @@ - - + - diff --git a/src/Umbraco.Web.UI.Client/src/views/content/overlays/sendtopublish.controller.js b/src/Umbraco.Web.UI.Client/src/views/content/overlays/sendtopublish.controller.js index d91d814886..dd9960d352 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/overlays/sendtopublish.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/content/overlays/sendtopublish.controller.js @@ -1,7 +1,7 @@ (function () { "use strict"; - function SendToPublishController($scope, localizationService) { + function SendToPublishController($scope, localizationService, contentEditingHelper) { var vm = this; vm.loading = true; @@ -27,25 +27,7 @@ if (vm.availableVariants.length !== 0) { - vm.availableVariants.sort((a, b) => { - if (a.language && b.language) { - if (a.language.name < b.language.name) { - return -1; - } - if (a.language.name > b.language.name) { - return 1; - } - } - if (a.segment && b.segment) { - if (a.segment < b.segment) { - return -1; - } - if (a.segment > b.segment) { - return 1; - } - } - return 0; - }); + vm.availableVariants = contentEditingHelper.getSortedVariantsAndSegments(vm.availableVariants); vm.availableVariants.forEach(v => { if(v.active) { diff --git a/src/Umbraco.Web.UI.Client/src/views/content/overlays/sendtopublish.html b/src/Umbraco.Web.UI.Client/src/views/content/overlays/sendtopublish.html index abf39c6542..8217da5752 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/overlays/sendtopublish.html +++ b/src/Umbraco.Web.UI.Client/src/views/content/overlays/sendtopublish.html @@ -32,7 +32,7 @@ - - + {{publishVariantSelectorForm.publishVariantSelector.errorMsg}} diff --git a/src/Umbraco.Web.UI.Client/src/views/content/overlays/unpublish.controller.js b/src/Umbraco.Web.UI.Client/src/views/content/overlays/unpublish.controller.js index 63c5b2da26..936ab3b104 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/overlays/unpublish.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/content/overlays/unpublish.controller.js @@ -1,7 +1,7 @@ (function () { "use strict"; - function UnpublishController($scope, localizationService) { + function UnpublishController($scope, localizationService, contentEditingHelper) { var vm = this; var autoSelectedVariants = []; @@ -27,25 +27,7 @@ // node has variants if (vm.variants.length !== 1) { - vm.unpublishableVariants.sort((a, b) => { - if (a.language && b.language) { - if (a.language.name < b.language.name) { - return -1; - } - if (a.language.name > b.language.name) { - return 1; - } - } - if (a.segment && b.segment) { - if (a.segment < b.segment) { - return -1; - } - if (a.segment > b.segment) { - return 1; - } - } - return 0; - }); + vm.unpublishableVariants = contentEditingHelper.getSortedVariantsAndSegments(vm.unpublishableVariants); var active = vm.variants.find(v => v.active); diff --git a/src/Umbraco.Web.UI.Client/src/views/content/overlays/unpublish.html b/src/Umbraco.Web.UI.Client/src/views/content/overlays/unpublish.html index 5ab32abf00..dc3862879a 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/overlays/unpublish.html +++ b/src/Umbraco.Web.UI.Client/src/views/content/overlays/unpublish.html @@ -36,7 +36,7 @@ - - + {{publishVariantSelectorForm.publishVariantSelector.errorMsg}} diff --git a/src/Umbraco.Web.UI.Client/src/views/content/recyclebin.html b/src/Umbraco.Web.UI.Client/src/views/content/recyclebin.html index 2981097656..410ad938f8 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/recyclebin.html +++ b/src/Umbraco.Web.UI.Client/src/views/content/recyclebin.html @@ -11,7 +11,8 @@
    - + +
    diff --git a/src/Umbraco.Web.UI.Client/src/views/contentblueprints/intro.html b/src/Umbraco.Web.UI.Client/src/views/contentblueprints/intro.html index 66695ace91..57c29d31d6 100644 --- a/src/Umbraco.Web.UI.Client/src/views/contentblueprints/intro.html +++ b/src/Umbraco.Web.UI.Client/src/views/contentblueprints/intro.html @@ -17,7 +17,7 @@ What are Content Templates?

    - Content Templates are pre-defined content that can be selected when creating a new content node. + Content Templates are pre-defined content that can be selected when creating a new content node.

    diff --git a/src/Umbraco.Web.UI.Client/src/views/dashboard/media/mediadashboardvideos.html b/src/Umbraco.Web.UI.Client/src/views/dashboard/media/mediadashboardvideos.html index 96d6a3f40a..670fae2f6e 100644 --- a/src/Umbraco.Web.UI.Client/src/views/dashboard/media/mediadashboardvideos.html +++ b/src/Umbraco.Web.UI.Client/src/views/dashboard/media/mediadashboardvideos.html @@ -1,5 +1,5 @@

    Hours of Umbraco training videos are only a click away

    -

    Want to master Umbraco? Spend a couple of minutes learning some best practices by watching one of these videos about using Umbraco. And visit umbraco.tv for even more Umbraco videos

    +

    Want to master Umbraco? Spend a couple of minutes learning some best practices by watching one of these videos about using Umbraco. And visit umbraco.tv for even more Umbraco videos

    - + {{key}} - {{val}} + {{values | umbCmsJoinArray:', '}} 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 de7beae0cc..84c138c180 100644 --- a/src/Umbraco.Web.UI.Client/src/views/dictionary/list.html +++ b/src/Umbraco.Web.UI.Client/src/views/dictionary/list.html @@ -32,19 +32,21 @@ - + - + - + diff --git a/src/Umbraco.Web.UI.Client/src/views/documenttypes/copy.html b/src/Umbraco.Web.UI.Client/src/views/documenttypes/copy.html index 8b81462ad5..824527be34 100644 --- a/src/Umbraco.Web.UI.Client/src/views/documenttypes/copy.html +++ b/src/Umbraco.Web.UI.Client/src/views/documenttypes/copy.html @@ -20,7 +20,7 @@
    {{source.name}} was copied underneath {{target.name}}
    - +
    diff --git a/src/Umbraco.Web.UI.Client/src/views/documenttypes/create.controller.js b/src/Umbraco.Web.UI.Client/src/views/documenttypes/create.controller.js index 71eee085ee..23173a404d 100644 --- a/src/Umbraco.Web.UI.Client/src/views/documenttypes/create.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/documenttypes/create.controller.js @@ -11,8 +11,7 @@ function DocumentTypesCreateController($scope, $location, navigationService, con $scope.model = { allowCreateFolder: $scope.currentNode.parentId === null || $scope.currentNode.nodeType === "container", folderName: "", - creatingFolder: false, - creatingDoctypeCollection: false + creatingFolder: false }; var disableTemplates = Umbraco.Sys.ServerVariables.features.disabledFeatures.disableTemplates; @@ -24,12 +23,6 @@ function DocumentTypesCreateController($scope, $location, navigationService, con $scope.model.creatingFolder = true; }; - $scope.showCreateDocTypeCollection = function () { - $scope.model.creatingDoctypeCollection = true; - $scope.model.collectionCreateTemplate = !$scope.model.disableTemplates; - $scope.model.collectionItemCreateTemplate = !$scope.model.disableTemplates; - }; - $scope.createContainer = function () { if (formHelper.submitForm({ scope: $scope, formCtrl: $scope.createFolderForm })) { @@ -58,55 +51,7 @@ function DocumentTypesCreateController($scope, $location, navigationService, con }); } - }; - - $scope.createCollection = function () { - - if (formHelper.submitForm({ scope: $scope, formCtrl: this.createDoctypeCollectionForm, statusMessage: "Creating Doctype Collection..." })) { - - // see if we can find matching icons - var collectionIcon = "icon-folders", collectionItemIcon = "icon-document"; - iconHelper.getIcons().then(function (icons) { - - for (var i = 0; i < icons.length; i++) { - // for matching we'll require a full match for collection, partial match for item - if (icons[i].substring(5) == $scope.model.collectionName.toLowerCase()) { - collectionIcon = icons[i]; - } else if (icons[i].substring(5).indexOf($scope.model.collectionItemName.toLowerCase()) > -1) { - collectionItemIcon = icons[i]; - } - } - - contentTypeResource.createCollection(node.id, $scope.model.collectionName, $scope.model.collectionCreateTemplate, $scope.model.collectionItemName, $scope.model.collectionItemCreateTemplate, collectionIcon, collectionItemIcon) - .then(function (collectionData) { - - navigationService.hideMenu(); - $location.search('create', null); - $location.search('notemplate', null); - - formHelper.resetForm({ scope: $scope, formCtrl: this.createDoctypeCollectionForm }); - - var section = appState.getSectionState("currentSection"); - - // redirect to the item id - $location.path("/" + section + "/documenttypes/edit/" + collectionData.containerId); - - }, function (err) { - - formHelper.resetForm({ scope: $scope, formCtrl: this.createDoctypeCollectionForm, hasErrors: true }); - $scope.error = err; - - //show any notifications - if (Utilities.isArray(err.data.notifications)) { - for (var i = 0; i < err.data.notifications.length; i++) { - notificationsService.showNotification(err.data.notifications[i]); - } - } - }); - }); - } - - }; + }; // Disabling logic for creating document type with template if disableTemplates is set to true if (!disableTemplates) { @@ -125,6 +70,22 @@ function DocumentTypesCreateController($scope, $location, navigationService, con navigationService.hideMenu(); }; + $scope.createComposition = function () { + $location.search('create', null); + $location.search('notemplate', null); + $location.search('iscomposition', null); + $location.path("/settings/documenttypes/edit/" + node.id).search("create", "true").search("notemplate", "true").search("iscomposition", "true"); + navigationService.hideMenu(); + }; + + $scope.createElement = function () { + $location.search('create', null); + $location.search('notemplate', null); + $location.search('iselement', null); + $location.path("/settings/documenttypes/edit/" + node.id).search("create", "true").search("notemplate", "true").search("iselement", "true"); + navigationService.hideMenu(); + }; + $scope.close = function() { const showMenu = true; navigationService.hideDialog(showMenu); diff --git a/src/Umbraco.Web.UI.Client/src/views/documenttypes/create.html b/src/Umbraco.Web.UI.Client/src/views/documenttypes/create.html index 4ebb96c09b..e370c30581 100644 --- a/src/Umbraco.Web.UI.Client/src/views/documenttypes/create.html +++ b/src/Umbraco.Web.UI.Client/src/views/documenttypes/create.html @@ -1,16 +1,15 @@
    -
  • -
  • -
  • - - - - New  - Member type - - - - -
    -
    -
    - - - - - - -
    + - -
    - - diff --git a/src/Umbraco.Web.UI.Client/src/views/prevalueeditors/treesourcetypepicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/prevalueeditors/treesourcetypepicker.controller.js index a87377c84b..dcc9add395 100644 --- a/src/Umbraco.Web.UI.Client/src/views/prevalueeditors/treesourcetypepicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/prevalueeditors/treesourcetypepicker.controller.js @@ -83,8 +83,8 @@ function TreeSourceTypePickerController($scope, contentTypeResource, mediaTypeRe $scope.model.value = _.pluck(vm.itemTypes, "alias").join(); angularHelper.getCurrentForm($scope).$setDirty(); } - - eventsService.on("treeSourceChanged", function (e, args) { + var evts = []; + evts.push(eventsService.on("treeSourceChanged", function (e, args) { // reset the model value if we changed node type (but not on the initial load) if (!!currentItemType && currentItemType !== args.value) { vm.itemTypes = []; @@ -92,6 +92,12 @@ function TreeSourceTypePickerController($scope, contentTypeResource, mediaTypeRe } currentItemType = args.value; init(); + })); + + $scope.$on('$destroy', function () { + for (var e in evts) { + eventsService.unsubscribe(evts[e]); + } }); } diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklistentryeditors/inlineblock/inlineblock.editor.less b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklistentryeditors/inlineblock/inlineblock.editor.less index 45a4c08598..12862d8f41 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklistentryeditors/inlineblock/inlineblock.editor.less +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklistentryeditors/inlineblock/inlineblock.editor.less @@ -45,18 +45,6 @@ } } - .umb-block-list__block.--active & { - border-color: @gray-8; - box-shadow: 0 0 2px 0px rgba(0, 0, 0, 0.05); - - > button { - > .caret { - transform: rotate(0deg); - } - } - } - - ng-form.ng-invalid-val-server-match-content > .umb-block-list__block > .umb-block-list__block--content > div > & { > button { color: @formErrorText; @@ -104,6 +92,22 @@ } } +.umb-block-list__block.--active { + border-color: @gray-8; + box-shadow: 0 0 2px 0px rgba(0, 0, 0, 0.05); + + > .umb-block-list__block--content { + > .umb-block-list__block--view { + > .blockelement-inlineblock-editor { + > button { + > .caret { + transform: rotate(0deg); + } + } + } + } + } +} .blockelement-inlineblock-editor__inner { border-top: 1px solid @gray-8; diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/umb-block-list-property-editor.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/umb-block-list-property-editor.html index 9726daf5e6..38959da6ba 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/umb-block-list-property-editor.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/umb-block-list-property-editor.html @@ -61,5 +61,4 @@ model="vm.blockTypePicker"> -
    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 56dbc68d90..613e6a8c6a 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 @@ -241,23 +241,24 @@ block.hideContentInOverlay = block.config.forceHideContentEditorInOverlay === true || inlineEditing === true; block.showSettings = block.config.settingsElementTypeKey != null; - block.showCopy = vm.supportCopy && block.config.contentElementTypeKey != null;// if we have content, otherwise it doesn't make sense to copy. + // If we have content, otherwise it doesn't make sense to copy. + block.showCopy = vm.supportCopy && block.config.contentElementTypeKey != null; // Index is set by umbblocklistblock component and kept up to date by it. block.index = 0; block.setParentForm = function (parentForm) { this._parentForm = parentForm; - } + }; block.activate = activateBlock.bind(null, block); block.edit = function () { var blockIndex = vm.layout.indexOf(this.layout); editBlock(this, false, blockIndex, this._parentForm); - } + }; block.editSettings = function () { var blockIndex = vm.layout.indexOf(this.layout); editBlock(this, true, blockIndex, this._parentForm); - } + }; block.requestDelete = requestDeleteBlock.bind(null, block); block.delete = deleteBlock.bind(null, block); block.copy = copyBlock.bind(null, block); @@ -265,7 +266,6 @@ return block; } - function addNewBlock(index, contentElementTypeKey) { // Create layout entry. (not added to property model jet.) @@ -292,7 +292,6 @@ vm.setBlockFocus(blockObject); return true; - } function deleteBlock(block) { @@ -316,7 +315,6 @@ }); modelObject.removeDataAndDestroyModel(block); - } function deleteAllBlocks() { @@ -409,6 +407,7 @@ } vm.showCreateDialog = showCreateDialog; + function showCreateDialog(createIndex, $event) { if (vm.blockTypePicker) { @@ -524,7 +523,7 @@ }; - var requestCopyAllBlocks = function() { + var requestCopyAllBlocks = function () { var aliases = []; @@ -534,7 +533,7 @@ aliases.push(entry.$block.content.contentTypeAlias); // No need to clone the data as its begin handled by the clipboardService. - return {"layout": entry.$block.layout, "data": entry.$block.data, "settingsData":entry.$block.settingsData} + return { "layout": entry.$block.layout, "data": entry.$block.data, "settingsData": entry.$block.settingsData } } ); @@ -543,9 +542,9 @@ var contentNodeName = "?"; var contentNodeIcon = null; - if(vm.umbVariantContent) { + if (vm.umbVariantContent) { contentNodeName = vm.umbVariantContent.editor.content.name; - if(vm.umbVariantContentEditors) { + if (vm.umbVariantContentEditors) { contentNodeIcon = vm.umbVariantContentEditors.content.icon.split(" ")[0]; } else if (vm.umbElementEditorContent) { contentNodeIcon = vm.umbElementEditorContent.model.documentType.icon.split(" ")[0]; @@ -555,13 +554,15 @@ contentNodeIcon = vm.umbElementEditorContent.model.documentType.icon.split(" ")[0]; } - localizationService.localize("clipboard_labelForArrayOfItemsFrom", [vm.model.label, contentNodeName]).then(function(localizedLabel) { + localizationService.localize("clipboard_labelForArrayOfItemsFrom", [vm.model.label, contentNodeName]).then(function (localizedLabel) { clipboardService.copyArray(clipboardService.TYPES.BLOCK, aliases, elementTypesToCopy, localizedLabel, contentNodeIcon || "icon-thumbnail-list", vm.model.id); }); - } + }; + function copyBlock(block) { clipboardService.copy(clipboardService.TYPES.BLOCK, block.content.contentTypeAlias, {"layout": block.layout, "data": block.data, "settingsData":block.settingsData}, block.label, block.content.icon, block.content.udi); } + function requestPasteFromClipboard(index, pasteEntry, pasteType) { if (pasteEntry === undefined) { @@ -599,7 +600,6 @@ vm.currentBlockInFocus = blockObject; return true; - } function requestDeleteBlock(block) { @@ -620,6 +620,7 @@ overlayService.confirmDelete(overlay); }); } + function requestDeleteAllBlocks() { localizationService.localizeMany(["content_nestedContentDeleteAllItems", "general_delete"]).then(function (data) { overlayService.confirmDelete({ @@ -647,7 +648,7 @@ requestDeleteBlock: requestDeleteBlock, deleteBlock: deleteBlock, openSettingsForBlock: openSettingsForBlock - } + }; vm.sortableOptions = { axis: "y", @@ -664,7 +665,6 @@ } }; - function onAmountOfBlocksChanged() { // enable/disable property actions diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/umbBlockListPropertyEditor.createButton.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/umbBlockListPropertyEditor.createButton.controller.js index 98d4f4ea3a..365ec809ac 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/umbBlockListPropertyEditor.createButton.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/umbBlockListPropertyEditor.createButton.controller.js @@ -9,9 +9,9 @@ var vm = this; vm.plusPosX = 0; - vm.onMouseMove = function($event) { + vm.onMouseMove = function ($event) { vm.plusPosX = $event.offsetX; - } + }; }); diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/umbblocklistblock.component.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/umbblocklistblock.component.js index 41d76475e4..1027b82e51 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/umbblocklistblock.component.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/umbblocklistblock.component.js @@ -59,12 +59,12 @@ -
    +
    `; $compile(shadowRoot)($scope); } else { - $element.append($compile('
    ')($scope)); + $element.append($compile('
    ')($scope)); } }; @@ -78,7 +78,7 @@ model.block.index = index; model.block.updateLabel(); } - } + }; } diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/changepassword/changepassword.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/changepassword/changepassword.controller.js index ab7f5c66e0..e80aad64f9 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/changepassword/changepassword.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/changepassword/changepassword.controller.js @@ -36,6 +36,9 @@ angular.module("umbraco").controller("Umbraco.PropertyEditors.ChangePasswordCont if (!$scope.model.config || $scope.model.config.minPasswordLength === undefined) { $scope.model.config.minPasswordLength = 0; } + if (!$scope.model.config || $scope.model.config.minNonAlphaNumericChars === undefined) { + $scope.model.config.minNonAlphaNumericChars = 0; + } //set the model defaults if (!Utilities.isObject($scope.model.value)) { diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/colorpicker/colorpicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/colorpicker/colorpicker.controller.js index c1b432b36e..886d051f4d 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/colorpicker/colorpicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/colorpicker/colorpicker.controller.js @@ -2,16 +2,16 @@ function ColorPickerController($scope, $timeout) { var vm = this; - //setup the default config + // setup the default config var config = { items: [], multiple: false }; - //map the user config - angular.extend(config, $scope.model.config); + // map the user config + Utilities.extend(config, $scope.model.config); - //map back to the model + // map back to the model $scope.model.config = config; $scope.isConfigured = $scope.model.config && $scope.model.config.items && _.keys($scope.model.config.items).length > 0; diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/colorpicker/multicolorpicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/colorpicker/multicolorpicker.controller.js index 6312ab9df6..967137b930 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/colorpicker/multicolorpicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/colorpicker/multicolorpicker.controller.js @@ -48,11 +48,15 @@ } }); } - - eventsService.on("toggleValue", function (e, args) { + var evts = []; + evts.push(eventsService.on("toggleValue", function (e, args) { vm.labelEnabled = args.value; + })); + $scope.$on('$destroy', function () { + for (var e in evts) { + eventsService.unsubscribe(evts[e]); + } }); - if (!Utilities.isArray($scope.model.value)) { //make an array from the dictionary var items = []; diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/datepicker/datepicker.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/datepicker/datepicker.html index 9501a6631b..f5ac69b9b8 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/datepicker/datepicker.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/datepicker/datepicker.html @@ -19,12 +19,12 @@ ng-required="model.validation.mandatory" val-server="value" class="datepickerinput" /> - -
    +

    {{mandatoryMessage}}

    {{datePickerForm.datepicker.errorMsg}}

    Invalid date

    diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/decimal/decimal.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/decimal/decimal.html index ac2fdabf3d..fb68a67c60 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/decimal/decimal.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/decimal/decimal.html @@ -9,10 +9,15 @@ aria-required="{{model.validation.mandatory}}" id="{{model.alias}}" val-server="value" - fix-number min="{{model.config.min}}" max="{{model.config.max}}" step="{{model.config.step}}" /> + min="{{model.config.min}}" + max="{{model.config.max}}" + step="{{model.config.step}}" + ng-step="model.config.step" + fix-number /> - + Not a number + Not a valid numeric step size {{decimalFieldForm.decimalField.errorMsg}} diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/email/email.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/email/email.html index 26ec22df8d..3ae03a2d7b 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/email/email.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/email/email.html @@ -1,6 +1,7 @@
    - -
    +

    {{mandatoryMessage}}

    Invalid email

    {{emailFieldForm.textbox.errorMsg}}

    diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/eyedropper/eyedropper.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/eyedropper/eyedropper.controller.js new file mode 100644 index 0000000000..82a0d18cab --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/eyedropper/eyedropper.controller.js @@ -0,0 +1,44 @@ +function EyeDropperColorPickerController($scope, angularHelper) { + + var vm = this; + + //setup the default config + var config = { + showAlpha: true, + showPalette: true, + allowEmpty: true + }; + + // map the user config + Utilities.extend(config, $scope.model.config); + + // map back to the model + $scope.model.config = config; + + vm.options = $scope.model.config; + + vm.color = $scope.model.value || null; + + vm.selectColor = function (color) { + angularHelper.safeApply($scope, function () { + vm.color = color ? color.toString() : null; + $scope.model.value = vm.color; + $scope.propertyForm.selectedColor.$setViewValue(vm.color); + }); + }; + + // Method required by the valPropertyValidator directive (returns true if the property editor has at least one color selected) + $scope.validateMandatory = function () { + var isValid = !$scope.model.validation.mandatory || ( + $scope.model.value != null + && $scope.model.value != ""); + + return { + isValid: isValid, + errorMsg: $scope.model.validation.mandatoryMessage || "Value cannot be empty", + errorKey: "required" + }; + }; +} + +angular.module("umbraco").controller("Umbraco.PropertyEditors.EyeDropperColorPickerController", EyeDropperColorPickerController); diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/eyedropper/eyedropper.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/eyedropper/eyedropper.html new file mode 100644 index 0000000000..821f50f92b --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/eyedropper/eyedropper.html @@ -0,0 +1,11 @@ +
    + + + + + + +
    diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/dialogs/config.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/dialogs/config.controller.js index 30c89d5f2e..7330d69b2f 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/dialogs/config.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/dialogs/config.controller.js @@ -16,7 +16,25 @@ function ConfigController($scope) { $scope.model.close(); } } - + + vm.showEmptyState = false; + vm.showConfig = false; + vm.showStyles = false; + + $scope.$watchCollection('model.config', onWatch); + $scope.$watchCollection('model.styles', onWatch); + + function onWatch() { + + vm.showConfig = $scope.model.config && + ($scope.model.config.length > 0 || Object.keys($scope.model.config).length > 0); + vm.showStyles = $scope.model.styles && + ($scope.model.styles.length > 0 || Object.keys($scope.model.styles).length > 0); + + vm.showEmptyState = vm.showConfig === false && vm.showStyles === false; + + } + } angular.module("umbraco").controller("Umbraco.PropertyEditors.GridPrevalueEditor.ConfigController", ConfigController); diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/dialogs/config.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/dialogs/config.html index a7cabd1636..88585b76fa 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/dialogs/config.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/dialogs/config.html @@ -13,11 +13,11 @@ - + No further configuration available - +
    @@ -29,7 +29,7 @@ - +
    @@ -52,7 +52,7 @@ shortcut="esc" action="vm.close()"> - { $scope.model.config.startNodeId = userData.startMediaIds.length !== 1 ? -1 : userData.startMediaIds[0]; $scope.model.config.startNodeIsVirtual = userData.startMediaIds.length !== 1; }); } } - - $scope.setImage = function(){ - var startNodeId = $scope.model.config && $scope.model.config.startNodeId ? $scope.model.config.startNodeId : undefined; - var startNodeIsVirtual = startNodeId ? $scope.model.config.startNodeIsVirtual : undefined; - var value = $scope.control.value; - var target = value - ? { - udi: value.udi, - url: value.image, - image: value.image, - focalPoint: value.focalPoint, - coordinates: value.coordinates - } - : null; + + $scope.setImage = function() { + var startNodeId = $scope.model.config && $scope.model.config.startNodeId ? $scope.model.config.startNodeId : null; + var mediaPicker = { startNodeId: startNodeId, - startNodeIsVirtual: startNodeIsVirtual, - cropSize: $scope.control.editor.config && $scope.control.editor.config.size ? $scope.control.editor.config.size : undefined, + startNodeIsVirtual: startNodeId ? $scope.model.config.startNodeIsVirtual : null, + cropSize: $scope.control.editor.config && $scope.control.editor.config.size ? $scope.control.editor.config.size : null, showDetails: true, disableFolderSelect: true, onlyImages: true, dataTypeKey: $scope.model.dataTypeKey, - currentTarget: target, - submit: function(model) { - var selectedImage = model.selection[0]; - - $scope.control.value = { - focalPoint: selectedImage.focalPoint, - coordinates: selectedImage.coordinates, - id: selectedImage.id, - udi: selectedImage.udi, - image: selectedImage.image, - caption: selectedImage.altText - }; - + submit: model => { + updateControlValue(model.selection[0]); editorService.close(); }, - close: function() { - editorService.close(); - } - } - + close: () => editorService.close() + }; + editorService.mediaPicker(mediaPicker); }; - - $scope.$watch('control.value', function(newValue, oldValue) { - if(Utilities.equals(newValue, oldValue)){ - return; // simply skip that - } - - $scope.thumbnailUrl = getThumbnailUrl(); - }, true); - + + $scope.editImage = function() { + + const mediaCropDetailsConfig = { + size: 'small', + target: $scope.control.value, + submit: model => { + updateControlValue(model.target); + editorService.close(); + }, + close: () => editorService.close() + }; + + localizationService.localize('defaultdialogs_editSelectedMedia').then(value => { + mediaCropDetailsConfig.title = value; + editorService.mediaCropDetails(mediaCropDetailsConfig); + }); + } + + /** + * + */ function getThumbnailUrl() { - if($scope.control.value && $scope.control.value.image) { + if ($scope.control.value && $scope.control.value.image) { var url = $scope.control.value.image; - if($scope.control.editor.config && $scope.control.editor.config.size){ + if ($scope.control.editor.config && $scope.control.editor.config.size){ if ($scope.control.value.coordinates) { // New way, crop by percent must come before width/height. var coords = $scope.control.value.coordinates; - url += "?crop=" + coords.x1 + "," + coords.y1 + "," + coords.x2 + "," + coords.y2 + "&cropmode=percentage"; + url += `?crop=${coords.x1},${coords.y1},${coords.x2},${coords.y2}&cropmode=percentage`; } else { // Here in order not to break existing content where focalPoint were used. // For some reason width/height have to come first when mode=crop. if ($scope.control.value.focalPoint) { - url += "?center=" + $scope.control.value.focalPoint.top + "," + $scope.control.value.focalPoint.left; - url += "&mode=crop"; + url += `?center=${$scope.control.value.focalPoint.top},${$scope.control.value.focalPoint.left}`; + url += '&mode=crop'; } else { // Prevent black padding and no crop when focal point not set / changed from default - url += "?center=0.5,0.5&mode=crop"; + url += '?center=0.5,0.5&mode=crop'; } } - url += "&width=" + $scope.control.editor.config.size.width; - url += "&height=" + $scope.control.editor.config.size.height; - url += "&animationprocessmode=first"; + url += '&width=' + $scope.control.editor.config.size.width; + url += '&height=' + $scope.control.editor.config.size.height; + url += '&animationprocessmode=first'; } // set default size if no crop present (moved from the view) - if (url.indexOf('?') == -1) + if (url.includes('?') === false) { - url += "?width=800&upscale=false&animationprocessmode=false" + url += '?width=800&upscale=false&animationprocessmode=false' } + return url; } - - return null; - }; -}); + return null; + } + + /** + * + * @param {object} selectedImage + */ + function updateControlValue(selectedImage) { + // we could apply selectedImage directly to $scope.control.value, + // but this allows excluding fields in future if needed + $scope.control.value = { + focalPoint: selectedImage.focalPoint, + coordinates: selectedImage.coordinates, + id: selectedImage.id, + udi: selectedImage.udi, + image: selectedImage.image, + caption: selectedImage.caption, + altText: selectedImage.altText + }; + $scope.thumbnailUrl = getThumbnailUrl(); + } + }); diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/media.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/media.html index 2ab42807fd..2f73dc82ec 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/media.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/media.html @@ -6,14 +6,23 @@ -
    - - - -
    +
    + + +
    + + + + +
    +
    diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/grid.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/grid.controller.js index 50146a4c36..15f5ceaa88 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/grid.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/grid.controller.js @@ -141,9 +141,9 @@ angular.module("umbraco") over: function (event, ui) { var area = event.target.getScope_HackForSortable().area; - var allowedEditors = area.allowed; + var allowedEditors = area.$allowedEditors.map(e => e.alias); - if (($.inArray(ui.item[0].getScope_HackForSortable().control.editor.alias, allowedEditors) < 0 && allowedEditors) || + if (($.inArray(ui.item[0].getScope_HackForSortable().control.editor.alias, allowedEditors) < 0) || (startingArea != area && area.maxItems != '' && area.maxItems > 0 && area.maxItems < area.controls.length + 1)) { $scope.$apply(function () { diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/grid.prevalues.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/grid.prevalues.html index 9bf32675e3..f1f1a90ef4 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/grid.prevalues.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/grid.prevalues.html @@ -117,11 +117,10 @@
  • - + {{configValue.label}}
  • @@ -145,12 +144,10 @@
  • - - + {{style.label}}
  • diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/imagecropper/imagecropper.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/imagecropper/imagecropper.html index 91abc3be87..84ddf7ee3b 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/imagecropper/imagecropper.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/imagecropper/imagecropper.html @@ -28,9 +28,9 @@
    - - - + + +
    diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/integer/integer.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/integer/integer.html index 5c10790400..24a8c33696 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/integer/integer.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/integer/integer.html @@ -9,10 +9,15 @@ aria-required="{{model.validation.mandatory}}" id="{{model.alias}}" val-server="value" - fix-number min="{{model.config.min}}" max="{{model.config.max}}" step="{{model.config.step}}" /> + min="{{model.config.min}}" + max="{{model.config.max}}" + step="{{model.config.step}}" + ng-step="model.config.step" + fix-number /> - + Not a number + Not a valid numeric step size {{integerFieldForm.integerField.errorMsg}} diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/includeproperties.prevalues.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/includeproperties.prevalues.html index 42597f0c82..7d863f6730 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/includeproperties.prevalues.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/includeproperties.prevalues.html @@ -27,7 +27,7 @@ - +
    @@ -54,7 +54,7 @@ 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 4527458d16..295345a827 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 @@ -6,20 +6,20 @@
    - +
    - -
    - -
    -
    @@ -32,12 +32,12 @@
    -
    +
    @@ -53,13 +53,12 @@ - @@ -70,7 +69,7 @@
  • Be a part of the community

    The Umbraco community is the best of its kind, be sure to visit, and if you have any questions, we're sure that you can get your answers from the community.

    - our.Umbraco → + our.Umbraco →
    diff --git a/src/Umbraco.Web.UI/config/umbracoSettings.Release.config b/src/Umbraco.Web.UI/config/umbracoSettings.Release.config index c20ef3038b..5ce2c24d41 100644 --- a/src/Umbraco.Web.UI/config/umbracoSettings.Release.config +++ b/src/Umbraco.Web.UI/config/umbracoSettings.Release.config @@ -183,7 +183,7 @@ throw - ashx,aspx,ascx,config,cshtml,vbhtml,asmx,air,axd,swf,xml,xhtml,html,htm,php,htaccess + ashx,aspx,ascx,config,cshtml,vbhtml,asmx,air,axd,swf,xml,xhtml,html,htm,php,htaccess,xamlx assets/img/login.jpg diff --git a/src/Umbraco.Web.UI/web.Template.config b/src/Umbraco.Web.UI/web.Template.config index 03f462fb9e..ae141e5408 100644 --- a/src/Umbraco.Web.UI/web.Template.config +++ b/src/Umbraco.Web.UI/web.Template.config @@ -37,6 +37,7 @@ + diff --git a/src/Umbraco.Web/AspNetHttpContextAccessor.cs b/src/Umbraco.Web/AspNetHttpContextAccessor.cs index babd8dfcfa..147c37fe8c 100644 --- a/src/Umbraco.Web/AspNetHttpContextAccessor.cs +++ b/src/Umbraco.Web/AspNetHttpContextAccessor.cs @@ -7,14 +7,8 @@ namespace Umbraco.Web { public HttpContext HttpContext { - get - { - return HttpContext.Current; - } - set - { - throw new NotSupportedException(); - } + get => HttpContext.Current; + set => throw new NotSupportedException(); } } } diff --git a/src/Umbraco.Web/Cache/DistributedCacheBinder.cs b/src/Umbraco.Web/Cache/DistributedCacheBinder.cs index 5f8d77f3b2..3ee24a23bf 100644 --- a/src/Umbraco.Web/Cache/DistributedCacheBinder.cs +++ b/src/Umbraco.Web/Cache/DistributedCacheBinder.cs @@ -72,7 +72,7 @@ namespace Umbraco.Web.Cache { // TODO: should this be fatal (ie, an exception)? var name = e.Sender.GetType().Name + "_" + e.EventName; - _logger.Warn("Dropping event {EventName} because no corresponding handler was found.", name); + _logger.Warn("Dropping event {EventName} because no corresponding handler was found.", name); continue; } diff --git a/src/Umbraco.Web/Cache/DistributedCacheExtensions.cs b/src/Umbraco.Web/Cache/DistributedCacheExtensions.cs index f360d37d03..92a9dd6e98 100644 --- a/src/Umbraco.Web/Cache/DistributedCacheExtensions.cs +++ b/src/Umbraco.Web/Cache/DistributedCacheExtensions.cs @@ -135,7 +135,10 @@ namespace Umbraco.Web.Cache public static void RemoveMemberCache(this DistributedCache dc, params IMember[] members) { if (members.Length == 0) return; - dc.RefreshByPayload(MemberCacheRefresher.UniqueId, members.Select(x => new MemberCacheRefresher.JsonPayload(x.Id, x.Username))); + dc.RefreshByPayload(MemberCacheRefresher.UniqueId, members.Select(x => new MemberCacheRefresher.JsonPayload(x.Id, x.Username) + { + Removed = true + })); } #endregion diff --git a/src/Umbraco.Web/Cache/MemberCacheRefresher.cs b/src/Umbraco.Web/Cache/MemberCacheRefresher.cs index 736a858af3..48ae40ce3b 100644 --- a/src/Umbraco.Web/Cache/MemberCacheRefresher.cs +++ b/src/Umbraco.Web/Cache/MemberCacheRefresher.cs @@ -33,6 +33,10 @@ namespace Umbraco.Web.Cache public int Id { get; } public string Username { get; } + // TODO: In netcore change this to be get only and adjust the ctor. We cannot do that now since that + // is a breaking change due to only having a single jsonconstructor allowed. + public bool Removed { get; set; } + } #region Define diff --git a/src/Umbraco.Web/Cache/UserCacheRefresher.cs b/src/Umbraco.Web/Cache/UserCacheRefresher.cs index 922a9df385..ce2cbbf754 100644 --- a/src/Umbraco.Web/Cache/UserCacheRefresher.cs +++ b/src/Umbraco.Web/Cache/UserCacheRefresher.cs @@ -42,7 +42,14 @@ namespace Umbraco.Web.Cache { var userCache = AppCaches.IsolatedCaches.Get(); if (userCache) + { userCache.Result.Clear(RepositoryCacheKeys.GetKey(id)); + userCache.Result.ClearByKey(CacheKeys.UserContentStartNodePathsPrefix + id); + userCache.Result.ClearByKey(CacheKeys.UserMediaStartNodePathsPrefix + id); + userCache.Result.ClearByKey(CacheKeys.UserAllContentStartNodesPrefix + id); + userCache.Result.ClearByKey(CacheKeys.UserAllMediaStartNodesPrefix + id); + } + base.Remove(id); } diff --git a/src/Umbraco.Web/ContentApps/ContentAppFactoryCollection.cs b/src/Umbraco.Web/ContentApps/ContentAppFactoryCollection.cs index 07987aea3e..da587f672a 100644 --- a/src/Umbraco.Web/ContentApps/ContentAppFactoryCollection.cs +++ b/src/Umbraco.Web/ContentApps/ContentAppFactoryCollection.cs @@ -50,7 +50,7 @@ namespace Umbraco.Web.ContentApps // dying is not user-friendly, so let's write to log instead, and wish people read logs... //throw new InvalidOperationException($"Duplicate content app aliases found: {string.Join(",", dups)}"); - _logger.Warn("Duplicate content app aliases found: {DuplicateAliases}", string.Join(",", dups)); + _logger.Warn("Duplicate content app aliases found: {DuplicateAliases}", string.Join(",", dups)); } return apps; diff --git a/src/Umbraco.Web/Dashboards/ContentDashboard.cs b/src/Umbraco.Web/Dashboards/ContentDashboard.cs index 0cd96f738c..260eb8baf9 100644 --- a/src/Umbraco.Web/Dashboards/ContentDashboard.cs +++ b/src/Umbraco.Web/Dashboards/ContentDashboard.cs @@ -1,15 +1,21 @@ -using Umbraco.Core; +using System.Collections.Generic; +using Umbraco.Core; using Umbraco.Core.Composing; using Umbraco.Core.Dashboards; +using Umbraco.Core.Services; namespace Umbraco.Web.Dashboards { [Weight(10)] public class ContentDashboard : IDashboard { + private readonly IContentDashboardSettings _dashboardSettings; + private readonly IUserService _userService; + private IAccessRule[] _accessRulesFromConfig; + public string Alias => "contentIntro"; - public string[] Sections => new [] { "content" }; + public string[] Sections => new[] { "content" }; public string View => "views/dashboard/default/startupdashboardintro.html"; @@ -17,13 +23,54 @@ namespace Umbraco.Web.Dashboards { get { - var rules = new IAccessRule[] + var rules = AccessRulesFromConfig; + + if (rules.Length == 0) { - new AccessRule {Type = AccessRuleType.Deny, Value = Constants.Security.TranslatorGroupAlias}, - new AccessRule {Type = AccessRuleType.Grant, Value = Constants.Security.AdminGroupAlias} - }; + rules = new IAccessRule[] + { + new AccessRule {Type = AccessRuleType.Deny, Value = Constants.Security.TranslatorGroupAlias}, + new AccessRule {Type = AccessRuleType.Grant, Value = Constants.Security.AdminGroupAlias} + }; + } + return rules; } } + + private IAccessRule[] AccessRulesFromConfig + { + get + { + if (_accessRulesFromConfig is null) + { + var rules = new List(); + + if (_dashboardSettings.AllowContentDashboardAccessToAllUsers) + { + var allUserGroups = _userService.GetAllUserGroups(); + + foreach (var userGroup in allUserGroups) + { + rules.Add(new AccessRule + { + Type = AccessRuleType.Grant, + Value = userGroup.Alias + }); + } + } + + _accessRulesFromConfig = rules.ToArray(); + } + + return _accessRulesFromConfig; + } + } + + public ContentDashboard(IContentDashboardSettings dashboardSettings, IUserService userService) + { + _dashboardSettings = dashboardSettings; + _userService = userService; + } } } diff --git a/src/Umbraco.Web/Editors/AuthenticationController.cs b/src/Umbraco.Web/Editors/AuthenticationController.cs index d5f4ef9fe8..30294b7581 100644 --- a/src/Umbraco.Web/Editors/AuthenticationController.cs +++ b/src/Umbraco.Web/Editors/AuthenticationController.cs @@ -433,19 +433,19 @@ namespace Umbraco.Web.Editors var lockedOut = await UserManager.IsLockedOutAsync(model.UserId); if (lockedOut) { - Logger.Info("User {UserId} is currently locked out, unlocking and resetting AccessFailedCount", model.UserId); + Logger.Info("User {UserId} is currently locked out, unlocking and resetting AccessFailedCount", model.UserId); //// var user = await UserManager.FindByIdAsync(model.UserId); var unlockResult = await UserManager.SetLockoutEndDateAsync(model.UserId, DateTimeOffset.Now); if (unlockResult.Succeeded == false) { - Logger.Warn("Could not unlock for user {UserId} - error {UnlockError}", model.UserId, unlockResult.Errors.First()); + Logger.Warn("Could not unlock for user {UserId} - error {UnlockError}", model.UserId, unlockResult.Errors.First()); } var resetAccessFailedCountResult = await UserManager.ResetAccessFailedCountAsync(model.UserId); if (resetAccessFailedCountResult.Succeeded == false) { - Logger.Warn("Could not reset access failed count {UserId} - error {UnlockError}", model.UserId, unlockResult.Errors.First()); + Logger.Warn("Could not reset access failed count {UserId} - error {UnlockError}", model.UserId, unlockResult.Errors.First()); } } @@ -499,7 +499,7 @@ namespace Umbraco.Web.Editors Core.Constants.Security.BackOfficeAuthenticationType, Core.Constants.Security.BackOfficeExternalAuthenticationType); - Logger.Info("User {UserName} from IP address {RemoteIpAddress} has logged out", User.Identity == null ? "UNKNOWN" : User.Identity.Name, owinContext.Request.RemoteIpAddress); + Logger.Info("User {UserName} from IP address {RemoteIpAddress} has logged out", User.Identity == null ? "UNKNOWN" : User.Identity.Name, owinContext.Request.RemoteIpAddress); if (UserManager != null) { diff --git a/src/Umbraco.Web/Editors/BackOfficeController.cs b/src/Umbraco.Web/Editors/BackOfficeController.cs index 5da0cd0cc3..baba9da894 100644 --- a/src/Umbraco.Web/Editors/BackOfficeController.cs +++ b/src/Umbraco.Web/Editors/BackOfficeController.cs @@ -1,4 +1,8 @@ -using System; +using Microsoft.AspNet.Identity; +using Microsoft.AspNet.Identity.Owin; +using Microsoft.Owin.Security; +using Newtonsoft.Json; +using System; using System.Collections.Generic; using System.Globalization; using System.Linq; @@ -6,24 +10,19 @@ using System.Threading; using System.Threading.Tasks; using System.Web.Mvc; using System.Web.UI; -using Microsoft.AspNet.Identity; -using Microsoft.AspNet.Identity.Owin; -using Microsoft.Owin.Security; -using Newtonsoft.Json; using Umbraco.Core; using Umbraco.Core.Cache; using Umbraco.Core.Configuration; using Umbraco.Core.Logging; using Umbraco.Core.Manifest; using Umbraco.Core.Models.Identity; -using Umbraco.Web.Models; -using Umbraco.Web.Mvc; using Umbraco.Core.Services; using Umbraco.Web.Composing; using Umbraco.Web.Features; using Umbraco.Web.JavaScript; +using Umbraco.Web.Models; +using Umbraco.Web.Mvc; using Umbraco.Web.Security; -using Umbraco.Web.Services; using Constants = Umbraco.Core.Constants; using JArray = Newtonsoft.Json.Linq.JArray; @@ -40,11 +39,9 @@ namespace Umbraco.Web.Editors private readonly ManifestParser _manifestParser; private readonly UmbracoFeatures _features; private readonly IRuntimeState _runtimeState; - private readonly IIconService _iconService; private BackOfficeUserManager _userManager; private BackOfficeSignInManager _signInManager; - [Obsolete("Use the constructor that injects IIconService.")] public BackOfficeController( ManifestParser manifestParser, UmbracoFeatures features, @@ -55,37 +52,11 @@ namespace Umbraco.Web.Editors IProfilingLogger profilingLogger, IRuntimeState runtimeState, UmbracoHelper umbracoHelper) - : this(manifestParser, - features, - globalSettings, - umbracoContextAccessor, - services, - appCaches, - profilingLogger, - runtimeState, - umbracoHelper, - Current.IconService) - { - - } - - public BackOfficeController( - ManifestParser manifestParser, - UmbracoFeatures features, - IGlobalSettings globalSettings, - IUmbracoContextAccessor umbracoContextAccessor, - ServiceContext services, - AppCaches appCaches, - IProfilingLogger profilingLogger, - IRuntimeState runtimeState, - UmbracoHelper umbracoHelper, - IIconService iconService) : base(globalSettings, umbracoContextAccessor, services, appCaches, profilingLogger, umbracoHelper) { _manifestParser = manifestParser; _features = features; _runtimeState = runtimeState; - _iconService = iconService; } protected BackOfficeSignInManager SignInManager => _signInManager ?? (_signInManager = OwinContext.GetBackOfficeSignInManager()); @@ -100,7 +71,7 @@ namespace Umbraco.Web.Editors /// public async Task Default() { - var backofficeModel = new BackOfficeModel(_features, GlobalSettings, _iconService); + var backofficeModel = new BackOfficeModel(_features, GlobalSettings); return await RenderDefaultOrProcessExternalLoginAsync( () => View(GlobalSettings.Path.EnsureEndsWith('/') + "Views/Default.cshtml", backofficeModel), () => View(GlobalSettings.Path.EnsureEndsWith('/') + "Views/Default.cshtml", backofficeModel)); @@ -129,7 +100,7 @@ namespace Umbraco.Web.Editors if (parts.Length != 2) { - Logger.Warn("VerifyUser endpoint reached with invalid token: {Invite}", invite); + Logger.Warn("VerifyUser endpoint reached with invalid token: {Invite}", invite); return RedirectToAction("Default"); } @@ -138,7 +109,7 @@ namespace Umbraco.Web.Editors var decoded = token.FromUrlBase64(); if (decoded.IsNullOrWhiteSpace()) { - Logger.Warn("VerifyUser endpoint reached with invalid token: {Invite}", invite); + Logger.Warn("VerifyUser endpoint reached with invalid token: {Invite}", invite); return RedirectToAction("Default"); } @@ -146,14 +117,14 @@ namespace Umbraco.Web.Editors int intId; if (int.TryParse(id, out intId) == false) { - Logger.Warn("VerifyUser endpoint reached with invalid token: {Invite}", invite); + Logger.Warn("VerifyUser endpoint reached with invalid token: {Invite}", invite); return RedirectToAction("Default"); } var identityUser = await UserManager.FindByIdAsync(intId); if (identityUser == null) { - Logger.Warn("VerifyUser endpoint reached with non existing user: {UserId}", id); + Logger.Warn("VerifyUser endpoint reached with non existing user: {UserId}", id); return RedirectToAction("Default"); } @@ -161,7 +132,7 @@ namespace Umbraco.Web.Editors if (result.Succeeded == false) { - Logger.Warn("Could not verify email, Error: {Errors}, Token: {Invite}", string.Join(",", result.Errors), invite); + Logger.Warn("Could not verify email, Error: {Errors}, Token: {Invite}", string.Join(",", result.Errors), invite); return new RedirectResult(Url.Action("Default") + "#/login/false?invite=3"); } @@ -186,7 +157,7 @@ namespace Umbraco.Web.Editors { return await RenderDefaultOrProcessExternalLoginAsync( //The default view to render when there is no external login info or errors - () => View(GlobalSettings.Path.EnsureEndsWith('/') + "Views/AuthorizeUpgrade.cshtml", new BackOfficeModel(_features, GlobalSettings, _iconService)), + () => View(GlobalSettings.Path.EnsureEndsWith('/') + "Views/AuthorizeUpgrade.cshtml", new BackOfficeModel(_features, GlobalSettings)), //The ActionResult to perform if external login is successful () => Redirect("/")); } @@ -233,7 +204,7 @@ namespace Umbraco.Web.Editors .ToDictionary(pv => pv.Key, pv => pv.ToDictionary(pve => pve.valueAlias, pve => pve.value)); - return new JsonNetResult { Data = nestedDictionary, Formatting = Formatting.None }; + return new JsonNetResult(JsonNetResult.DefaultJsonSerializerSettings) { Data = nestedDictionary, Formatting = Formatting.None }; } /// @@ -280,7 +251,7 @@ namespace Umbraco.Web.Editors GetAssetList, new TimeSpan(0, 2, 0)); - return new JsonNetResult { Data = result, Formatting = Formatting.None }; + return new JsonNetResult(JsonNetResult.DefaultJsonSerializerSettings) { Data = result, Formatting = Formatting.None }; } [UmbracoAuthorize(Order = 0)] @@ -288,7 +259,7 @@ namespace Umbraco.Web.Editors public JsonNetResult GetGridConfig() { var gridConfig = Current.Configs.Grids(); - return new JsonNetResult { Data = gridConfig.EditorsConfig.Editors, Formatting = Formatting.None }; + return new JsonNetResult(JsonNetResult.DefaultJsonSerializerSettings) { Data = gridConfig.EditorsConfig.Editors, Formatting = Formatting.None }; } @@ -358,6 +329,7 @@ namespace Umbraco.Web.Editors return RedirectToLocal(Url.Action("Default", "BackOffice")); } + [UmbracoAuthorize] [HttpGet] public async Task ExternalLinkLoginCallback() { @@ -440,7 +412,7 @@ namespace Umbraco.Web.Editors var authType = OwinContext.Authentication.GetExternalAuthenticationTypes().FirstOrDefault(x => x.AuthenticationType == loginInfo.Login.LoginProvider); if (authType == null) { - Logger.Warn("Could not find external authentication provider registered: {LoginProvider}", loginInfo.Login.LoginProvider); + Logger.Warn("Could not find external authentication provider registered: {LoginProvider}", loginInfo.Login.LoginProvider); } else { @@ -457,7 +429,7 @@ namespace Umbraco.Web.Editors shouldSignIn = autoLinkOptions.OnExternalLogin(user, loginInfo); if (shouldSignIn == false) { - Logger.Warn("The AutoLinkOptions of the external authentication provider '{LoginProvider}' have refused the login based on the OnExternalLogin method. Affected user id: '{UserId}'", loginInfo.Login.LoginProvider, user.Id); + Logger.Warn("The AutoLinkOptions of the external authentication provider '{LoginProvider}' have refused the login based on the OnExternalLogin method. Affected user id: '{UserId}'", loginInfo.Login.LoginProvider, user.Id); } } diff --git a/src/Umbraco.Web/Editors/BackOfficeModel.cs b/src/Umbraco.Web/Editors/BackOfficeModel.cs index cbdafd2e94..d0d2e324f3 100644 --- a/src/Umbraco.Web/Editors/BackOfficeModel.cs +++ b/src/Umbraco.Web/Editors/BackOfficeModel.cs @@ -1,32 +1,19 @@ using System; using Umbraco.Core.Configuration; -using Umbraco.Core.Services; -using Umbraco.Web.Composing; using Umbraco.Web.Features; namespace Umbraco.Web.Editors { - public class BackOfficeModel { - - [Obsolete("Use the overload that injects IIconService.")] - public BackOfficeModel(UmbracoFeatures features, IGlobalSettings globalSettings) : this(features, globalSettings, Current.IconService) - { - - } - public BackOfficeModel(UmbracoFeatures features, IGlobalSettings globalSettings, IIconService iconService) + public BackOfficeModel(UmbracoFeatures features, IGlobalSettings globalSettings) { Features = features; GlobalSettings = globalSettings; - IconCheckData = iconService.GetIcon("icon-check")?.SvgString; - IconDeleteData = iconService.GetIcon("icon-delete")?.SvgString; } public UmbracoFeatures Features { get; } public IGlobalSettings GlobalSettings { get; } - public string IconCheckData { get; } - public string IconDeleteData { get; } } } diff --git a/src/Umbraco.Web/Editors/BackOfficePreviewModel.cs b/src/Umbraco.Web/Editors/BackOfficePreviewModel.cs index cc7356b687..6ace8e7198 100644 --- a/src/Umbraco.Web/Editors/BackOfficePreviewModel.cs +++ b/src/Umbraco.Web/Editors/BackOfficePreviewModel.cs @@ -1,9 +1,6 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using Umbraco.Core.Configuration; using Umbraco.Core.Models; -using Umbraco.Core.Services; -using Umbraco.Web.Composing; using Umbraco.Web.Features; namespace Umbraco.Web.Editors @@ -13,21 +10,11 @@ namespace Umbraco.Web.Editors private readonly UmbracoFeatures _features; public IEnumerable Languages { get; } - [Obsolete("Use the overload that injects IIconService.")] public BackOfficePreviewModel( UmbracoFeatures features, IGlobalSettings globalSettings, IEnumerable languages) - : this(features, globalSettings, languages, Current.IconService) - { - } - - public BackOfficePreviewModel( - UmbracoFeatures features, - IGlobalSettings globalSettings, - IEnumerable languages, - IIconService iconService) - : base(features, globalSettings, iconService) + : base(features, globalSettings) { _features = features; Languages = languages; diff --git a/src/Umbraco.Web/Editors/BackOfficeServerVariables.cs b/src/Umbraco.Web/Editors/BackOfficeServerVariables.cs index dd4dd67681..6ec9ac4f90 100644 --- a/src/Umbraco.Web/Editors/BackOfficeServerVariables.cs +++ b/src/Umbraco.Web/Editors/BackOfficeServerVariables.cs @@ -1,4 +1,6 @@ -using System; +using ClientDependency.Core.Config; +using Microsoft.Owin; +using System; using System.Collections; using System.Collections.Generic; using System.Configuration; @@ -7,15 +9,10 @@ using System.Runtime.Serialization; using System.Web; using System.Web.Configuration; using System.Web.Mvc; -using ClientDependency.Core.Config; -using Microsoft.Owin; -using Microsoft.Owin.Security; using Umbraco.Core; using Umbraco.Core.Composing; using Umbraco.Core.Configuration; -using Umbraco.Core.Configuration.UmbracoSettings; using Umbraco.Core.IO; -using Umbraco.Web.Controllers; using Umbraco.Web.Features; using Umbraco.Web.HealthCheck; using Umbraco.Web.Models.ContentEditing; @@ -60,7 +57,7 @@ namespace Umbraco.Web.Editors var keepOnlyKeys = new Dictionary { {"umbracoUrls", new[] {"authenticationApiBaseUrl", "serverVarsJs", "externalLoginsUrl", "currentUserApiBaseUrl", "iconApiBaseUrl"}}, - {"umbracoSettings", new[] {"allowPasswordReset", "imageFileTypes", "maxFileSize", "loginBackgroundImage", "loginLogoImage", "canSendRequiredEmail", "usernameIsEmail"}}, + {"umbracoSettings", new[] {"allowPasswordReset", "imageFileTypes", "maxFileSize", "loginBackgroundImage", "loginLogoImage", "canSendRequiredEmail", "usernameIsEmail", "minimumPasswordLength", "minimumPasswordNonAlphaNum"}}, {"application", new[] {"applicationPath", "cacheBuster"}}, {"isDebuggingEnabled", new string[] { }}, {"features", new [] {"disabledFeatures"}} @@ -103,6 +100,8 @@ namespace Umbraco.Web.Editors /// internal Dictionary GetServerVariables() { + var userMembershipProvider = Core.Security.MembershipProviderExtensions.GetUsersMembershipProvider(); + var defaultVals = new Dictionary { { @@ -360,6 +359,8 @@ namespace Umbraco.Web.Editors {"showUserInvite", EmailSender.CanSendRequiredEmail}, {"canSendRequiredEmail", EmailSender.CanSendRequiredEmail}, {"showAllowSegmentationForDocumentTypes", false}, + {"minimumPasswordLength", userMembershipProvider.MinRequiredPasswordLength}, + {"minimumPasswordNonAlphaNum", userMembershipProvider.MinRequiredNonAlphanumericCharacters}, } }, { diff --git a/src/Umbraco.Web/Editors/Binders/ContentModelBinderHelper.cs b/src/Umbraco.Web/Editors/Binders/ContentModelBinderHelper.cs index e0d39b5f65..75060d059a 100644 --- a/src/Umbraco.Web/Editors/Binders/ContentModelBinderHelper.cs +++ b/src/Umbraco.Web/Editors/Binders/ContentModelBinderHelper.cs @@ -1,5 +1,9 @@ -using System.Net; +using System.Collections.Generic; +using System.Linq; +using System.Net; using System.Net.Http; +using System.Text; +using System.Text.RegularExpressions; using System.Web.Http; using System.Web.Http.Controllers; using Umbraco.Core; @@ -17,6 +21,8 @@ namespace Umbraco.Web.Editors.Binders /// internal static class ContentModelBinderHelper { + private const char _escapeChar = '\\'; + public static TModelSave BindModelFromMultipartRequest(HttpActionContext actionContext, ModelBindingContext bindingContext) where TModelSave : IHaveUploadedFiles { @@ -30,6 +36,7 @@ namespace Umbraco.Web.Editors.Binders //The name that has been assigned in JS has 2 or more parts. The second part indicates the property id // for which the file belongs, the remaining parts are just metadata that can be used by the property editor. var parts = file.Headers.ContentDisposition.Name.Trim(Constants.CharArrays.DoubleQuote).Split(Constants.CharArrays.Underscore); + if (parts.Length < 2) { var response = actionContext.Request.CreateResponse(HttpStatusCode.BadRequest); diff --git a/src/Umbraco.Web/Editors/CodeFileController.cs b/src/Umbraco.Web/Editors/CodeFileController.cs index bcd9362f3d..36cda4bd11 100644 --- a/src/Umbraco.Web/Editors/CodeFileController.cs +++ b/src/Umbraco.Web/Editors/CodeFileController.cs @@ -638,7 +638,10 @@ namespace Umbraco.Web.Editors { var path = IOHelper.MapPath(systemDirectory + "/" + virtualPath); var dirInfo = new DirectoryInfo(path); - return dirInfo.Attributes == FileAttributes.Directory; + + // If you turn off indexing in Windows this will have the attribute: + // `FileAttributes.Directory | FileAttributes.NotContentIndexed` + return (dirInfo.Attributes & FileAttributes.Directory) != 0; } // this is an internal class for passing stylesheet data from the client to the controller while editing diff --git a/src/Umbraco.Web/Editors/ContentController.cs b/src/Umbraco.Web/Editors/ContentController.cs index ed629d746a..6d4e8af3f0 100644 --- a/src/Umbraco.Web/Editors/ContentController.cs +++ b/src/Umbraco.Web/Editors/ContentController.cs @@ -1168,7 +1168,7 @@ namespace Umbraco.Web.Editors //if this item's path has already been denied or if the user doesn't have access to it, add to the deny list if (denied.Any(x => c.Path.StartsWith($"{x.Path},")) || (ContentPermissionsHelper.CheckPermissions(c, - Security.CurrentUser, Services.UserService, Services.EntityService, + Security.CurrentUser, Services.UserService, Services.EntityService, AppCaches, ActionPublish.ActionLetter) == ContentPermissionsHelper.ContentAccess.Denied)) { denied.Add(c); @@ -1978,7 +1978,7 @@ namespace Umbraco.Web.Editors if (template == null) { //ModelState.AddModelError("Template", "No template exists with the specified alias: " + contentItem.TemplateAlias); - Logger.Warn("No template exists with the specified alias: {TemplateAlias}", contentSave.TemplateAlias); + Logger.Warn("No template exists with the specified alias: {TemplateAlias}", contentSave.TemplateAlias); } else if (template.Id != contentSave.PersistedContent.TemplateId) { diff --git a/src/Umbraco.Web/Editors/ContentControllerBase.cs b/src/Umbraco.Web/Editors/ContentControllerBase.cs index 893c9f5941..d94ab8058f 100644 --- a/src/Umbraco.Web/Editors/ContentControllerBase.cs +++ b/src/Umbraco.Web/Editors/ContentControllerBase.cs @@ -62,7 +62,7 @@ namespace Umbraco.Web.Editors // get the property editor if (propertyDto.PropertyEditor == null) { - Logger.Warn("No property editor found for property {PropertyAlias}", propertyDto.Alias); + Logger.Warn("No property editor found for property {PropertyAlias}", propertyDto.Alias); continue; } diff --git a/src/Umbraco.Web/Editors/ContentTypeController.cs b/src/Umbraco.Web/Editors/ContentTypeController.cs index de69ca858f..c878d58e6c 100644 --- a/src/Umbraco.Web/Editors/ContentTypeController.cs +++ b/src/Umbraco.Web/Editors/ContentTypeController.cs @@ -282,91 +282,35 @@ namespace Umbraco.Web.Editors : Request.CreateNotificationValidationErrorResponse(result.Exception.Message); } - public CreatedContentTypeCollectionResult PostCreateCollection(int parentId, string collectionName, bool collectionCreateTemplate, string collectionItemName, bool collectionItemCreateTemplate, string collectionIcon, string collectionItemIcon) - { - // create item doctype - var itemDocType = new ContentType(parentId); - itemDocType.Name = collectionItemName; - itemDocType.Alias = collectionItemName.ToSafeAlias(true); - itemDocType.Icon = collectionItemIcon; - - // create item doctype template - if (collectionItemCreateTemplate) - { - var template = CreateTemplateForContentType(itemDocType.Alias, itemDocType.Name); - itemDocType.SetDefaultTemplate(template); - } - - // save item doctype - Services.ContentTypeService.Save(itemDocType); - - // create collection doctype - var collectionDocType = new ContentType(parentId); - collectionDocType.Name = collectionName; - collectionDocType.Alias = collectionName.ToSafeAlias(true); - collectionDocType.Icon = collectionIcon; - collectionDocType.IsContainer = true; - collectionDocType.AllowedContentTypes = new List() - { - new ContentTypeSort(itemDocType.Id, 0) - }; - - // create collection doctype template - if (collectionCreateTemplate) - { - var template = CreateTemplateForContentType(collectionDocType.Alias, collectionDocType.Name); - collectionDocType.SetDefaultTemplate(template); - } - - // save collection doctype - Services.ContentTypeService.Save(collectionDocType); - - // test if the parent exist and then allow the collection underneath - var parentCt = Services.ContentTypeService.Get(parentId); - if (parentCt != null) - { - var allowedCts = parentCt.AllowedContentTypes.ToList(); - allowedCts.Add(new ContentTypeSort(collectionDocType.Id, allowedCts.Count())); - parentCt.AllowedContentTypes = allowedCts; - Services.ContentTypeService.Save(parentCt); - } - - return new CreatedContentTypeCollectionResult - { - CollectionId = collectionDocType.Id, - ContainerId = itemDocType.Id - }; - } - public DocumentTypeDisplay PostSave(DocumentTypeSave contentTypeSave) { - //Before we send this model into this saving/mapping pipeline, we need to do some cleanup on variations. - //If the doc type does not allow content variations, we need to update all of it's property types to not allow this either - //else we may end up with ysods. I'm unsure if the service level handles this but we'll make sure it is updated here + //Before we send this model into this saving/mapping pipeline, we need to do some cleanup on variations. + //If the doc type does not allow content variations, we need to update all of it's property types to not allow this either + //else we may end up with ysods. I'm unsure if the service level handles this but we'll make sure it is updated here if (!contentTypeSave.AllowCultureVariant) { - foreach(var prop in contentTypeSave.Groups.SelectMany(x => x.Properties)) + foreach (var prop in contentTypeSave.Groups.SelectMany(x => x.Properties)) { prop.AllowCultureVariant = false; } } var savedCt = PerformPostSave( - contentTypeSave: contentTypeSave, - getContentType: i => Services.ContentTypeService.Get(i), - saveContentType: type => Services.ContentTypeService.Save(type), - beforeCreateNew: ctSave => + contentTypeSave: contentTypeSave, + getContentType: i => Services.ContentTypeService.Get(i), + saveContentType: type => Services.ContentTypeService.Save(type), + beforeCreateNew: ctSave => { - //create a default template if it doesn't exist -but only if default template is == to the content type + //create a default template if it doesn't exist -but only if default template is == to the content type if (ctSave.DefaultTemplate.IsNullOrWhiteSpace() == false && ctSave.DefaultTemplate == ctSave.Alias) { var template = CreateTemplateForContentType(ctSave.Alias, ctSave.Name); - // If the alias has been manually updated before the first save, - // make sure to also update the first allowed template, as the - // name will come back as a SafeAlias of the document type name, - // not as the actual document type alias. - // For more info: http://issues.umbraco.org/issue/U4-11059 + // If the alias has been manually updated before the first save, + // make sure to also update the first allowed template, as the + // name will come back as a SafeAlias of the document type name, + // not as the actual document type alias. + // For more info: http://issues.umbraco.org/issue/U4-11059 if (ctSave.DefaultTemplate != template.Alias) { var allowedTemplates = ctSave.AllowedTemplates.ToArray(); @@ -375,7 +319,7 @@ namespace Umbraco.Web.Editors ctSave.AllowedTemplates = allowedTemplates; } - //make sure the template alias is set on the default and allowed template so we can map it back + //make sure the template alias is set on the default and allowed template so we can map it back ctSave.DefaultTemplate = template.Alias; } @@ -415,7 +359,7 @@ namespace Umbraco.Web.Editors var tryCreateTemplate = Services.FileService.CreateTemplateForContentType(contentTypeAlias, contentTypeName); if (tryCreateTemplate == false) { - Logger.Warn("Could not create a template for Content Type: \"{ContentTypeAlias}\", status: {Status}", + Logger.Warn("Could not create a template for Content Type: \"{ContentTypeAlias}\", status: {Status}", contentTypeAlias, tryCreateTemplate.Result.Result); } @@ -600,7 +544,7 @@ namespace Umbraco.Web.Editors } catch (Exception ex) { - Logger.Error(ex, "Error cleaning up temporary udt file in App_Data: {File}", filePath); + Logger.Error(ex, "Error cleaning up temporary udt file in App_Data: {File}", filePath); } return Request.CreateResponse(HttpStatusCode.OK); @@ -647,7 +591,7 @@ namespace Umbraco.Web.Editors } catch (Exception ex) { - Logger.Error(ex, "Error uploading udt file to App_Data: {File}", destFileName); + Logger.Error(ex, "Error uploading udt file to App_Data: {File}", destFileName); } if (ext.InvariantEquals("udt")) diff --git a/src/Umbraco.Web/Editors/DashboardController.cs b/src/Umbraco.Web/Editors/DashboardController.cs index eef0b5df93..97db8818f2 100644 --- a/src/Umbraco.Web/Editors/DashboardController.cs +++ b/src/Umbraco.Web/Editors/DashboardController.cs @@ -17,6 +17,7 @@ using Umbraco.Core.Logging; using Umbraco.Core.Persistence; using Umbraco.Core.Services; using Umbraco.Core.Dashboards; +using Umbraco.Core.Models; using Umbraco.Web.Services; namespace Umbraco.Web.Editors @@ -52,8 +53,9 @@ namespace Umbraco.Web.Editors var allowedSections = string.Join(",", user.AllowedSections); var language = user.Language; var version = UmbracoVersion.SemanticVersion.ToSemanticString(); + var isAdmin = user.IsAdmin(); - var url = string.Format(baseUrl + "{0}?section={0}&allowed={1}&lang={2}&version={3}", section, allowedSections, language, version); + var url = string.Format(baseUrl + "{0}?section={0}&allowed={1}&lang={2}&version={3}&admin={4}", section, allowedSections, language, version, isAdmin); var key = "umbraco-dynamic-dashboard-" + language + allowedSections.Replace(",", "-") + section; var content = AppCaches.RuntimeCache.GetCacheItem(key); @@ -76,7 +78,7 @@ namespace Umbraco.Web.Editors } catch (HttpRequestException ex) { - Logger.Error(ex.InnerException ?? ex, "Error getting dashboard content from {Url}", url); + Logger.Error(ex.InnerException ?? ex, "Error getting dashboard content from {Url}", url); //it's still new JObject() - we return it like this to avoid error codes which triggers UI warnings AppCaches.RuntimeCache.InsertCacheItem(key, () => result, new TimeSpan(0, 5, 0)); @@ -114,7 +116,7 @@ namespace Umbraco.Web.Editors } catch (HttpRequestException ex) { - Logger.Error(ex.InnerException ?? ex, "Error getting dashboard CSS from {Url}", url); + Logger.Error(ex.InnerException ?? ex, "Error getting dashboard CSS from {Url}", url); //it's still string.Empty - we return it like this to avoid error codes which triggers UI warnings AppCaches.RuntimeCache.InsertCacheItem(key, () => result, new TimeSpan(0, 5, 0)); @@ -178,7 +180,7 @@ namespace Umbraco.Web.Editors } catch (HttpRequestException ex) { - Logger.Error(ex.InnerException ?? ex, "Error getting remote dashboard data from {UrlPrefix}{Url}", urlPrefix, url); + Logger.Error(ex.InnerException ?? ex, "Error getting remote dashboard data from {UrlPrefix}{Url}", urlPrefix, url); //it's still string.Empty - we return it like this to avoid error codes which triggers UI warnings AppCaches.RuntimeCache.InsertCacheItem(key, () => result, new TimeSpan(0, 5, 0)); diff --git a/src/Umbraco.Web/Editors/DictionaryController.cs b/src/Umbraco.Web/Editors/DictionaryController.cs index f3246276b1..fcc2e2af02 100644 --- a/src/Umbraco.Web/Editors/DictionaryController.cs +++ b/src/Umbraco.Web/Editors/DictionaryController.cs @@ -124,7 +124,7 @@ namespace Umbraco.Web.Editors } catch (Exception ex) { - Logger.Error(GetType(), ex, "Error creating dictionary with {Name} under {ParentId}", key, parentId); + Logger.Error(GetType(), ex, "Error creating dictionary with {Name} under {ParentId}", key, parentId); return Request.CreateNotificationValidationErrorResponse("Error creating dictionary item"); } } @@ -254,7 +254,7 @@ namespace Umbraco.Web.Editors } catch (Exception ex) { - Logger.Error(GetType(), ex, "Error saving dictionary with {Name} under {ParentId}", dictionary.Name, dictionary.ParentId); + Logger.Error(GetType(), ex, "Error saving dictionary with {Name} under {ParentId}", dictionary.Name, dictionary.ParentId); throw new HttpResponseException(Request.CreateNotificationValidationErrorResponse("Something went wrong saving dictionary")); } } diff --git a/src/Umbraco.Web/Editors/EntityController.cs b/src/Umbraco.Web/Editors/EntityController.cs index fe78b29123..9399deb1e5 100644 --- a/src/Umbraco.Web/Editors/EntityController.cs +++ b/src/Umbraco.Web/Editors/EntityController.cs @@ -670,9 +670,9 @@ namespace Umbraco.Web.Editors switch (type) { case UmbracoEntityTypes.Document: - return Security.CurrentUser.CalculateContentStartNodeIds(Services.EntityService); + return Security.CurrentUser.CalculateContentStartNodeIds(Services.EntityService, AppCaches); case UmbracoEntityTypes.Media: - return Security.CurrentUser.CalculateMediaStartNodeIds(Services.EntityService); + return Security.CurrentUser.CalculateMediaStartNodeIds(Services.EntityService, AppCaches); default: return Array.Empty(); } @@ -811,10 +811,10 @@ namespace Umbraco.Web.Editors switch (entityType) { case UmbracoEntityTypes.Document: - aids = Security.CurrentUser.CalculateContentStartNodeIds(Services.EntityService); + aids = Security.CurrentUser.CalculateContentStartNodeIds(Services.EntityService, AppCaches); break; case UmbracoEntityTypes.Media: - aids = Security.CurrentUser.CalculateMediaStartNodeIds(Services.EntityService); + aids = Security.CurrentUser.CalculateMediaStartNodeIds(Services.EntityService, AppCaches); break; } diff --git a/src/Umbraco.Web/Editors/ExamineManagementController.cs b/src/Umbraco.Web/Editors/ExamineManagementController.cs index cf1dfd5d5d..132cc25404 100644 --- a/src/Umbraco.Web/Editors/ExamineManagementController.cs +++ b/src/Umbraco.Web/Editors/ExamineManagementController.cs @@ -25,7 +25,6 @@ namespace Umbraco.Web.Editors private readonly IAppPolicyCache _runtimeCache; private readonly IndexRebuilder _indexRebuilder; - public ExamineManagementController(IExamineManager examineManager, ILogger logger, AppCaches appCaches, IndexRebuilder indexRebuilder) @@ -79,14 +78,11 @@ namespace Umbraco.Web.Editors { Id = x.Id, Score = x.Score, - //order the values by key - Values = new Dictionary(x.Values.OrderBy(y => y.Key).ToDictionary(y => y.Key, y => y.Value)) + Values = x.AllValues.OrderBy(y => y.Key).ToDictionary(y => y.Key, y => y.Value) }) }; } - - /// /// Check if the index has been rebuilt /// @@ -113,7 +109,6 @@ namespace Umbraco.Web.Editors return found != null ? null : CreateModel(index); - } /// @@ -131,7 +126,7 @@ namespace Umbraco.Web.Editors if (!validate.IsSuccessStatusCode) return validate; - _logger.Info("Rebuilding index '{IndexName}'", indexName); + _logger.Info("Rebuilding index '{IndexName}'", indexName); //remove it in case there's a handler there already index.IndexOperationComplete -= Indexer_IndexOperationComplete; @@ -167,8 +162,6 @@ namespace Umbraco.Web.Editors } } - - private ExamineIndexModel CreateModel(IIndex index) { var indexName = index.Name; @@ -182,11 +175,13 @@ namespace Umbraco.Web.Editors } var isHealth = indexDiag.IsHealthy(); + var properties = new Dictionary { [nameof(IIndexDiagnostics.DocumentCount)] = indexDiag.DocumentCount, [nameof(IIndexDiagnostics.FieldCount)] = indexDiag.FieldCount, }; + foreach (var p in indexDiag.Metadata) properties[p.Key] = p.Value; @@ -198,7 +193,6 @@ namespace Umbraco.Web.Editors CanRebuild = _indexRebuilder.CanRebuild(index) }; - return indexerModel; } @@ -211,7 +205,6 @@ namespace Umbraco.Web.Editors return Request.CreateResponse(HttpStatusCode.OK); } - //if we didn't find anything try to find it by an explicitly declared searcher if (_examineManager.TryGetSearcher(searcherName, out searcher)) return Request.CreateResponse(HttpStatusCode.OK); @@ -253,7 +246,7 @@ namespace Umbraco.Web.Editors { var indexer = (IIndex)sender; - _logger.Debug("Logging operation completed for index {IndexName}", indexer.Name); + _logger.Debug("Logging operation completed for index {IndexName}", indexer.Name); //ensure it's not listening anymore indexer.IndexOperationComplete -= Indexer_IndexOperationComplete; diff --git a/src/Umbraco.Web/Editors/Filters/ContentSaveValidationAttribute.cs b/src/Umbraco.Web/Editors/Filters/ContentSaveValidationAttribute.cs index fbce2d0414..c0e190989a 100644 --- a/src/Umbraco.Web/Editors/Filters/ContentSaveValidationAttribute.cs +++ b/src/Umbraco.Web/Editors/Filters/ContentSaveValidationAttribute.cs @@ -7,6 +7,7 @@ using System.Web.Http; using System.Web.Http.Controllers; using System.Web.Http.Filters; using Umbraco.Core; +using Umbraco.Core.Cache; using Umbraco.Core.Logging; using Umbraco.Core.Models; using Umbraco.Core.Security; @@ -29,11 +30,12 @@ namespace Umbraco.Web.Editors.Filters private readonly IContentService _contentService; private readonly IUserService _userService; private readonly IEntityService _entityService; + private readonly AppCaches _appCaches; - public ContentSaveValidationAttribute(): this(Current.Logger, Current.UmbracoContextAccessor, Current.Services.TextService, Current.Services.ContentService, Current.Services.UserService, Current.Services.EntityService) + public ContentSaveValidationAttribute(): this(Current.Logger, Current.UmbracoContextAccessor, Current.Services.TextService, Current.Services.ContentService, Current.Services.UserService, Current.Services.EntityService, Current.AppCaches) { } - public ContentSaveValidationAttribute(ILogger logger, IUmbracoContextAccessor umbracoContextAccessor, ILocalizedTextService textService, IContentService contentService, IUserService userService, IEntityService entityService) + public ContentSaveValidationAttribute(ILogger logger, IUmbracoContextAccessor umbracoContextAccessor, ILocalizedTextService textService, IContentService contentService, IUserService userService, IEntityService entityService, AppCaches appCaches) { _logger = logger ?? throw new ArgumentNullException(nameof(logger)); _umbracoContextAccessor = umbracoContextAccessor ?? throw new ArgumentNullException(nameof(umbracoContextAccessor)); @@ -41,6 +43,7 @@ namespace Umbraco.Web.Editors.Filters _contentService = contentService ?? throw new ArgumentNullException(nameof(contentService)); _userService = userService ?? throw new ArgumentNullException(nameof(userService)); _entityService = entityService ?? throw new ArgumentNullException(nameof(entityService)); + _appCaches = appCaches; } public override void OnActionExecuting(HttpActionContext actionContext) @@ -195,13 +198,13 @@ namespace Umbraco.Web.Editors.Filters accessResult = ContentPermissionsHelper.CheckPermissions( contentToCheck, webSecurity.CurrentUser, - _userService, _entityService, permissionToCheck.ToArray()); + _userService, _entityService, _appCaches, permissionToCheck.ToArray()); } else { accessResult = ContentPermissionsHelper.CheckPermissions( contentIdToCheck, webSecurity.CurrentUser, - _userService, _contentService, _entityService, + _userService, _contentService, _entityService, _appCaches, out contentToCheck, permissionToCheck.ToArray()); if (contentToCheck != null) diff --git a/src/Umbraco.Web/Editors/Filters/MediaItemSaveValidationAttribute.cs b/src/Umbraco.Web/Editors/Filters/MediaItemSaveValidationAttribute.cs index 449ef95675..af973d0662 100644 --- a/src/Umbraco.Web/Editors/Filters/MediaItemSaveValidationAttribute.cs +++ b/src/Umbraco.Web/Editors/Filters/MediaItemSaveValidationAttribute.cs @@ -4,6 +4,7 @@ using System.Net.Http; using System.Web.Http.Controllers; using System.Web.Http.Filters; using Umbraco.Core; +using Umbraco.Core.Cache; using Umbraco.Core.Logging; using Umbraco.Core.Models; using Umbraco.Core.Services; @@ -23,18 +24,20 @@ namespace Umbraco.Web.Editors.Filters private readonly ILocalizedTextService _textService; private readonly IMediaService _mediaService; private readonly IEntityService _entityService; + private readonly AppCaches _appCaches; - public MediaItemSaveValidationAttribute() : this(Current.Logger, Current.UmbracoContextAccessor, Current.Services.TextService, Current.Services.MediaService, Current.Services.EntityService) + public MediaItemSaveValidationAttribute() : this(Current.Logger, Current.UmbracoContextAccessor, Current.Services.TextService, Current.Services.MediaService, Current.Services.EntityService, Current.AppCaches) { } - public MediaItemSaveValidationAttribute(ILogger logger, IUmbracoContextAccessor umbracoContextAccessor, ILocalizedTextService textService, IMediaService mediaService, IEntityService entityService) + public MediaItemSaveValidationAttribute(ILogger logger, IUmbracoContextAccessor umbracoContextAccessor, ILocalizedTextService textService, IMediaService mediaService, IEntityService entityService, AppCaches appCaches) { _logger = logger ?? throw new ArgumentNullException(nameof(logger)); _umbracoContextAccessor = umbracoContextAccessor ?? throw new ArgumentNullException(nameof(umbracoContextAccessor)); _textService = textService ?? throw new ArgumentNullException(nameof(textService)); _mediaService = mediaService ?? throw new ArgumentNullException(nameof(mediaService)); _entityService = entityService ?? throw new ArgumentNullException(nameof(entityService)); + _appCaches = appCaches; } public override void OnActionExecuting(HttpActionContext actionContext) @@ -91,7 +94,7 @@ namespace Umbraco.Web.Editors.Filters if (MediaController.CheckPermissions( actionContext.Request.Properties, _umbracoContextAccessor.UmbracoContext.Security.CurrentUser, - _mediaService, _entityService, + _mediaService, _entityService, _appCaches, contentIdToCheck, contentToCheck) == false) { actionContext.Response = actionContext.Request.CreateUserNoAccessResponse(); diff --git a/src/Umbraco.Web/Editors/Filters/UserGroupAuthorizationAttribute.cs b/src/Umbraco.Web/Editors/Filters/UserGroupAuthorizationAttribute.cs index e1d6626055..e94098db82 100644 --- a/src/Umbraco.Web/Editors/Filters/UserGroupAuthorizationAttribute.cs +++ b/src/Umbraco.Web/Editors/Filters/UserGroupAuthorizationAttribute.cs @@ -54,7 +54,8 @@ namespace Umbraco.Web.Editors.Filters Current.Services.UserService, Current.Services.ContentService, Current.Services.MediaService, - Current.Services.EntityService); + Current.Services.EntityService, + Current.AppCaches); return authHelper.AuthorizeGroupAccess(currentUser, intIds); } } diff --git a/src/Umbraco.Web/Editors/Filters/UserGroupEditorAuthorizationHelper.cs b/src/Umbraco.Web/Editors/Filters/UserGroupEditorAuthorizationHelper.cs index 985c42bbbf..ad3dae5e06 100644 --- a/src/Umbraco.Web/Editors/Filters/UserGroupEditorAuthorizationHelper.cs +++ b/src/Umbraco.Web/Editors/Filters/UserGroupEditorAuthorizationHelper.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using System.Linq; using Umbraco.Core; +using Umbraco.Core.Cache; using Umbraco.Core.Models; using Umbraco.Core.Models.Membership; using Umbraco.Core.Services; @@ -13,13 +14,15 @@ namespace Umbraco.Web.Editors.Filters private readonly IContentService _contentService; private readonly IMediaService _mediaService; private readonly IEntityService _entityService; + private readonly AppCaches _appCaches; - public UserGroupEditorAuthorizationHelper(IUserService userService, IContentService contentService, IMediaService mediaService, IEntityService entityService) + public UserGroupEditorAuthorizationHelper(IUserService userService, IContentService contentService, IMediaService mediaService, IEntityService entityService, AppCaches appCaches) { _userService = userService; _contentService = contentService; _mediaService = mediaService; _entityService = entityService; + _appCaches = appCaches; } /// @@ -73,18 +76,15 @@ namespace Umbraco.Web.Editors.Filters /// /// Authorize that the user is not adding a section to the group that they don't have access to /// - /// - /// - /// - /// - public Attempt AuthorizeSectionChanges(IUser currentUser, - IEnumerable currentAllowedSections, + public Attempt AuthorizeSectionChanges( + IUser currentUser, + IEnumerable existingSections, IEnumerable proposedAllowedSections) { if (currentUser.IsAdmin()) return Attempt.Succeed(); - var sectionsAdded = currentAllowedSections.Except(proposedAllowedSections).ToArray(); + var sectionsAdded = proposedAllowedSections.Except(existingSections).ToArray(); var sectionAccessMissing = sectionsAdded.Except(currentUser.AllowedSections).ToArray(); return sectionAccessMissing.Length > 0 ? Attempt.Fail("Current user doesn't have access to add these sections " + string.Join(", ", sectionAccessMissing)) @@ -111,7 +111,7 @@ namespace Umbraco.Web.Editors.Filters var content = _contentService.GetById(proposedContentStartId.Value); if (content != null) { - if (currentUser.HasPathAccess(content, _entityService) == false) + if (currentUser.HasPathAccess(content, _entityService, _appCaches) == false) return Attempt.Fail("Current user doesn't have access to the content path " + content.Path); } } @@ -121,7 +121,7 @@ namespace Umbraco.Web.Editors.Filters var media = _mediaService.GetById(proposedMediaStartId.Value); if (media != null) { - if (currentUser.HasPathAccess(media, _entityService) == false) + if (currentUser.HasPathAccess(media, _entityService, _appCaches) == false) return Attempt.Fail("Current user doesn't have access to the media path " + media.Path); } } diff --git a/src/Umbraco.Web/Editors/Filters/UserGroupValidateAttribute.cs b/src/Umbraco.Web/Editors/Filters/UserGroupValidateAttribute.cs index 78cd8e6a4d..a2647d2ee9 100644 --- a/src/Umbraco.Web/Editors/Filters/UserGroupValidateAttribute.cs +++ b/src/Umbraco.Web/Editors/Filters/UserGroupValidateAttribute.cs @@ -58,13 +58,9 @@ namespace Umbraco.Web.Editors.Filters return; } - //map the model to the persisted instance - Mapper.Map(userGroupSave, persisted); break; case ContentSaveAction.SaveNew: - //create the persisted model from mapping the saved model - persisted = Mapper.Map(userGroupSave); - ((UserGroup)persisted).ResetIdentity(); + persisted = new UserGroup(); break; default: actionContext.Response = actionContext.Request.CreateErrorResponse(HttpStatusCode.NotFound, new ArgumentOutOfRangeException()); diff --git a/src/Umbraco.Web/Editors/IconController.cs b/src/Umbraco.Web/Editors/IconController.cs index 87303a4e62..2aac92088d 100644 --- a/src/Umbraco.Web/Editors/IconController.cs +++ b/src/Umbraco.Web/Editors/IconController.cs @@ -1,13 +1,20 @@ -using System.Collections.Generic; +using Newtonsoft.Json; +using System; +using System.Collections.Generic; using Umbraco.Core.Models; using Umbraco.Core.Services; using Umbraco.Web.Mvc; using Umbraco.Web.WebApi; +using Umbraco.Web.WebApi.Filters; namespace Umbraco.Web.Editors { [PluginController("UmbracoApi")] - public class IconController : UmbracoAuthorizedApiController + [IsBackOffice] + [UmbracoWebApiRequireHttps] + [UnhandedExceptionLoggerConfiguration] + [EnableDetailedErrors] + public class IconController : UmbracoApiController { private readonly IIconService _iconService; @@ -30,9 +37,22 @@ namespace Umbraco.Web.Editors /// Gets a list of all svg icons found at at the global icons path. /// /// + [Obsolete("This method should not be used - use GetIcons instead")] public IList GetAllIcons() { return _iconService.GetAllIcons(); } + + /// + /// Gets a list of all svg icons found at at the global icons path. + /// + /// + public JsonNetResult GetIcons() + { + return new JsonNetResult(JsonNetResult.DefaultJsonSerializerSettings) { + Data = _iconService.GetIcons(), + Formatting = Formatting.None + }; + } } } diff --git a/src/Umbraco.Web/Editors/KeepAliveController.cs b/src/Umbraco.Web/Editors/KeepAliveController.cs index 23815e1bbe..f29ee6c60a 100644 --- a/src/Umbraco.Web/Editors/KeepAliveController.cs +++ b/src/Umbraco.Web/Editors/KeepAliveController.cs @@ -1,14 +1,12 @@ using System.Runtime.Serialization; using System.Web.Http; -using Umbraco.Web.Mvc; using Umbraco.Web.WebApi; -using Umbraco.Web.WebApi.Filters; namespace Umbraco.Web.Editors { public class KeepAliveController : UmbracoApiController { - [OnlyLocalRequests] + [HttpHead] [HttpGet] public KeepAlivePingResult Ping() { diff --git a/src/Umbraco.Web/Editors/MediaController.cs b/src/Umbraco.Web/Editors/MediaController.cs index 406d3e95da..4a122ad956 100644 --- a/src/Umbraco.Web/Editors/MediaController.cs +++ b/src/Umbraco.Web/Editors/MediaController.cs @@ -239,7 +239,7 @@ namespace Umbraco.Web.Editors protected int[] UserStartNodes { - get { return _userStartNodes ?? (_userStartNodes = Security.CurrentUser.CalculateMediaStartNodeIds(Services.EntityService)); } + get { return _userStartNodes ?? (_userStartNodes = Security.CurrentUser.CalculateMediaStartNodeIds(Services.EntityService, AppCaches)); } } /// @@ -835,6 +835,7 @@ namespace Umbraco.Web.Editors Security.CurrentUser, Services.MediaService, Services.EntityService, + AppCaches, intParentId) == false) { throw new HttpResponseException(Request.CreateResponse( @@ -919,7 +920,7 @@ namespace Umbraco.Web.Editors /// The content to lookup, if the contentItem is not specified /// Specifies the already resolved content item to check against, setting this ignores the nodeId /// - internal static bool CheckPermissions(IDictionary storage, IUser user, IMediaService mediaService, IEntityService entityService, int nodeId, IMedia media = null) + internal static bool CheckPermissions(IDictionary storage, IUser user, IMediaService mediaService, IEntityService entityService, AppCaches appCaches, int nodeId, IMedia media = null) { if (storage == null) throw new ArgumentNullException("storage"); if (user == null) throw new ArgumentNullException("user"); @@ -940,10 +941,10 @@ namespace Umbraco.Web.Editors } var hasPathAccess = (nodeId == Constants.System.Root) - ? user.HasMediaRootAccess(entityService) + ? user.HasMediaRootAccess(entityService, appCaches) : (nodeId == Constants.System.RecycleBinMedia) - ? user.HasMediaBinAccess(entityService) - : user.HasPathAccess(media, entityService); + ? user.HasMediaBinAccess(entityService, appCaches) + : user.HasPathAccess(media, entityService, appCaches); return hasPathAccess; } diff --git a/src/Umbraco.Web/Editors/MemberTypeController.cs b/src/Umbraco.Web/Editors/MemberTypeController.cs index d55afd5e95..8d705aa20e 100644 --- a/src/Umbraco.Web/Editors/MemberTypeController.cs +++ b/src/Umbraco.Web/Editors/MemberTypeController.cs @@ -237,6 +237,18 @@ namespace Umbraco.Web.Editors return display; } + /// + /// Copy the member type + /// + /// + /// + public HttpResponseMessage PostCopy(MoveOrCopy copy) + { + return PerformCopy( + copy, + getContentType: i => Services.MemberTypeService.Get(i), + doCopy: (type, i) => Services.MemberTypeService.Copy(type, i)); + } } } diff --git a/src/Umbraco.Web/Editors/PasswordChanger.cs b/src/Umbraco.Web/Editors/PasswordChanger.cs index 2698a68b40..f680245087 100644 --- a/src/Umbraco.Web/Editors/PasswordChanger.cs +++ b/src/Umbraco.Web/Editors/PasswordChanger.cs @@ -1,5 +1,6 @@ using System; using System.ComponentModel.DataAnnotations; +using System.Linq; using System.Threading.Tasks; using System.Web; using System.Web.Http.ModelBinding; @@ -84,6 +85,11 @@ namespace Umbraco.Web.Editors return Attempt.Fail(new PasswordChangedModel { ChangeError = new ValidationResult("The current user is not authorized", new[] { "resetPassword" }) }); } + if (!currentUser.IsAdmin() && savingUser.IsAdmin()) + { + return Attempt.Fail(new PasswordChangedModel { ChangeError = new ValidationResult("The current user cannot change the password for the specified user", new[] { "resetPassword" }) }); + } + //ok, we should be able to reset it var resetToken = await userMgr.GeneratePasswordResetTokenAsync(savingUser.Id); var newPass = passwordModel.NewPassword.IsNullOrWhiteSpace() @@ -95,7 +101,7 @@ namespace Umbraco.Web.Editors if (resetResult.Succeeded == false) { var errors = string.Join(". ", resetResult.Errors); - _logger.Warn("Could not reset user password {PasswordErrors}", errors); + _logger.Warn("Could not reset user password {PasswordErrors}", errors); return Attempt.Fail(new PasswordChangedModel { ChangeError = new ValidationResult(errors, new[] { "resetPassword" }) }); } @@ -136,7 +142,7 @@ namespace Umbraco.Web.Editors { //no, fail with error messages for "password" var errors = string.Join(". ", changeResult.Errors); - _logger.Warn("Could not change user password {PasswordErrors}", errors); + _logger.Warn("Could not change user password {PasswordErrors}", errors); return Attempt.Fail(new PasswordChangedModel { ChangeError = new ValidationResult(errors, new[] { "password" }) }); } return Attempt.Succeed(new PasswordChangedModel()); @@ -197,7 +203,7 @@ namespace Umbraco.Web.Editors } catch (Exception ex) { - _logger.Warn("Could not change member password", ex); + _logger.Warn(ex,"Could not change member password"); return Attempt.Fail(new PasswordChangedModel { ChangeError = new ValidationResult("Could not change password, error: " + ex.Message + " (see log for full details)", new[] { "value" }) }); } } @@ -246,7 +252,7 @@ namespace Umbraco.Web.Editors return Attempt.Fail(new PasswordChangedModel { ChangeError = new ValidationResult("Cannot set an empty password", new[] { "value" }) }); } - //without being able to retrieve the original password, + //without being able to retrieve the original password, //we cannot arbitrarily change the password without knowing the old one and no old password was supplied - need to return an error if (passwordModel.OldPassword.IsNullOrWhiteSpace() && membershipProvider.EnablePasswordRetrieval == false) { diff --git a/src/Umbraco.Web/Editors/PreviewController.cs b/src/Umbraco.Web/Editors/PreviewController.cs index f00805d2dc..e2770b14ba 100644 --- a/src/Umbraco.Web/Editors/PreviewController.cs +++ b/src/Umbraco.Web/Editors/PreviewController.cs @@ -9,10 +9,8 @@ using Umbraco.Core.Services; using Umbraco.Web.Composing; using Umbraco.Web.Features; using Umbraco.Web.JavaScript; -using Umbraco.Web.Models.ContentEditing; using Umbraco.Web.Mvc; using Umbraco.Web.PublishedCache; -using Umbraco.Web.Services; using Constants = Umbraco.Core.Constants; namespace Umbraco.Web.Editors @@ -25,39 +23,19 @@ namespace Umbraco.Web.Editors private readonly IPublishedSnapshotService _publishedSnapshotService; private readonly IUmbracoContextAccessor _umbracoContextAccessor; private readonly ILocalizationService _localizationService; - private readonly IIconService _iconService; - [Obsolete("Use the constructor that injects IIconService.")] public PreviewController( UmbracoFeatures features, IGlobalSettings globalSettings, IPublishedSnapshotService publishedSnapshotService, IUmbracoContextAccessor umbracoContextAccessor, ILocalizationService localizationService) - :this(features, - globalSettings, - publishedSnapshotService, - umbracoContextAccessor, - localizationService, - Current.IconService) - { - - } - - public PreviewController( - UmbracoFeatures features, - IGlobalSettings globalSettings, - IPublishedSnapshotService publishedSnapshotService, - IUmbracoContextAccessor umbracoContextAccessor, - ILocalizationService localizationService, - IIconService iconService) { _features = features; _globalSettings = globalSettings; _publishedSnapshotService = publishedSnapshotService; _umbracoContextAccessor = umbracoContextAccessor; _localizationService = localizationService; - _iconService = iconService; } [UmbracoAuthorize(redirectToUmbracoLogin: true)] @@ -74,7 +52,7 @@ namespace Umbraco.Web.Editors availableLanguages = availableLanguages.Where(language => content.Cultures.ContainsKey(language.IsoCode)); } - var model = new BackOfficePreviewModel(_features, _globalSettings, availableLanguages, _iconService); + var model = new BackOfficePreviewModel(_features, _globalSettings, availableLanguages); if (model.PreviewExtendedHeaderView.IsNullOrWhiteSpace() == false) { diff --git a/src/Umbraco.Web/Editors/RelationTypeController.cs b/src/Umbraco.Web/Editors/RelationTypeController.cs index 3e45600be9..e56d856933 100644 --- a/src/Umbraco.Web/Editors/RelationTypeController.cs +++ b/src/Umbraco.Web/Editors/RelationTypeController.cs @@ -155,7 +155,7 @@ namespace Umbraco.Web.Editors } catch (Exception ex) { - Logger.Error(GetType(), ex, "Error creating relation type with {Name}", relationType.Name); + Logger.Error(GetType(), ex, "Error creating relation type with {Name}", relationType.Name); return Request.CreateNotificationValidationErrorResponse("Error creating relation type."); } } @@ -186,7 +186,7 @@ namespace Umbraco.Web.Editors } catch (Exception ex) { - Logger.Error(GetType(), ex, "Error saving relation type with {Id}", relationType.Id); + Logger.Error(GetType(), ex, "Error saving relation type with {Id}", relationType.Id); throw new HttpResponseException(Request.CreateNotificationValidationErrorResponse("Something went wrong when saving the relation type")); } } diff --git a/src/Umbraco.Web/Editors/TinyMceController.cs b/src/Umbraco.Web/Editors/TinyMceController.cs index 1d292c64fd..1ceb4f0a1f 100644 --- a/src/Umbraco.Web/Editors/TinyMceController.cs +++ b/src/Umbraco.Web/Editors/TinyMceController.cs @@ -100,7 +100,7 @@ namespace Umbraco.Web.Editors catch (Exception ex) { // IOException, PathTooLong, DirectoryNotFound, UnathorizedAccess - Logger.Error(ex, "Error when trying to move {CurrentFilePath} to {NewFilePath}", currentFile, newFilePath); + Logger.Error(ex, "Error when trying to move {CurrentFilePath} to {NewFilePath}", currentFile, newFilePath); return Request.CreateErrorResponse(HttpStatusCode.InternalServerError, $"Error when trying to move {currentFile} to {newFilePath}", ex); } diff --git a/src/Umbraco.Web/Editors/UserEditorAuthorizationHelper.cs b/src/Umbraco.Web/Editors/UserEditorAuthorizationHelper.cs index 320580aaf9..bd031c0527 100644 --- a/src/Umbraco.Web/Editors/UserEditorAuthorizationHelper.cs +++ b/src/Umbraco.Web/Editors/UserEditorAuthorizationHelper.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using System.Linq; using Umbraco.Core; +using Umbraco.Core.Cache; using Umbraco.Core.Models; using Umbraco.Core.Models.Membership; using Umbraco.Core.Security; @@ -14,13 +15,15 @@ namespace Umbraco.Web.Editors private readonly IMediaService _mediaService; private readonly IUserService _userService; private readonly IEntityService _entityService; + private readonly AppCaches _appCaches; - public UserEditorAuthorizationHelper(IContentService contentService, IMediaService mediaService, IUserService userService, IEntityService entityService) + public UserEditorAuthorizationHelper(IContentService contentService, IMediaService mediaService, IUserService userService, IEntityService entityService, AppCaches appCaches) { _contentService = contentService; _mediaService = mediaService; _userService = userService; _entityService = entityService; + _appCaches = appCaches; } /// @@ -75,6 +78,18 @@ namespace Umbraco.Web.Editors if (userGroupAliases != null) { var savingGroupAliases = userGroupAliases.ToArray(); + var existingGroupAliases = savingUser == null + ? new string[0] + : savingUser.Groups.Select(x => x.Alias).ToArray(); + + var addedGroupAliases = savingGroupAliases.Except(existingGroupAliases); + + // As we know the current user is not admin, it is only allowed to use groups that the user do have themselves. + var savingGroupAliasesNotAllowed = addedGroupAliases.Except(currentUser.Groups.Select(x=>x.Alias)).ToArray(); + if (savingGroupAliasesNotAllowed.Any()) + { + return Attempt.Fail("Cannot assign the group(s) '" + string.Join(", ", savingGroupAliasesNotAllowed) + "', the current user is not part of them or admin"); + } //only validate any groups that have changed. //a non-admin user can remove groups and add groups that they have access to @@ -90,9 +105,7 @@ namespace Umbraco.Web.Editors if (userGroupsChanged) { // d) A user cannot assign a group to another user that they do not belong to - var currentUserGroups = currentUser.Groups.Select(x => x.Alias).ToArray(); - foreach (var group in newGroups) { if (currentUserGroups.Contains(group) == false) @@ -114,7 +127,7 @@ namespace Umbraco.Web.Editors { if (contentId == Constants.System.Root) { - var hasAccess = ContentPermissionsHelper.HasPathAccess("-1", currentUser.CalculateContentStartNodeIds(_entityService), Constants.System.RecycleBinContent); + var hasAccess = ContentPermissionsHelper.HasPathAccess("-1", currentUser.CalculateContentStartNodeIds(_entityService, _appCaches), Constants.System.RecycleBinContent); if (hasAccess == false) return Attempt.Fail("The current user does not have access to the content root"); } @@ -122,7 +135,7 @@ namespace Umbraco.Web.Editors { var content = _contentService.GetById(contentId); if (content == null) continue; - var hasAccess = currentUser.HasPathAccess(content, _entityService); + var hasAccess = currentUser.HasPathAccess(content, _entityService, _appCaches); if (hasAccess == false) return Attempt.Fail("The current user does not have access to the content path " + content.Path); } @@ -135,7 +148,7 @@ namespace Umbraco.Web.Editors { if (mediaId == Constants.System.Root) { - var hasAccess = ContentPermissionsHelper.HasPathAccess("-1", currentUser.CalculateMediaStartNodeIds(_entityService), Constants.System.RecycleBinMedia); + var hasAccess = ContentPermissionsHelper.HasPathAccess("-1", currentUser.CalculateMediaStartNodeIds(_entityService, _appCaches), Constants.System.RecycleBinMedia); if (hasAccess == false) return Attempt.Fail("The current user does not have access to the media root"); } @@ -143,7 +156,7 @@ namespace Umbraco.Web.Editors { var media = _mediaService.GetById(mediaId); if (media == null) continue; - var hasAccess = currentUser.HasPathAccess(media, _entityService); + var hasAccess = currentUser.HasPathAccess(media, _entityService, _appCaches); if (hasAccess == false) return Attempt.Fail("The current user does not have access to the media path " + media.Path); } diff --git a/src/Umbraco.Web/Editors/UserGroupsController.cs b/src/Umbraco.Web/Editors/UserGroupsController.cs index 03e6c5b8b7..2b5ecb063f 100644 --- a/src/Umbraco.Web/Editors/UserGroupsController.cs +++ b/src/Umbraco.Web/Editors/UserGroupsController.cs @@ -28,14 +28,19 @@ namespace Umbraco.Web.Editors //authorize that the user has access to save this user group var authHelper = new UserGroupEditorAuthorizationHelper( - Services.UserService, Services.ContentService, Services.MediaService, Services.EntityService); + Services.UserService, + Services.ContentService, + Services.MediaService, + Services.EntityService, + AppCaches); var isAuthorized = authHelper.AuthorizeGroupAccess(Security.CurrentUser, userGroupSave.Alias); if (isAuthorized == false) throw new HttpResponseException(Request.CreateResponse(HttpStatusCode.Unauthorized, isAuthorized.Result)); //if sections were added we need to check that the current user has access to that section - isAuthorized = authHelper.AuthorizeSectionChanges(Security.CurrentUser, + isAuthorized = authHelper.AuthorizeSectionChanges( + Security.CurrentUser, userGroupSave.PersistedUserGroup.AllowedSections, userGroupSave.Sections); if (isAuthorized == false) @@ -53,6 +58,9 @@ namespace Umbraco.Web.Editors //need to ensure current user is in a group if not an admin to avoid a 401 EnsureNonAdminUserIsInSavedUserGroup(userGroupSave); + //map the model to the persisted instance + Mapper.Map(userGroupSave, userGroupSave.PersistedUserGroup); + //save the group Services.UserService.Save(userGroupSave.PersistedUserGroup, userGroupSave.Users.ToArray()); diff --git a/src/Umbraco.Web/Editors/UsersController.cs b/src/Umbraco.Web/Editors/UsersController.cs index 9d3d1f78fd..947a12384b 100644 --- a/src/Umbraco.Web/Editors/UsersController.cs +++ b/src/Umbraco.Web/Editors/UsersController.cs @@ -304,7 +304,7 @@ namespace Umbraco.Web.Editors CheckUniqueEmail(userSave.Email, null); //Perform authorization here to see if the current user can actually save this user with the info being requested - var authHelper = new UserEditorAuthorizationHelper(Services.ContentService, Services.MediaService, Services.UserService, Services.EntityService); + var authHelper = new UserEditorAuthorizationHelper(Services.ContentService, Services.MediaService, Services.UserService, Services.EntityService, AppCaches); var canSaveUser = authHelper.IsAuthorized(Security.CurrentUser, null, null, null, userSave.UserGroups); if (canSaveUser == false) { @@ -398,7 +398,7 @@ namespace Umbraco.Web.Editors } //Perform authorization here to see if the current user can actually save this user with the info being requested - var authHelper = new UserEditorAuthorizationHelper(Services.ContentService, Services.MediaService, Services.UserService, Services.EntityService); + var authHelper = new UserEditorAuthorizationHelper(Services.ContentService, Services.MediaService, Services.UserService, Services.EntityService, AppCaches); var canSaveUser = authHelper.IsAuthorized(Security.CurrentUser, user, null, null, userSave.UserGroups); if (canSaveUser == false) { @@ -573,7 +573,7 @@ namespace Umbraco.Web.Editors throw new HttpResponseException(HttpStatusCode.NotFound); //Perform authorization here to see if the current user can actually save this user with the info being requested - var authHelper = new UserEditorAuthorizationHelper(Services.ContentService, Services.MediaService, Services.UserService, Services.EntityService); + var authHelper = new UserEditorAuthorizationHelper(Services.ContentService, Services.MediaService, Services.UserService, Services.EntityService, AppCaches); var canSaveUser = authHelper.IsAuthorized(Security.CurrentUser, found, userSave.StartContentIds, userSave.StartMediaIds, userSave.UserGroups); if (canSaveUser == false) { diff --git a/src/Umbraco.Web/FormDataCollectionExtensions.cs b/src/Umbraco.Web/FormDataCollectionExtensions.cs index aabf13ac9b..ebabaa74c7 100644 --- a/src/Umbraco.Web/FormDataCollectionExtensions.cs +++ b/src/Umbraco.Web/FormDataCollectionExtensions.cs @@ -23,9 +23,9 @@ namespace Umbraco.Web if (items.Any() == false) return ""; var builder = new StringBuilder(); - foreach (var i in items.Where(i => keysToIgnore.InvariantContains(i.Key) == false)) + foreach (var (key, value) in items.Where(i => keysToIgnore.InvariantContains(i.Key) == false)) { - builder.Append(string.Format("{0}={1}&", i.Key, i.Value)); + builder.Append($"{key}={value}&"); } return builder.ToString().TrimEnd(Constants.CharArrays.Ampersand); } diff --git a/src/Umbraco.Web/HealthCheck/Checks/Security/ExcessiveHeadersCheck.cs b/src/Umbraco.Web/HealthCheck/Checks/Security/ExcessiveHeadersCheck.cs index 28904cc6bd..30ccf96691 100644 --- a/src/Umbraco.Web/HealthCheck/Checks/Security/ExcessiveHeadersCheck.cs +++ b/src/Umbraco.Web/HealthCheck/Checks/Security/ExcessiveHeadersCheck.cs @@ -49,7 +49,7 @@ namespace Umbraco.Web.HealthCheck.Checks.Security { var message = string.Empty; var success = false; - var url = _runtime.ApplicationUrl; + var url = _runtime.ApplicationUrl.GetLeftPart(UriPartial.Authority); // Access the site home page and check for the headers var request = WebRequest.Create(url); diff --git a/src/Umbraco.Web/HealthCheck/Checks/Security/HttpsCheck.cs b/src/Umbraco.Web/HealthCheck/Checks/Security/HttpsCheck.cs index 51b253fe94..18d27ba028 100644 --- a/src/Umbraco.Web/HealthCheck/Checks/Security/HttpsCheck.cs +++ b/src/Umbraco.Web/HealthCheck/Checks/Security/HttpsCheck.cs @@ -2,9 +2,9 @@ using System.Collections.Generic; using System.Net; using System.Security.Cryptography.X509Certificates; -using System.Web; using Umbraco.Core; using Umbraco.Core.Configuration; +using Umbraco.Core.Configuration.UmbracoSettings; using Umbraco.Core.IO; using Umbraco.Core.Services; using Umbraco.Web.HealthCheck.Checks.Config; @@ -21,14 +21,16 @@ namespace Umbraco.Web.HealthCheck.Checks.Security private readonly ILocalizedTextService _textService; private readonly IRuntimeState _runtime; private readonly IGlobalSettings _globalSettings; + private readonly IContentSection _contentSection; private const string FixHttpsSettingAction = "fixHttpsSetting"; - public HttpsCheck(ILocalizedTextService textService, IRuntimeState runtime, IGlobalSettings globalSettings) + public HttpsCheck(ILocalizedTextService textService, IRuntimeState runtime, IGlobalSettings globalSettings, IContentSection contentSection) { _textService = textService; _runtime = runtime; _globalSettings = globalSettings; + _contentSection = contentSection; } /// @@ -65,12 +67,25 @@ namespace Umbraco.Web.HealthCheck.Checks.Security // Attempt to access the site over HTTPS to see if it HTTPS is supported // and a valid certificate has been configured var url = _runtime.ApplicationUrl.ToString().Replace("http:", "https:"); + var request = (HttpWebRequest) WebRequest.Create(url); - request.Method = "HEAD"; + request.AllowAutoRedirect = false; try { + var response = (HttpWebResponse)request.GetResponse(); + + // Check for 301/302 as a external login provider such as UmbracoID might be in use + if (response.StatusCode == HttpStatusCode.Moved || response.StatusCode == HttpStatusCode.Redirect) + { + // Reset request to use the static login background image + var absoluteLoginBackgroundImage = $"{url}/{_contentSection.LoginBackgroundImage}"; + + request = (HttpWebRequest)WebRequest.Create(absoluteLoginBackgroundImage); + response = (HttpWebResponse)request.GetResponse(); + } + if (response.StatusCode == HttpStatusCode.OK) { // Got a valid response, check now for if certificate expiring within 14 days diff --git a/src/Umbraco.Web/HealthCheck/HealthCheckController.cs b/src/Umbraco.Web/HealthCheck/HealthCheckController.cs index 2f72b946de..770b41c323 100644 --- a/src/Umbraco.Web/HealthCheck/HealthCheckController.cs +++ b/src/Umbraco.Web/HealthCheck/HealthCheckController.cs @@ -73,7 +73,7 @@ namespace Umbraco.Web.HealthCheck } catch (Exception ex) { - _logger.Error(ex, "Exception in health check: {HealthCheckName}", check.Name); + _logger.Error(ex, "Exception in health check: {HealthCheckName}", check.Name); throw; } } diff --git a/src/Umbraco.Web/HealthCheck/HealthCheckResults.cs b/src/Umbraco.Web/HealthCheck/HealthCheckResults.cs index 61028699f0..9f58e04f79 100644 --- a/src/Umbraco.Web/HealthCheck/HealthCheckResults.cs +++ b/src/Umbraco.Web/HealthCheck/HealthCheckResults.cs @@ -27,7 +27,7 @@ namespace Umbraco.Web.HealthCheck } catch (Exception ex) { - Logger.Error(ex, "Error running scheduled health check: {HealthCheckName}", t.Name); + Logger.Error(ex, "Error running scheduled health check: {HealthCheckName}", t.Name); var message = $"Health check failed with exception: {ex.Message}. See logs for details."; return new List { @@ -62,16 +62,16 @@ namespace Umbraco.Web.HealthCheck var checkIsSuccess = result.Value.All(x => x.ResultType == StatusResultType.Success); if (checkIsSuccess) { - Logger.Info("Checks for '{HealthCheckName}' all completed successfully.", checkName); + Logger.Info("Checks for '{HealthCheckName}' all completed successfully.", checkName); } else { - Logger.Warn("Checks for '{HealthCheckName}' completed with errors.", checkName); + Logger.Warn("Checks for '{HealthCheckName}' completed with errors.", checkName); } foreach (var checkResult in checkResults) { - Logger.Info("Result for {HealthCheckName}: {HealthCheckResult}, Message: '{HealthCheckMessage}'", checkName, checkResult.ResultType, checkResult.Message); + Logger.Info("Result for {HealthCheckName}: {HealthCheckResult}, Message: '{HealthCheckMessage}'", checkName, checkResult.ResultType, checkResult.Message); } } } diff --git a/src/Umbraco.Web/HtmlHelperRenderExtensions.cs b/src/Umbraco.Web/HtmlHelperRenderExtensions.cs index e19ae883e9..2f55f0a1b1 100644 --- a/src/Umbraco.Web/HtmlHelperRenderExtensions.cs +++ b/src/Umbraco.Web/HtmlHelperRenderExtensions.cs @@ -85,7 +85,7 @@ namespace Umbraco.Web var cacheKey = new StringBuilder(partialViewName); //let's always cache by the current culture to allow variants to have different cache results var cultureName = System.Threading.Thread.CurrentThread.CurrentUICulture.Name; - if (!String.IsNullOrEmpty(cultureName)) + if (!string.IsNullOrEmpty(cultureName)) { cacheKey.AppendFormat("{0}-", cultureName); } diff --git a/src/Umbraco.Web/HttpUrlHelperExtensions.cs b/src/Umbraco.Web/HttpUrlHelperExtensions.cs index 4e5533d327..9a2f394d90 100644 --- a/src/Umbraco.Web/HttpUrlHelperExtensions.cs +++ b/src/Umbraco.Web/HttpUrlHelperExtensions.cs @@ -103,7 +103,7 @@ namespace Umbraco.Web string routeName; if (area.IsNullOrWhiteSpace()) { - routeName = string.Format("umbraco-{0}-{1}", "api", controllerName); + routeName = $"umbraco-{"api"}-{controllerName}"; if (id == null) { return url.Route(routeName, new { controller = controllerName, action = actionName, httproute = "" }); @@ -115,7 +115,7 @@ namespace Umbraco.Web } else { - routeName = string.Format("umbraco-{0}-{1}-{2}", "api", area, controllerName); + routeName = $"umbraco-{"api"}-{area}-{controllerName}"; if (id == null) { return url.Route(routeName, new { controller = controllerName, action = actionName, httproute = "" }); diff --git a/src/Umbraco.Web/HybridEventMessagesAccessor.cs b/src/Umbraco.Web/HybridEventMessagesAccessor.cs index fddde403d8..ab67b4774f 100644 --- a/src/Umbraco.Web/HybridEventMessagesAccessor.cs +++ b/src/Umbraco.Web/HybridEventMessagesAccessor.cs @@ -12,8 +12,8 @@ namespace Umbraco.Web public EventMessages EventMessages { - get { return Value; } - set { Value = value; } + get => Value; + set => Value = value; } } } diff --git a/src/Umbraco.Web/ImageCropperTemplateExtensions.cs b/src/Umbraco.Web/ImageCropperTemplateExtensions.cs index 26dd2a5d36..dad2f9e3f3 100644 --- a/src/Umbraco.Web/ImageCropperTemplateExtensions.cs +++ b/src/Umbraco.Web/ImageCropperTemplateExtensions.cs @@ -9,6 +9,7 @@ using Umbraco.Core.Models; using Umbraco.Core.Models.PublishedContent; using Umbraco.Core.PropertyEditors.ValueConverters; using Umbraco.Web.Models; +using Umbraco.Core.Logging; namespace Umbraco.Web { @@ -253,26 +254,28 @@ namespace Umbraco.Web ImageCropRatioMode? ratioMode = null, bool upScale = true) => ImageCropperTemplateCoreExtensions.GetCropUrl(imageUrl, Current.ImageUrlGenerator, cropDataSet, width, height, cropAlias, quality, imageCropMode, imageCropAnchor, preferFocalPoint, useCropDimensions, cacheBusterValue, furtherOptions, ratioMode, upScale); + private static readonly JsonSerializerSettings ImageCropperValueJsonSerializerSettings = new JsonSerializerSettings + { + Culture = CultureInfo.InvariantCulture, + FloatParseHandling = FloatParseHandling.Decimal + }; internal static ImageCropperValue DeserializeImageCropperValue(this string json) { - var imageCrops = new ImageCropperValue(); + ImageCropperValue imageCrops = null; if (json.DetectIsJson()) { try { - imageCrops = JsonConvert.DeserializeObject(json, new JsonSerializerSettings - { - Culture = CultureInfo.InvariantCulture, - FloatParseHandling = FloatParseHandling.Decimal - }); + imageCrops = JsonConvert.DeserializeObject(json, ImageCropperValueJsonSerializerSettings); } catch (Exception ex) { - Current.Logger.Error(typeof(ImageCropperTemplateExtensions), ex, "Could not parse the json string: {Json}", json); + Current.Logger.Error(typeof(ImageCropperTemplateExtensions), ex, "Could not parse the json string: {Json}", json); } } + imageCrops = imageCrops ?? new ImageCropperValue(); return imageCrops; } } diff --git a/src/Umbraco.Web/Install/Controllers/InstallApiController.cs b/src/Umbraco.Web/Install/Controllers/InstallApiController.cs index d151df46f7..924bd3ff99 100644 --- a/src/Umbraco.Web/Install/Controllers/InstallApiController.cs +++ b/src/Umbraco.Web/Install/Controllers/InstallApiController.cs @@ -120,7 +120,7 @@ namespace Umbraco.Web.Install.Controllers catch (Exception ex) { - _logger.Error(ex, "An error occurred during installation step {Step}", step.Name); + _logger.Error(ex, "An error occurred during installation step {Step}", step.Name); if (ex is TargetInvocationException && ex.InnerException != null) { @@ -210,7 +210,7 @@ namespace Umbraco.Web.Install.Controllers } catch (Exception ex) { - _logger.Error(ex, "Checking if step requires execution ({Step}) failed.", step.Name); + _logger.Error(ex, "Checking if step requires execution ({Step}) failed.", step.Name); throw; } } @@ -232,7 +232,7 @@ namespace Umbraco.Web.Install.Controllers } catch (Exception ex) { - _logger.Error(ex, "Installation step {Step} failed.", step.Name); + _logger.Error(ex, "Installation step {Step} failed.", step.Name); throw; } } diff --git a/src/Umbraco.Web/JavaScript/ClientDependencyConfiguration.cs b/src/Umbraco.Web/JavaScript/ClientDependencyConfiguration.cs index 2bf069b06d..019c7e29a6 100644 --- a/src/Umbraco.Web/JavaScript/ClientDependencyConfiguration.cs +++ b/src/Umbraco.Web/JavaScript/ClientDependencyConfiguration.cs @@ -79,7 +79,7 @@ namespace Umbraco.Web.JavaScript versionAttribute.SetValue(newVersion); clientDependencyConfigXml.Save(_fileName, SaveOptions.DisableFormatting); - _logger.Info("Updated version number from {OldVersion} to {NewVersion}", oldVersion, newVersion); + _logger.Info("Updated version number from {OldVersion} to {NewVersion}", oldVersion, newVersion); return true; } } diff --git a/src/Umbraco.Web/Logging/WebProfiler.cs b/src/Umbraco.Web/Logging/WebProfiler.cs index 512edb2296..740afba6a7 100755 --- a/src/Umbraco.Web/Logging/WebProfiler.cs +++ b/src/Umbraco.Web/Logging/WebProfiler.cs @@ -26,12 +26,16 @@ namespace Umbraco.Web.Logging _provider = new WebProfilerProvider(); //see https://miniprofiler.com/dotnet/AspDotNet - MiniProfiler.Configure(new MiniProfilerOptions + var options = new MiniProfilerOptions { SqlFormatter = new SqlServerFormatter(), - StackMaxLength = 5000, + StackMaxLength = 5000, ProfilerProvider = _provider - }); + }; + // this is a default path and by default it performs a 'contains' check which will match our content controller + // (and probably other requests) and ignore them. + options.IgnoredPaths.Remove("/content/"); + MiniProfiler.Configure(options); } public void UmbracoApplicationBeginRequest(object sender, EventArgs e) diff --git a/src/Umbraco.Web/Logging/WebProfilerProvider.cs b/src/Umbraco.Web/Logging/WebProfilerProvider.cs index dbb81cf232..f38a606745 100755 --- a/src/Umbraco.Web/Logging/WebProfilerProvider.cs +++ b/src/Umbraco.Web/Logging/WebProfilerProvider.cs @@ -85,7 +85,11 @@ namespace Umbraco.Web.Logging public override MiniProfiler Start(string profilerName, MiniProfilerBaseOptions options) { var first = Interlocked.Exchange(ref _first, 1) == 0; - if (first == false) return base.Start(profilerName, options); + if (first == false) + { + var profiler = base.Start(profilerName, options); + return profiler; + } _startupProfiler = new MiniProfiler("StartupProfiler", options); CurrentProfiler = _startupProfiler; diff --git a/src/Umbraco.Web/Macros/MacroRenderer.cs b/src/Umbraco.Web/Macros/MacroRenderer.cs index 84c883b406..e93a29723e 100755 --- a/src/Umbraco.Web/Macros/MacroRenderer.cs +++ b/src/Umbraco.Web/Macros/MacroRenderer.cs @@ -89,7 +89,7 @@ namespace Umbraco.Web.Macros if (macroContent == null) return null; - _plogger.Debug("Macro content loaded from cache '{MacroCacheId}'", model.CacheIdentifier); + _plogger.Debug("Macro content loaded from cache '{MacroCacheId}'", model.CacheIdentifier); // ensure that the source has not changed // note: does not handle dependencies, and never has @@ -149,7 +149,7 @@ namespace Umbraco.Web.Macros priority: CacheItemPriority.NotRemovable ); - _plogger.Debug("Macro content saved to cache '{MacroCacheId}'", model.CacheIdentifier); + _plogger.Debug("Macro content saved to cache '{MacroCacheId}'", model.CacheIdentifier); } // gets the macro source file name @@ -284,7 +284,7 @@ namespace Umbraco.Web.Macros } catch (Exception e) { - _plogger.Warn(e, "Failed {MsgIn}", msgIn); + _plogger.Warn(e, "Failed {MsgIn}", msgIn); var macroErrorEventArgs = new MacroErrorEventArgs { diff --git a/src/Umbraco.Web/Media/UploadAutoFillProperties.cs b/src/Umbraco.Web/Media/UploadAutoFillProperties.cs index 60de03b450..4e0201ad99 100644 --- a/src/Umbraco.Web/Media/UploadAutoFillProperties.cs +++ b/src/Umbraco.Web/Media/UploadAutoFillProperties.cs @@ -74,7 +74,7 @@ namespace Umbraco.Web.Media } catch (Exception ex) { - _logger.Error(typeof(UploadAutoFillProperties), ex, "Could not populate upload auto-fill properties for file '{File}'.", filepath); + _logger.Error(typeof(UploadAutoFillProperties), ex, "Could not populate upload auto-fill properties for file '{File}'.", filepath); ResetProperties(content, autoFillConfig, culture, segment); } } diff --git a/src/Umbraco.Web/ModelStateExtensions.cs b/src/Umbraco.Web/ModelStateExtensions.cs index e224a6122b..2b0cd01e21 100644 --- a/src/Umbraco.Web/ModelStateExtensions.cs +++ b/src/Umbraco.Web/ModelStateExtensions.cs @@ -24,11 +24,11 @@ namespace Umbraco.Web { if (dictionary == null) return; - foreach (var keyValuePair in dictionary + foreach (var (key, value) in dictionary //It can either equal the prefix exactly (model level errors) or start with the prefix. (property level errors) .Where(keyValuePair => keyValuePair.Key == prefix || keyValuePair.Key.StartsWith(prefix + "."))) { - state[keyValuePair.Key] = keyValuePair.Value; + state[key] = value; } } diff --git a/src/Umbraco.Web/Models/ContentEditing/SearchResult.cs b/src/Umbraco.Web/Models/ContentEditing/SearchResult.cs index 1cdd539165..d33bc3530e 100644 --- a/src/Umbraco.Web/Models/ContentEditing/SearchResult.cs +++ b/src/Umbraco.Web/Models/ContentEditing/SearchResult.cs @@ -16,6 +16,6 @@ namespace Umbraco.Web.Models.ContentEditing public int FieldCount => Values?.Count ?? 0; [DataMember(Name = "values")] - public IReadOnlyDictionary Values { get; set; } + public IReadOnlyDictionary> Values { get; set; } } } diff --git a/src/Umbraco.Web/Models/Mapping/ContentMapDefinition.cs b/src/Umbraco.Web/Models/Mapping/ContentMapDefinition.cs index 16465914cb..0360ee568b 100644 --- a/src/Umbraco.Web/Models/Mapping/ContentMapDefinition.cs +++ b/src/Umbraco.Web/Models/Mapping/ContentMapDefinition.cs @@ -2,6 +2,8 @@ using System.Collections.Generic; using System.Linq; using Umbraco.Core; +using Umbraco.Core.Cache; +using Umbraco.Core.Composing; using Umbraco.Core.Logging; using Umbraco.Core.Mapping; using Umbraco.Core.Models; @@ -29,14 +31,25 @@ namespace Umbraco.Web.Models.Mapping private readonly ILogger _logger; private readonly IUserService _userService; private readonly IEntityService _entityService; + private readonly AppCaches _appCaches; private readonly TabsAndPropertiesMapper _tabsAndPropertiesMapper; private readonly ContentSavedStateMapper _stateMapper; private readonly ContentBasicSavedStateMapper _basicStateMapper; private readonly ContentVariantMapper _contentVariantMapper; - public ContentMapDefinition(CommonMapper commonMapper, ILocalizedTextService localizedTextService, IContentService contentService, IContentTypeService contentTypeService, - IFileService fileService, IUmbracoContextAccessor umbracoContextAccessor, IPublishedRouter publishedRouter, ILocalizationService localizationService, ILogger logger, - IUserService userService, IEntityService entityService) + public ContentMapDefinition( + CommonMapper commonMapper, + ILocalizedTextService localizedTextService, + IContentService contentService, + IContentTypeService contentTypeService, + IFileService fileService, + IUmbracoContextAccessor umbracoContextAccessor, + IPublishedRouter publishedRouter, + ILocalizationService localizationService, + ILogger logger, + IUserService userService, + IEntityService entityService, + AppCaches appCaches) { _commonMapper = commonMapper; _localizedTextService = localizedTextService; @@ -49,6 +62,7 @@ namespace Umbraco.Web.Models.Mapping _logger = logger; _userService = userService; _entityService = entityService; + _appCaches = appCaches; _tabsAndPropertiesMapper = new TabsAndPropertiesMapper(localizedTextService); _stateMapper = new ContentSavedStateMapper(); _basicStateMapper = new ContentBasicSavedStateMapper(); @@ -238,7 +252,7 @@ namespace Umbraco.Web.Models.Mapping // false here. if (context.HasItems && context.Items.TryGetValue("CurrentUser", out var usr) && usr is IUser currentUser) { - userStartNodes = currentUser.CalculateContentStartNodeIds(_entityService); + userStartNodes = currentUser.CalculateContentStartNodeIds(_entityService, _appCaches); if (!userStartNodes.Contains(Constants.System.Root)) { // return false if this is the user's actual start node, the node will be rendered in the tree diff --git a/src/Umbraco.Web/Models/Mapping/ContentPropertyBasicMapper.cs b/src/Umbraco.Web/Models/Mapping/ContentPropertyBasicMapper.cs index 2e035430df..bcfeb5e330 100644 --- a/src/Umbraco.Web/Models/Mapping/ContentPropertyBasicMapper.cs +++ b/src/Umbraco.Web/Models/Mapping/ContentPropertyBasicMapper.cs @@ -38,7 +38,7 @@ namespace Umbraco.Web.Models.Mapping var editor = _propertyEditors[property.PropertyType.PropertyEditorAlias]; if (editor == null) { - _logger.Error>( + _logger.Error, string>( new NullReferenceException("The property editor with alias " + property.PropertyType.PropertyEditorAlias + " does not exist"), "No property editor '{PropertyEditorAlias}' found, converting to a Label", property.PropertyType.PropertyEditorAlias); diff --git a/src/Umbraco.Web/Models/Mapping/DataTypeMapDefinition.cs b/src/Umbraco.Web/Models/Mapping/DataTypeMapDefinition.cs index 6b5797e05b..817d99c0fd 100644 --- a/src/Umbraco.Web/Models/Mapping/DataTypeMapDefinition.cs +++ b/src/Umbraco.Web/Models/Mapping/DataTypeMapDefinition.cs @@ -173,7 +173,7 @@ namespace Umbraco.Web.Models.Mapping else { // weird - just leave the field without a value - but warn - _logger.Warn("Could not find a value for configuration field '{ConfigField}'", field.Key); + _logger.Warn("Could not find a value for configuration field '{ConfigField}'", field.Key); } } } diff --git a/src/Umbraco.Web/Models/Mapping/MacroMapDefinition.cs b/src/Umbraco.Web/Models/Mapping/MacroMapDefinition.cs index e654fc16a1..216e1e100e 100644 --- a/src/Umbraco.Web/Models/Mapping/MacroMapDefinition.cs +++ b/src/Umbraco.Web/Models/Mapping/MacroMapDefinition.cs @@ -73,7 +73,7 @@ namespace Umbraco.Web.Models.Mapping { //we'll just map this to a text box paramEditor = _parameterEditors[Constants.PropertyEditors.Aliases.TextBox]; - _logger.Warn("Could not resolve a parameter editor with alias {PropertyEditorAlias}, a textbox will be rendered in it's place", source.EditorAlias); + _logger.Warn("Could not resolve a parameter editor with alias {PropertyEditorAlias}, a textbox will be rendered in it's place", source.EditorAlias); } target.View = paramEditor.GetValueEditor().View; diff --git a/src/Umbraco.Web/Models/Mapping/UserMapDefinition.cs b/src/Umbraco.Web/Models/Mapping/UserMapDefinition.cs index 924ffda554..6198630898 100644 --- a/src/Umbraco.Web/Models/Mapping/UserMapDefinition.cs +++ b/src/Umbraco.Web/Models/Mapping/UserMapDefinition.cs @@ -273,8 +273,8 @@ namespace Umbraco.Web.Models.Mapping { target.AvailableCultures = _textService.GetSupportedCultures().ToDictionary(x => x.Name, x => x.DisplayName); target.Avatars = source.GetUserAvatarUrls(_appCaches.RuntimeCache); - target.CalculatedStartContentIds = GetStartNodes(source.CalculateContentStartNodeIds(_entityService), UmbracoObjectTypes.Document, "content","contentRoot", context); - target.CalculatedStartMediaIds = GetStartNodes(source.CalculateMediaStartNodeIds(_entityService), UmbracoObjectTypes.Media, "media","mediaRoot", context); + target.CalculatedStartContentIds = GetStartNodes(source.CalculateContentStartNodeIds(_entityService,_appCaches), UmbracoObjectTypes.Document, "content","contentRoot", context); + target.CalculatedStartMediaIds = GetStartNodes(source.CalculateMediaStartNodeIds(_entityService, _appCaches), UmbracoObjectTypes.Media, "media","mediaRoot", context); target.CreateDate = source.CreateDate; target.Culture = source.GetUserCulture(_textService, _globalSettings).ToString(); target.Email = source.Email; @@ -327,8 +327,8 @@ namespace Umbraco.Web.Models.Mapping target.Email = source.Email; target.EmailHash = source.Email.ToLowerInvariant().Trim().GenerateHash(); target.Name = source.Name; - target.StartContentIds = source.CalculateContentStartNodeIds(_entityService); - target.StartMediaIds = source.CalculateMediaStartNodeIds(_entityService); + target.StartContentIds = source.CalculateContentStartNodeIds(_entityService, _appCaches); + target.StartMediaIds = source.CalculateMediaStartNodeIds(_entityService, _appCaches); target.UserId = source.Id; //we need to map the legacy UserType diff --git a/src/Umbraco.Web/Mvc/JsonNetResult.cs b/src/Umbraco.Web/Mvc/JsonNetResult.cs index da6780451e..3dd6c2f398 100644 --- a/src/Umbraco.Web/Mvc/JsonNetResult.cs +++ b/src/Umbraco.Web/Mvc/JsonNetResult.cs @@ -22,10 +22,19 @@ namespace Umbraco.Web.Mvc public JsonSerializerSettings SerializerSettings { get; set; } public Formatting Formatting { get; set; } + /// + /// Default, unchanged JsonSerializerSettings + /// + public static readonly JsonSerializerSettings DefaultJsonSerializerSettings = new JsonSerializerSettings(); + public JsonNetResult() { SerializerSettings = new JsonSerializerSettings(); } + public JsonNetResult(JsonSerializerSettings jsonSerializerSettings) + { + SerializerSettings = jsonSerializerSettings; + } public override void ExecuteResult(ControllerContext context) { diff --git a/src/Umbraco.Web/Mvc/RenderMvcController.cs b/src/Umbraco.Web/Mvc/RenderMvcController.cs index 64c9ad52c4..7c0345c300 100644 --- a/src/Umbraco.Web/Mvc/RenderMvcController.cs +++ b/src/Umbraco.Web/Mvc/RenderMvcController.cs @@ -68,7 +68,7 @@ namespace Umbraco.Web.Mvc var result = ViewEngines.Engines.FindView(ControllerContext, template, null); if (result.View != null) return true; - Logger.Warn("No physical template file was found for template {Template}", template); + Logger.Warn("No physical template file was found for template {Template}", template); return false; } diff --git a/src/Umbraco.Web/PropertyEditors/BlockEditorPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/BlockEditorPropertyEditor.cs index 705d4a706d..52cdc98718 100644 --- a/src/Umbraco.Web/PropertyEditors/BlockEditorPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/BlockEditorPropertyEditor.cs @@ -148,7 +148,7 @@ namespace Umbraco.Web.PropertyEditors { // deal with weird situations by ignoring them (no comment) row.PropertyValues.Remove(prop.Key); - _logger.Warn( + _logger.Warn( "ToEditor removed property value {PropertyKey} in row {RowId} for property type {PropertyTypeAlias}", prop.Key, row.Key, property.PropertyType.Alias); continue; diff --git a/src/Umbraco.Web/PropertyEditors/EyeDropperColorPickerConfigurationEditor.cs b/src/Umbraco.Web/PropertyEditors/EyeDropperColorPickerConfigurationEditor.cs new file mode 100644 index 0000000000..55867feddc --- /dev/null +++ b/src/Umbraco.Web/PropertyEditors/EyeDropperColorPickerConfigurationEditor.cs @@ -0,0 +1,53 @@ +using System.Collections.Generic; +using Umbraco.Core; +using Umbraco.Core.PropertyEditors; + +namespace Umbraco.Web.PropertyEditors +{ + internal class EyeDropperColorPickerConfigurationEditor : ConfigurationEditor + { + public EyeDropperColorPickerConfigurationEditor() + { + + } + + /// + public override Dictionary ToConfigurationEditor(EyeDropperColorPickerConfiguration configuration) + { + return new Dictionary + { + { "showAlpha", configuration.ShowAlpha }, + { "showPalette", configuration.ShowPalette } + }; + } + + /// + public override EyeDropperColorPickerConfiguration FromConfigurationEditor(IDictionary editorValues, EyeDropperColorPickerConfiguration configuration) + { + var output = new EyeDropperColorPickerConfiguration(); + + var showAlpha = true; + var showPalette = true; + + if (editorValues.TryGetValue("showAlpha", out var alpha)) + { + var attempt = alpha.TryConvertTo(); + if (attempt.Success) + showAlpha = attempt.Result; + } + + if (editorValues.TryGetValue("showPalette", out var palette)) + { + var attempt = palette.TryConvertTo(); + if (attempt.Success) + showPalette = attempt.Result; + } + + return new EyeDropperColorPickerConfiguration + { + ShowAlpha = showAlpha, + ShowPalette = showPalette + }; + } + } +} diff --git a/src/Umbraco.Web/PropertyEditors/EyeDropperColorPickerPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/EyeDropperColorPickerPropertyEditor.cs new file mode 100644 index 0000000000..cf0d239523 --- /dev/null +++ b/src/Umbraco.Web/PropertyEditors/EyeDropperColorPickerPropertyEditor.cs @@ -0,0 +1,22 @@ +using Umbraco.Core; +using Umbraco.Core.Logging; +using Umbraco.Core.PropertyEditors; + +namespace Umbraco.Web.PropertyEditors +{ + [DataEditor( + Constants.PropertyEditors.Aliases.ColorPickerEyeDropper, + "Eye Dropper Color Picker", + "eyedropper", + Icon = "icon-colorpicker", + Group = Constants.PropertyEditors.Groups.Pickers)] + public class EyeDropperColorPickerPropertyEditor : DataEditor + { + public EyeDropperColorPickerPropertyEditor(ILogger logger) + : base(logger) + { } + + /// + protected override IConfigurationEditor CreateConfigurationEditor() => new EyeDropperColorPickerConfigurationEditor(); + } +} diff --git a/src/Umbraco.Web/PropertyEditors/GridPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/GridPropertyEditor.cs index 862837381a..f9eacd9e73 100644 --- a/src/Umbraco.Web/PropertyEditors/GridPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/GridPropertyEditor.cs @@ -202,8 +202,8 @@ namespace Umbraco.Web.PropertyEditors _richTextPropertyValueEditor.GetReferences(x.Value))) yield return umbracoEntityReference; - foreach (var umbracoEntityReference in mediaValues.SelectMany(x => - _mediaPickerPropertyValueEditor.GetReferences(x.Value["udi"]))) + foreach (var umbracoEntityReference in mediaValues.Where(x => x.Value.HasValues) + .SelectMany(x => _mediaPickerPropertyValueEditor.GetReferences(x.Value["udi"]))) yield return umbracoEntityReference; } } diff --git a/src/Umbraco.Web/PropertyEditors/ImageCropperPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/ImageCropperPropertyEditor.cs index b0e5bf30bd..e66af480f8 100644 --- a/src/Umbraco.Web/PropertyEditors/ImageCropperPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/ImageCropperPropertyEditor.cs @@ -91,7 +91,7 @@ namespace Umbraco.Web.PropertyEditors catch (Exception ex) { if (writeLog) - Logger.Error(ex, "Could not parse image cropper value '{Json}'", value); + Logger.Error(ex, "Could not parse image cropper value '{Json}'", value); return null; } } diff --git a/src/Umbraco.Web/PropertyEditors/ListViewConfiguration.cs b/src/Umbraco.Web/PropertyEditors/ListViewConfiguration.cs index de538793a5..2ea7b9e44e 100644 --- a/src/Umbraco.Web/PropertyEditors/ListViewConfiguration.cs +++ b/src/Umbraco.Web/PropertyEditors/ListViewConfiguration.cs @@ -28,7 +28,7 @@ namespace Umbraco.Web.PropertyEditors Layouts = new[] { new Layout { Name = "List", Icon = "icon-list", IsSystem = 1, Selected = true, Path = "views/propertyeditors/listview/layouts/list/list.html" }, - new Layout { Name = "grid", Icon = "icon-thumbnails-small", IsSystem = 1, Selected = true, Path = "views/propertyeditors/listview/layouts/grid/grid.html" } + new Layout { Name = "Grid", Icon = "icon-thumbnails-small", IsSystem = 1, Selected = true, Path = "views/propertyeditors/listview/layouts/grid/grid.html" } }; IncludeProperties = new [] diff --git a/src/Umbraco.Web/PropertyEditors/MultiUrlPickerValueEditor.cs b/src/Umbraco.Web/PropertyEditors/MultiUrlPickerValueEditor.cs index 5a84e4b20c..aae691f624 100644 --- a/src/Umbraco.Web/PropertyEditors/MultiUrlPickerValueEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/MultiUrlPickerValueEditor.cs @@ -115,12 +115,16 @@ namespace Umbraco.Web.PropertyEditors } catch (Exception ex) { - _logger.Error("Error getting links", ex); + _logger.Error(ex, "Error getting links"); } return base.ToEditor(property, dataTypeService, culture, segment); } + private static readonly JsonSerializerSettings LinkDisplayJsonSerializerSettings = new JsonSerializerSettings + { + NullValueHandling = NullValueHandling.Ignore + }; public override object FromEditor(ContentPropertyData editorValue, object currentValue) { @@ -142,15 +146,12 @@ namespace Umbraco.Web.PropertyEditors Target = link.Target, Udi = link.Udi, Url = link.Udi == null ? link.Url : null, // only save the URL for external links - }, - new JsonSerializerSettings - { - NullValueHandling = NullValueHandling.Ignore - }); + }, LinkDisplayJsonSerializerSettings + ); } catch (Exception ex) { - _logger.Error("Error saving links", ex); + _logger.Error(ex, "Error saving links"); } return base.FromEditor(editorValue, currentValue); diff --git a/src/Umbraco.Web/PropertyEditors/NestedContentPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/NestedContentPropertyEditor.cs index ca3c5a2a04..1047c4317d 100644 --- a/src/Umbraco.Web/PropertyEditors/NestedContentPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/NestedContentPropertyEditor.cs @@ -126,7 +126,7 @@ namespace Umbraco.Web.PropertyEditors { // deal with weird situations by ignoring them (no comment) row.RawPropertyValues.Remove(prop.Key); - _logger.Warn( + _logger.Warn( ex, "ConvertDbToString removed property value {PropertyKey} in row {RowId} for property type {PropertyTypeAlias}", prop.Key, row.Id, propertyType.Alias); @@ -195,7 +195,7 @@ namespace Umbraco.Web.PropertyEditors { // deal with weird situations by ignoring them (no comment) row.RawPropertyValues.Remove(prop.Key); - _logger.Warn( + _logger.Warn( ex, "ToEditor removed property value {PropertyKey} in row {RowId} for property type {PropertyTypeAlias}", prop.Key, row.Id, property.PropertyType.Alias); diff --git a/src/Umbraco.Web/PropertyEditors/RichTextEditorPastedImages.cs b/src/Umbraco.Web/PropertyEditors/RichTextEditorPastedImages.cs index 5a3334685c..c21e2dae20 100644 --- a/src/Umbraco.Web/PropertyEditors/RichTextEditorPastedImages.cs +++ b/src/Umbraco.Web/PropertyEditors/RichTextEditorPastedImages.cs @@ -132,7 +132,7 @@ namespace Umbraco.Web.PropertyEditors } catch (Exception ex) { - _logger.Error(typeof(HtmlImageSourceParser), ex, "Could not delete temp file or folder {FileName}", absoluteTempImagePath); + _logger.Error(typeof(HtmlImageSourceParser), ex, "Could not delete temp file or folder {FileName}", absoluteTempImagePath); } } } diff --git a/src/Umbraco.Web/PropertyEditors/RteEmbedController.cs b/src/Umbraco.Web/PropertyEditors/RteEmbedController.cs index ca3bd36374..9c9cd929bf 100644 --- a/src/Umbraco.Web/PropertyEditors/RteEmbedController.cs +++ b/src/Umbraco.Web/PropertyEditors/RteEmbedController.cs @@ -60,7 +60,7 @@ namespace Umbraco.Web.PropertyEditors } catch(Exception ex) { - Logger.Error(ex, "Error embedding URL {Url} - width: {Width} height: {Height}", url, width, height); + Logger.Error(ex, "Error embedding url {Url} - width: {Width} height: {Height}", url, width, height); result.OEmbedStatus = OEmbedStatus.Error; } diff --git a/src/Umbraco.Web/PublishedCache/NuCache/ContentStore.cs b/src/Umbraco.Web/PublishedCache/NuCache/ContentStore.cs index f25e7fa1c3..1de53e9c6e 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/ContentStore.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/ContentStore.cs @@ -598,7 +598,7 @@ namespace Umbraco.Web.PublishedCache.NuCache throw new ArgumentException("Kit content cannot have children.", nameof(kit)); // ReSharper restore LocalizableElement - _logger.Debug("Set content ID: {KitNodeId}", kit.Node.Id); + _logger.Debug("Set content ID: {KitNodeId}", kit.Node.Id); // get existing _contentNodes.TryGetValue(kit.Node.Id, out var link); @@ -863,7 +863,7 @@ namespace Umbraco.Web.PublishedCache.NuCache if (link?.Value == null) return false; var content = link.Value; - _logger.Debug("Clear content ID: {ContentId}", content.Id); + _logger.Debug("Clear content ID: {ContentId}", content.Id); // clear the entire branch ClearBranchLocked(content); @@ -1670,7 +1670,7 @@ namespace Umbraco.Web.PublishedCache.NuCache { if (_gen < 0) return; #if DEBUG - _logger.Debug("Dispose snapshot ({Snapshot})", _genRef?.GenObj.Count.ToString() ?? "live"); + _logger.Debug("Dispose snapshot ({Snapshot})", _genRef?.GenObj.Count.ToString() ?? "live"); #endif _gen = -1; if (_genRef != null) diff --git a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/DatabaseDataSource.cs b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/DatabaseDataSource.cs index f62014a368..8079363548 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/DatabaseDataSource.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/DatabaseDataSource.cs @@ -209,7 +209,7 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource { if (Debugger.IsAttached) throw new InvalidOperationException("Missing cmsContentNu edited content for node " + dto.Id + ", consider rebuilding."); - Current.Logger.Warn("Missing cmsContentNu edited content for node {NodeId}, consider rebuilding.", dto.Id); + Current.Logger.Warn("Missing cmsContentNu edited content for node {NodeId}, consider rebuilding.", dto.Id); } else { @@ -236,7 +236,7 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource { if (Debugger.IsAttached) throw new InvalidOperationException("Missing cmsContentNu published content for node " + dto.Id + ", consider rebuilding."); - Current.Logger.Warn("Missing cmsContentNu published content for node {NodeId}, consider rebuilding.", dto.Id); + Current.Logger.Warn("Missing cmsContentNu published content for node {NodeId}, consider rebuilding.", dto.Id); } else { @@ -303,17 +303,18 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource return s; } + private static readonly JsonSerializerSettings NestedContentDataJsonSerializerSettings = new JsonSerializerSettings + { + Converters = new List { new ForceInt32Converter() } + }; + private static ContentNestedData DeserializeNestedData(string data) { // by default JsonConvert will deserialize our numeric values as Int64 // which is bad, because they were Int32 in the database - take care - var settings = new JsonSerializerSettings - { - Converters = new List { new ForceInt32Converter() } - }; - return JsonConvert.DeserializeObject(data, settings); + return JsonConvert.DeserializeObject(data, NestedContentDataJsonSerializerSettings); } } } diff --git a/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs b/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs index a39e26e2b1..15e45cfa71 100755 --- a/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs @@ -131,9 +131,9 @@ namespace Umbraco.Web.PublishedCache.NuCache // stores need to be populated, happens in OnResolutionFrozen which uses _localDbExists to // figure out whether it can read the databases or it should populate them from sql - _logger.Info("Creating the content store, localContentDbExists? {LocalContentDbExists}", _localContentDbExists); + _logger.Info("Creating the content store, localContentDbExists? {LocalContentDbExists}", _localContentDbExists); _contentStore = new ContentStore(publishedSnapshotAccessor, variationContextAccessor, logger, _localContentDb); - _logger.Info("Creating the media store, localMediaDbExists? {LocalMediaDbExists}", _localMediaDbExists); + _logger.Info("Creating the media store, localMediaDbExists? {LocalMediaDbExists}", _localMediaDbExists); _mediaStore = new ContentStore(publishedSnapshotAccessor, variationContextAccessor, logger, _localMediaDb); } else @@ -182,7 +182,7 @@ namespace Umbraco.Web.PublishedCache.NuCache _localContentDb = BTree.GetTree(localContentDbPath, _localContentDbExists); _localMediaDb = BTree.GetTree(localMediaDbPath, _localMediaDbExists); - _logger.Info("Registered with MainDom, localContentDbExists? {LocalContentDbExists}, localMediaDbExists? {LocalMediaDbExists}", _localContentDbExists, _localMediaDbExists); + _logger.Info("Registered with MainDom, localContentDbExists? {LocalContentDbExists}, localMediaDbExists? {LocalMediaDbExists}", _localContentDbExists, _localMediaDbExists); } /// @@ -690,7 +690,7 @@ namespace Umbraco.Web.PublishedCache.NuCache foreach (var payload in payloads) { - _logger.Debug("Notified {ChangeTypes} for content {ContentId}", payload.ChangeTypes, payload.Id); + _logger.Debug("Notified {ChangeTypes} for content {ContentId}", payload.ChangeTypes, payload.Id); if (payload.ChangeTypes.HasType(TreeChangeTypes.RefreshAll)) { @@ -783,7 +783,7 @@ namespace Umbraco.Web.PublishedCache.NuCache foreach (var payload in payloads) { - _logger.Debug("Notified {ChangeTypes} for media {MediaId}", payload.ChangeTypes, payload.Id); + _logger.Debug("Notified {ChangeTypes} for media {MediaId}", payload.ChangeTypes, payload.Id); if (payload.ChangeTypes.HasType(TreeChangeTypes.RefreshAll)) { @@ -854,7 +854,7 @@ namespace Umbraco.Web.PublishedCache.NuCache return; foreach (var payload in payloads) - _logger.Debug("Notified {ChangeTypes} for {ItemType} {ItemId}", payload.ChangeTypes, payload.ItemType, payload.Id); + _logger.Debug("Notified {ChangeTypes} for {ItemType} {ItemId}", payload.ChangeTypes, payload.ItemType, payload.Id); Notify(_contentStore, payloads, RefreshContentTypesLocked); Notify(_mediaStore, payloads, RefreshMediaTypesLocked); @@ -938,7 +938,7 @@ namespace Umbraco.Web.PublishedCache.NuCache var idsA = payloads.Select(x => x.Id).ToArray(); foreach (var payload in payloads) - _logger.Debug("Notified {RemovedStatus} for data type {DataTypeId}", + _logger.Debug("Notified {RemovedStatus} for data type {DataTypeId}", payload.Removed ? "Removed" : "Refreshed", payload.Id); diff --git a/src/Umbraco.Web/PublishedCache/PublishedContentTypeCache.cs b/src/Umbraco.Web/PublishedCache/PublishedContentTypeCache.cs index 8eb50b0588..0f6e9af6bd 100644 --- a/src/Umbraco.Web/PublishedCache/PublishedContentTypeCache.cs +++ b/src/Umbraco.Web/PublishedCache/PublishedContentTypeCache.cs @@ -72,7 +72,7 @@ namespace Umbraco.Web.PublishedCache /// An identifier. public void ClearContentType(int id) { - _logger.Debug("Clear content type w/id {ContentTypeId}", id); + _logger.Debug("Clear content type w/id {ContentTypeId}", id); try { @@ -107,7 +107,7 @@ namespace Umbraco.Web.PublishedCache /// A data type identifier. public void ClearDataType(int id) { - _logger.Debug("Clear data type w/id {DataTypeId}.", id); + _logger.Debug("Clear data type w/id {DataTypeId}.", id); // there is no recursion to handle here because a PublishedContentType contains *all* its // properties ie both its own properties and those that were inherited (it's based upon an diff --git a/src/Umbraco.Web/PublishedContentExtensions.cs b/src/Umbraco.Web/PublishedContentExtensions.cs index f4c11977dc..c851894149 100644 --- a/src/Umbraco.Web/PublishedContentExtensions.cs +++ b/src/Umbraco.Web/PublishedContentExtensions.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Data; using System.Linq; @@ -174,7 +174,7 @@ namespace Umbraco.Web // else... if we have a property, at least let the converter return its own // vision of 'no value' (could be an empty enumerable) - otherwise, default - return property == null ? default : property.Value(culture, segment); + return property == null ? default : property.Value(culture, segment, fallback, defaultValue); } #endregion @@ -814,6 +814,64 @@ namespace Umbraco.Web #endregion + #region Axes: breadcrumbs + + /// + /// Gets the breadcrumbs (ancestors and self, top to bottom) for the specified . + /// + /// The content. + /// Indicates whether the specified content should be included. + /// + /// The breadcrumbs (ancestors and self, top to bottom) for the specified . + /// + public static IEnumerable Breadcrumbs(this IPublishedContent content, bool andSelf = true) + { + return content.AncestorsOrSelf(andSelf, null).Reverse(); + } + + /// + /// Gets the breadcrumbs (ancestors and self, top to bottom) for the specified at a level higher or equal to . + /// + /// The content. + /// The minimum level. + /// Indicates whether the specified content should be included. + /// + /// The breadcrumbs (ancestors and self, top to bottom) for the specified at a level higher or equal to . + /// + public static IEnumerable Breadcrumbs(this IPublishedContent content, int minLevel, bool andSelf = true) + { + return content.AncestorsOrSelf(andSelf, n => n.Level >= minLevel).Reverse(); + } + + /// + /// Gets the breadcrumbs (ancestors and self, top to bottom) for the specified at a level higher or equal to the specified root content type . + /// + /// The root content type. + /// The content. + /// Indicates whether the specified content should be included. + /// + /// The breadcrumbs (ancestors and self, top to bottom) for the specified at a level higher or equal to the specified root content type . + /// + public static IEnumerable Breadcrumbs(this IPublishedContent content, bool andSelf = true) + where T : class, IPublishedContent + { + static IEnumerable TakeUntil(IEnumerable source, Func predicate) + { + foreach (var item in source) + { + yield return item; + if (predicate(item)) + { + yield break; + } + } + } + + return TakeUntil(content.AncestorsOrSelf(andSelf, null), n => n is T).Reverse(); + } + + #endregion + #region Axes: descendants, descendants-or-self /// @@ -1271,15 +1329,37 @@ namespace Umbraco.Web #region Axes: custom /// - /// Gets the root content for this content. + /// Gets the root content (ancestor or self at level 1) for the specified . /// /// The content. - /// The 'site' content ie AncestorOrSelf(1). + /// + /// The root content (ancestor or self at level 1) for the specified . + /// + /// + /// This is the same as calling with maxLevel set to 1. + /// public static IPublishedContent Root(this IPublishedContent content) { return content.AncestorOrSelf(1); } + /// + /// Gets the root content (ancestor or self at level 1) for the specified if it's of the specified content type . + /// + /// The content type. + /// The content. + /// + /// The root content (ancestor or self at level 1) for the specified of content type . + /// + /// + /// This is the same as calling with maxLevel set to 1. + /// + public static T Root(this IPublishedContent content) + where T : class, IPublishedContent + { + return content.AncestorOrSelf(1); + } + #endregion #region PropertyAliasesAndNames diff --git a/src/Umbraco.Web/PublishedPropertyExtension.cs b/src/Umbraco.Web/PublishedPropertyExtension.cs index b431f24828..6e8647db47 100644 --- a/src/Umbraco.Web/PublishedPropertyExtension.cs +++ b/src/Umbraco.Web/PublishedPropertyExtension.cs @@ -1,4 +1,3 @@ -using System.Collections.Generic; using Umbraco.Core; using Umbraco.Core.Composing; using Umbraco.Core.Models.PublishedContent; @@ -37,9 +36,16 @@ namespace Umbraco.Web // we have a value // try to cast or convert it var value = property.GetValue(culture, segment); - if (value is T valueAsT) return valueAsT; + if (value is T valueAsT) + { + return valueAsT; + } + var valueConverted = value.TryConvertTo(); - if (valueConverted) return valueConverted.Result; + if (valueConverted) + { + return valueConverted.Result; + } // cannot cast nor convert the value, nothing we can return but 'default' // note: we don't want to fallback in that case - would make little sense @@ -48,14 +54,28 @@ namespace Umbraco.Web // we don't have a value, try fallback if (PublishedValueFallback.TryGetValue(property, culture, segment, fallback, defaultValue, out var fallbackValue)) + { return fallbackValue; + } // we don't have a value - neither direct nor fallback // give a chance to the converter to return something (eg empty enumerable) var noValue = property.GetValue(culture, segment); - if (noValue is T noValueAsT) return noValueAsT; + if (noValue == null) + { + return default; + } + + if (noValue is T noValueAsT) + { + return noValueAsT; + } + var noValueConverted = noValue.TryConvertTo(); - if (noValueConverted) return noValueConverted.Result; + if (noValueConverted) + { + return noValueConverted.Result; + } // cannot cast noValue nor convert it, nothing we can return but 'default' return default; diff --git a/src/Umbraco.Web/Routing/ContentFinderByConfigured404.cs b/src/Umbraco.Web/Routing/ContentFinderByConfigured404.cs index eae198bb59..5255da6521 100644 --- a/src/Umbraco.Web/Routing/ContentFinderByConfigured404.cs +++ b/src/Umbraco.Web/Routing/ContentFinderByConfigured404.cs @@ -69,7 +69,7 @@ namespace Umbraco.Web.Routing if (error404.HasValue) { - _logger.Debug("Got id={ErrorNodeId}.", error404.Value); + _logger.Debug("Got id={ErrorNodeId}.", error404.Value); content = frequest.UmbracoContext.Content.GetById(error404.Value); diff --git a/src/Umbraco.Web/Routing/ContentFinderByIdPath.cs b/src/Umbraco.Web/Routing/ContentFinderByIdPath.cs index b339198928..8cc52054f3 100644 --- a/src/Umbraco.Web/Routing/ContentFinderByIdPath.cs +++ b/src/Umbraco.Web/Routing/ContentFinderByIdPath.cs @@ -49,7 +49,7 @@ namespace Umbraco.Web.Routing if (nodeId > 0) { - _logger.Debug("Id={NodeId}", nodeId); + _logger.Debug("Id={NodeId}", nodeId); node = frequest.UmbracoContext.Content.GetById(nodeId); if (node != null) @@ -62,7 +62,7 @@ namespace Umbraco.Web.Routing } frequest.PublishedContent = node; - _logger.Debug("Found node with id={PublishedContentId}", frequest.PublishedContent.Id); + _logger.Debug("Found node with id={PublishedContentId}", frequest.PublishedContent.Id); } else { diff --git a/src/Umbraco.Web/Routing/ContentFinderByRedirectUrl.cs b/src/Umbraco.Web/Routing/ContentFinderByRedirectUrl.cs index 6c1195d222..265d6ff3a4 100644 --- a/src/Umbraco.Web/Routing/ContentFinderByRedirectUrl.cs +++ b/src/Umbraco.Web/Routing/ContentFinderByRedirectUrl.cs @@ -42,7 +42,7 @@ namespace Umbraco.Web.Routing if (redirectUrl == null) { - _logger.Debug("No match for route: {Route}", route); + _logger.Debug("No match for route: {Route}", route); return false; } @@ -50,14 +50,14 @@ namespace Umbraco.Web.Routing var url = content == null ? "#" : content.Url(redirectUrl.Culture); if (url.StartsWith("#")) { - _logger.Debug("Route {Route} matches content {ContentId} which has no URL.", route, redirectUrl.ContentId); + _logger.Debug("Route {Route} matches content {ContentId} which has no URL.", route, redirectUrl.ContentId); return false; } // Appending any querystring from the incoming request to the redirect URL url = string.IsNullOrEmpty(frequest.Uri.Query) ? url : url + frequest.Uri.Query; - _logger.Debug("Route {Route} matches content {ContentId} with URL '{Url}', redirecting.", route, content.Id, url); + _logger.Debug("Route {Route} matches content {ContentId} with url '{Url}', redirecting.", route, content.Id, url); frequest.SetRedirectPermanent(url); diff --git a/src/Umbraco.Web/Routing/ContentFinderByUrl.cs b/src/Umbraco.Web/Routing/ContentFinderByUrl.cs index 074193417a..571dfddd2e 100644 --- a/src/Umbraco.Web/Routing/ContentFinderByUrl.cs +++ b/src/Umbraco.Web/Routing/ContentFinderByUrl.cs @@ -46,13 +46,13 @@ namespace Umbraco.Web.Routing { if (docreq == null) throw new System.ArgumentNullException(nameof(docreq)); - Logger.Debug("Test route {Route}", route); + Logger.Debug("Test route {Route}", route); var node = docreq.UmbracoContext.Content.GetByRoute(docreq.UmbracoContext.InPreviewMode, route, culture: docreq.Culture?.Name); if (node != null) { docreq.PublishedContent = node; - Logger.Debug("Got content, id={NodeId}", node.Id); + Logger.Debug("Got content, id={NodeId}", node.Id); } else { diff --git a/src/Umbraco.Web/Routing/ContentFinderByUrlAlias.cs b/src/Umbraco.Web/Routing/ContentFinderByUrlAlias.cs index 0ee5f3d42d..9f470b83ac 100644 --- a/src/Umbraco.Web/Routing/ContentFinderByUrlAlias.cs +++ b/src/Umbraco.Web/Routing/ContentFinderByUrlAlias.cs @@ -44,7 +44,7 @@ namespace Umbraco.Web.Routing if (node != null) { frequest.PublishedContent = node; - Logger.Debug("Path '{UriAbsolutePath}' is an alias for id={PublishedContentId}", frequest.Uri.AbsolutePath, frequest.PublishedContent.Id); + Logger.Debug("Path '{UriAbsolutePath}' is an alias for id={PublishedContentId}", frequest.Uri.AbsolutePath, frequest.PublishedContent.Id); } } diff --git a/src/Umbraco.Web/Routing/ContentFinderByUrlAndTemplate.cs b/src/Umbraco.Web/Routing/ContentFinderByUrlAndTemplate.cs index a02ebbb2a8..bb5cea063a 100644 --- a/src/Umbraco.Web/Routing/ContentFinderByUrlAndTemplate.cs +++ b/src/Umbraco.Web/Routing/ContentFinderByUrlAndTemplate.cs @@ -56,11 +56,11 @@ namespace Umbraco.Web.Routing if (template == null) { - Logger.Debug("Not a valid template: '{TemplateAlias}'", templateAlias); + Logger.Debug("Not a valid template: '{TemplateAlias}'", templateAlias); return false; } - Logger.Debug("Valid template: '{TemplateAlias}'", templateAlias); + Logger.Debug("Valid template: '{TemplateAlias}'", templateAlias); // look for node corresponding to the rest of the route var route = frequest.HasDomain ? (frequest.Domain.ContentId + path) : path; @@ -68,14 +68,14 @@ namespace Umbraco.Web.Routing if (node == null) { - Logger.Debug("Not a valid route to node: '{Route}'", route); + Logger.Debug("Not a valid route to node: '{Route}'", route); return false; } // IsAllowedTemplate deals both with DisableAlternativeTemplates and ValidateAlternativeTemplates settings if (!node.IsAllowedTemplate(template.Id)) { - Logger.Warn("Alternative template '{TemplateAlias}' is not allowed on node {NodeId}.", template.Alias, node.Id); + Logger.Warn("Alternative template '{TemplateAlias}' is not allowed on node {NodeId}.", template.Alias, node.Id); frequest.PublishedContent = null; // clear return false; } diff --git a/src/Umbraco.Web/Routing/DefaultUrlProvider.cs b/src/Umbraco.Web/Routing/DefaultUrlProvider.cs index f26f05c36c..fdd76a9160 100644 --- a/src/Umbraco.Web/Routing/DefaultUrlProvider.cs +++ b/src/Umbraco.Web/Routing/DefaultUrlProvider.cs @@ -44,7 +44,7 @@ namespace Umbraco.Web.Routing { if (string.IsNullOrWhiteSpace(route)) { - _logger.Debug("Couldn't find any page with nodeId={NodeId}. This is most likely caused by the page not being published.", id); + _logger.Debug("Couldn't find any page with nodeId={NodeId}. This is most likely caused by the page not being published.", id); return null; } diff --git a/src/Umbraco.Web/Routing/NotFoundHandlerHelper.cs b/src/Umbraco.Web/Routing/NotFoundHandlerHelper.cs index 9fefeeb843..f2494696cf 100644 --- a/src/Umbraco.Web/Routing/NotFoundHandlerHelper.cs +++ b/src/Umbraco.Web/Routing/NotFoundHandlerHelper.cs @@ -107,7 +107,7 @@ namespace Umbraco.Web.Routing } catch (Exception ex) { - Current.Logger.Error(ex, "Could not parse xpath expression: {ContentXPath}", errorPage.ContentXPath); + Current.Logger.Error(ex, "Could not parse xpath expression: {ContentXPath}", errorPage.ContentXPath); return null; } } diff --git a/src/Umbraco.Web/Routing/PublishedRouter.cs b/src/Umbraco.Web/Routing/PublishedRouter.cs index fee9a62158..ebf935dcf8 100644 --- a/src/Umbraco.Web/Routing/PublishedRouter.cs +++ b/src/Umbraco.Web/Routing/PublishedRouter.cs @@ -253,7 +253,7 @@ namespace Umbraco.Web.Routing // note - we are not handling schemes nor ports here. - _logger.Debug("{TracePrefix}Uri={RequestUri}", tracePrefix, request.Uri); + _logger.Debug("{TracePrefix}Uri={RequestUri}", tracePrefix, request.Uri); var domainsCache = request.UmbracoContext.PublishedSnapshot.Domains; var domains = domainsCache.GetAll(includeWildcards: false).ToList(); @@ -305,12 +305,12 @@ namespace Umbraco.Web.Routing else { // not matching any existing domain - _logger.Debug("{TracePrefix}Matches no domain", tracePrefix); + _logger.Debug("{TracePrefix}Matches no domain", tracePrefix); request.Culture = defaultCulture == null ? CultureInfo.CurrentUICulture : new CultureInfo(defaultCulture); } - _logger.Debug("{TracePrefix}Culture={CultureName}", tracePrefix, request.Culture.Name); + _logger.Debug("{TracePrefix}Culture={CultureName}", tracePrefix, request.Culture.Name); return request.Domain != null; } @@ -326,7 +326,7 @@ namespace Umbraco.Web.Routing return; var nodePath = request.PublishedContent.Path; - _logger.Debug("{TracePrefix}Path={NodePath}", tracePrefix, nodePath); + _logger.Debug("{TracePrefix}Path={NodePath}", tracePrefix, nodePath); var rootNodeId = request.HasDomain ? request.Domain.ContentId : (int?)null; var domain = DomainUtilities.FindWildcardDomainInPath(request.UmbracoContext.PublishedSnapshot.Domains.GetAll(true), nodePath, rootNodeId); @@ -334,11 +334,11 @@ namespace Umbraco.Web.Routing if (domain != null) { request.Culture = domain.Culture; - _logger.Debug("{TracePrefix}Got domain on node {DomainContentId}, set culture to {CultureName}", tracePrefix, domain.ContentId, request.Culture.Name); + _logger.Debug("{TracePrefix}Got domain on node {DomainContentId}, set culture to {CultureName}", tracePrefix, domain.ContentId, request.Culture.Name); } else { - _logger.Debug("{TracePrefix}No match.", tracePrefix); + _logger.Debug("{TracePrefix}No match.", tracePrefix); } } @@ -380,7 +380,7 @@ namespace Umbraco.Web.Routing /// A value indicating whether a document and template were found. private void FindPublishedContentAndTemplate(PublishedRequest request) { - _logger.Debug("FindPublishedContentAndTemplate: Path={UriAbsolutePath}", request.Uri.AbsolutePath); + _logger.Debug("FindPublishedContentAndTemplate: Path={UriAbsolutePath}", request.Uri.AbsolutePath); // run the document finders FindPublishedContent(request); @@ -417,15 +417,25 @@ namespace Umbraco.Web.Routing // some finders may implement caching using (_profilingLogger.DebugDuration( - $"{tracePrefix}Begin finders", - $"{tracePrefix}End finders, {(request.HasPublishedContent ? "a document was found" : "no document was found")}")) + $"{tracePrefix}Executing finders...", + $"{tracePrefix}Completed executing finders")) { //iterate but return on first one that finds it var found = _contentFinders.Any(finder => { - _logger.Debug("Finder {ContentFinderType}", finder.GetType().FullName); + _logger.Debug("Finder {ContentFinderType}", finder.GetType().FullName); return finder.TryFindContent(request); }); + + _logger.Debug( + "Found? {Found} Content: {PublishedContentId}, Template: {TemplateAlias}, Domain: {Domain}, Culture: {Culture}, Is404: {Is404}, StatusCode: {StatusCode}", + found, + request.HasPublishedContent ? request.PublishedContent.Id : "NULL", + request.HasTemplate ? request.TemplateAlias : "NULL", + request.HasDomain ? request.Domain.ToString() : "NULL", + request.Culture?.Name ?? "NULL", + request.Is404, + request.ResponseStatusCode); } // indicate that the published content (if any) we have at the moment is the @@ -447,7 +457,7 @@ namespace Umbraco.Web.Routing const int maxLoop = 8; do { - _logger.Debug("HandlePublishedContent: Loop {LoopCounter}", i); + _logger.Debug("HandlePublishedContent: Loop {LoopCounter}", i); // handle not found if (request.HasPublishedContent == false) @@ -506,55 +516,47 @@ namespace Umbraco.Web.Routing // don't try to find a redirect if the property doesn't exist if (request.PublishedContent.HasProperty(Constants.Conventions.Content.InternalRedirectId) == false) + { return false; + } + + var internalRedirectId = request.PublishedContent.Value(Constants.Conventions.Content.InternalRedirectId)?.ToString(); + + if (internalRedirectId == null) + { + // no value stored, just return, no need to log + return false; + } + + if (int.TryParse(internalRedirectId, out var internalRedirectIdAsInt) && internalRedirectIdAsInt == request.PublishedContent.Id) + { + // redirect to self + _logger.Debug("FollowInternalRedirects: Redirecting to self, ignore"); + return false; + } - var redirect = false; - var valid = false; IPublishedContent internalRedirectNode = null; - var internalRedirectId = request.PublishedContent.Value(Constants.Conventions.Content.InternalRedirectId, defaultValue: -1); - - if (internalRedirectId > 0) + if (internalRedirectIdAsInt > 0) { // try and get the redirect node from a legacy integer ID - valid = true; - internalRedirectNode = request.UmbracoContext.Content.GetById(internalRedirectId); + internalRedirectNode = request.UmbracoContext.Content.GetById(internalRedirectIdAsInt); } - else + else if (GuidUdi.TryParse(internalRedirectId, out var internalRedirectIdAsUdi)) { - var udiInternalRedirectId = request.PublishedContent.Value(Constants.Conventions.Content.InternalRedirectId); - if (udiInternalRedirectId != null) - { - // try and get the redirect node from a UDI Guid - valid = true; - internalRedirectNode = request.UmbracoContext.Content.GetById(udiInternalRedirectId.Guid); - } - } - - if (valid == false) - { - // bad redirect - log and display the current page (legacy behavior) - _logger.Debug("FollowInternalRedirects: Failed to redirect to id={InternalRedirectId}: value is not an int nor a GuidUdi.", - request.PublishedContent.GetProperty(Constants.Conventions.Content.InternalRedirectId).GetSourceValue()); + // try and get the redirect node from a UDI Guid + internalRedirectNode = request.UmbracoContext.Content.GetById(internalRedirectIdAsUdi.Guid); } if (internalRedirectNode == null) { - _logger.Debug("FollowInternalRedirects: Failed to redirect to id={InternalRedirectId}: no such published document.", + _logger.Debug("FollowInternalRedirects: Failed to redirect to id={InternalRedirectId}: no such published document.", request.PublishedContent.GetProperty(Constants.Conventions.Content.InternalRedirectId).GetSourceValue()); - } - else if (internalRedirectId == request.PublishedContent.Id) - { - // redirect to self - _logger.Debug("FollowInternalRedirects: Redirecting to self, ignore"); - } - else - { - request.SetInternalRedirectPublishedContent(internalRedirectNode); // don't use .PublishedContent here - redirect = true; - _logger.Debug("FollowInternalRedirects: Redirecting to id={InternalRedirectId}", internalRedirectId); + return false; } - return redirect; + request.SetInternalRedirectPublishedContent(internalRedirectNode); // don't use .PublishedContent here + _logger.Debug("FollowInternalRedirects: Redirecting to id={InternalRedirectId}", internalRedirectIdAsInt); + return true; } /// @@ -691,7 +693,7 @@ namespace Umbraco.Web.Routing if (request.HasTemplate) _logger.Debug("FindTemplate: Has a template already, but also an alternative template."); - _logger.Debug("FindTemplate: Look for alternative template alias={AltTemplate}", altTemplate); + _logger.Debug("FindTemplate: Look for alternative template alias={AltTemplate}", altTemplate); // IsAllowedTemplate deals both with DisableAlternativeTemplates and ValidateAlternativeTemplates settings if (request.PublishedContent.IsAllowedTemplate(altTemplate)) @@ -702,16 +704,16 @@ namespace Umbraco.Web.Routing if (template != null) { request.TemplateModel = template; - _logger.Debug("FindTemplate: Got alternative template id={TemplateId} alias={TemplateAlias}", template.Id, template.Alias); + _logger.Debug("FindTemplate: Got alternative template id={TemplateId} alias={TemplateAlias}", template.Id, template.Alias); } else { - _logger.Debug("FindTemplate: The alternative template with alias={AltTemplate} does not exist, ignoring.", altTemplate); + _logger.Debug("FindTemplate: The alternative template with alias={AltTemplate} does not exist, ignoring.", altTemplate); } } else { - _logger.Warn("FindTemplate: Alternative template {TemplateAlias} is not allowed on node {NodeId}, ignoring.", altTemplate, request.PublishedContent.Id); + _logger.Warn("FindTemplate: Alternative template {TemplateAlias} is not allowed on node {NodeId}, ignoring.", altTemplate, request.PublishedContent.Id); // no allowed, back to default var templateId = request.PublishedContent.TemplateId; @@ -734,7 +736,7 @@ namespace Umbraco.Web.Routing } else { - _logger.Debug("FindTemplate: Running with template id={TemplateId} alias={TemplateAlias}", request.TemplateModel.Id, request.TemplateModel.Alias); + _logger.Debug("FindTemplate: Running with template id={TemplateId} alias={TemplateAlias}", request.TemplateModel.Id, request.TemplateModel.Alias); } } @@ -746,7 +748,7 @@ namespace Umbraco.Web.Routing return null; } - _logger.Debug("GetTemplateModel: Get template id={TemplateId}", templateId); + _logger.Debug("GetTemplateModel: Get template id={TemplateId}", templateId); if (templateId == null) throw new InvalidOperationException("The template is not set, the page cannot render."); @@ -754,7 +756,7 @@ namespace Umbraco.Web.Routing var template = _services.FileService.GetTemplate(templateId.Value); if (template == null) throw new InvalidOperationException("The template with Id " + templateId + " does not exist, the page cannot render."); - _logger.Debug("GetTemplateModel: Got template id={TemplateId} alias={TemplateAlias}", template.Id, template.Alias); + _logger.Debug("GetTemplateModel: Got template id={TemplateId} alias={TemplateAlias}", template.Id, template.Alias); return template; } diff --git a/src/Umbraco.Web/Routing/UrlProviderExtensions.cs b/src/Umbraco.Web/Routing/UrlProviderExtensions.cs index 6a2d35206d..0d40137e0a 100644 --- a/src/Umbraco.Web/Routing/UrlProviderExtensions.cs +++ b/src/Umbraco.Web/Routing/UrlProviderExtensions.cs @@ -78,7 +78,7 @@ namespace Umbraco.Web.Routing if (urls.Add(otherUrl)) //avoid duplicates yield return otherUrl; } - + /// /// Tries to return a for each culture for the content while detecting collisions/errors /// @@ -131,10 +131,14 @@ namespace Umbraco.Web.Routing // got a URL, deal with collisions, add URL default: - if (DetectCollision(content, url, culture, umbracoContext, publishedRouter, textService, out var urlInfo)) // detect collisions, etc + if (DetectCollision(logger, content, url, culture, umbracoContext, publishedRouter, textService, out var urlInfo)) // detect collisions, etc + { yield return urlInfo; + } else + { yield return UrlInfo.Url(url, culture); + } break; } } @@ -152,16 +156,23 @@ namespace Umbraco.Web.Routing while (parent != null && parent.Published && (!parent.ContentType.VariesByCulture() || parent.IsCulturePublished(culture))); if (parent == null) // oops, internal error + { return UrlInfo.Message(textService.Localize("content", "parentNotPublishedAnomaly"), culture); + } else if (!parent.Published) // totally not published - return UrlInfo.Message(textService.Localize("content", "parentNotPublished", new[] {parent.Name}), culture); + { + return UrlInfo.Message(textService.Localize("content", "parentNotPublished", new[] { parent.Name }), culture); + } - else // culture not published - return UrlInfo.Message(textService.Localize("content", "parentCultureNotPublished", new[] {parent.Name}), culture); + else + { + // culture not published + return UrlInfo.Message(textService.Localize("content", "parentCultureNotPublished", new[] { parent.Name }), culture); + } } - private static bool DetectCollision(IContent content, string url, string culture, UmbracoContext umbracoContext, IPublishedRouter publishedRouter, ILocalizedTextService textService, out UrlInfo urlInfo) + private static bool DetectCollision(ILogger logger, IContent content, string url, string culture, UmbracoContext umbracoContext, IPublishedRouter publishedRouter, ILocalizedTextService textService, out UrlInfo urlInfo) { // test for collisions on the 'main' URL var uri = new Uri(url.TrimEnd(Constants.CharArrays.ForwardSlash), UriKind.RelativeOrAbsolute); @@ -174,6 +185,16 @@ namespace Umbraco.Web.Routing if (pcr.HasPublishedContent == false) { + var logMsg = nameof(DetectCollision) + " did not resolve a content item for original url: {Url}, translated to {TranslatedUrl} and culture: {Culture}"; + if (pcr.IgnorePublishedContentCollisions) + { + logger.Debug(typeof(UrlProviderExtensions), logMsg, url, uri, culture); + } + else + { + logger.Warn(typeof(UrlProviderExtensions), logMsg, url, uri, culture); + } + urlInfo = UrlInfo.Message(textService.Localize("content", "routeErrorCannotRoute"), culture); return true; } diff --git a/src/Umbraco.Web/Runtime/WebInitialComposer.cs b/src/Umbraco.Web/Runtime/WebInitialComposer.cs index ac7e34d5cb..5d97bfe4a2 100644 --- a/src/Umbraco.Web/Runtime/WebInitialComposer.cs +++ b/src/Umbraco.Web/Runtime/WebInitialComposer.cs @@ -139,7 +139,6 @@ namespace Umbraco.Web.Runtime composition.RegisterUnique(); composition.RegisterUnique(); composition.RegisterUnique(); - composition.RegisterUnique(factory => ExamineManager.Instance); // configure the container for web diff --git a/src/Umbraco.Web/Scheduling/BackgroundTaskRunner.cs b/src/Umbraco.Web/Scheduling/BackgroundTaskRunner.cs index 81bb45e270..e8cb592536 100644 --- a/src/Umbraco.Web/Scheduling/BackgroundTaskRunner.cs +++ b/src/Umbraco.Web/Scheduling/BackgroundTaskRunner.cs @@ -239,7 +239,7 @@ namespace Umbraco.Web.Scheduling throw new InvalidOperationException("The task runner has completed."); // add task - _logger.Debug("{LogPrefix} Task Added {TaskType}", _logPrefix , task.GetType().FullName); + _logger.Debug("{LogPrefix} Task Added {TaskType}", _logPrefix , task.GetType().FullName); _tasks.Post(task); // start @@ -259,12 +259,12 @@ namespace Umbraco.Web.Scheduling { if (_completed) { - _logger.Debug("{LogPrefix} Task cannot be added {TaskType}, the task runner has already shutdown", _logPrefix, task.GetType().FullName); + _logger.Debug("{LogPrefix} Task cannot be added {TaskType}, the task runner has already shutdown", _logPrefix, task.GetType().FullName); return false; } // add task - _logger.Debug("{LogPrefix} Task added {TaskType}", _logPrefix, task.GetType().FullName); + _logger.Debug("{LogPrefix} Task added {TaskType}", _logPrefix, task.GetType().FullName); _tasks.Post(task); // start @@ -319,9 +319,12 @@ namespace Umbraco.Web.Scheduling // create a new token source since this is a new process _shutdownTokenSource = new CancellationTokenSource(); _shutdownToken = _shutdownTokenSource.Token; - _runningTask = Task.Run(async () => await Pump().ConfigureAwait(false), _shutdownToken); + using (ExecutionContext.SuppressFlow()) + { + _runningTask = Task.Run(async () => await Pump().ConfigureAwait(false), _shutdownToken); + } - _logger.Debug("{LogPrefix} Starting", _logPrefix); + _logger.Debug("{LogPrefix} Starting", _logPrefix); } /// @@ -342,7 +345,7 @@ namespace Umbraco.Web.Scheduling var hasTasks = TaskCount > 0; if (!force && hasTasks) - _logger.Info("{LogPrefix} Waiting for tasks to complete", _logPrefix); + _logger.Info("{LogPrefix} Waiting for tasks to complete", _logPrefix); // complete the queue // will stop waiting on the queue or on a latch @@ -408,7 +411,7 @@ namespace Umbraco.Web.Scheduling } catch (Exception ex) { - _logger.Error(ex, "{LogPrefix} Task runner exception", _logPrefix); + _logger.Error(ex, "{LogPrefix} Task runner exception", _logPrefix); } } } @@ -440,7 +443,7 @@ namespace Umbraco.Web.Scheduling if (_shutdownToken.IsCancellationRequested == false && TaskCount > 0) continue; // if we really have nothing to do, stop - _logger.Debug("{LogPrefix} Stopping", _logPrefix); + _logger.Debug("{LogPrefix} Stopping", _logPrefix); if (_options.PreserveRunningTask == false) _runningTask = null; @@ -544,10 +547,14 @@ namespace Umbraco.Web.Scheduling try { if (bgTask.IsAsync) + { // configure await = false since we don't care about the context, we're on a background thread. await bgTask.RunAsync(token).ConfigureAwait(false); + } else + { bgTask.Run(); + } } finally // ensure we disposed - unless latched again ie wants to re-run { @@ -567,7 +574,7 @@ namespace Umbraco.Web.Scheduling catch (Exception ex) { - _logger.Error(ex, "{LogPrefix} Task has failed", _logPrefix); + _logger.Error(ex, "{LogPrefix} Task has failed", _logPrefix); } } @@ -601,7 +608,7 @@ namespace Umbraco.Web.Scheduling private void OnEvent(TypedEventHandler, TArgs> handler, string name, TArgs e) { - _logger.Debug("{LogPrefix} OnEvent {EventName}", _logPrefix, name); + _logger.Debug("{LogPrefix} OnEvent {EventName}", _logPrefix, name); if (handler == null) return; @@ -611,7 +618,7 @@ namespace Umbraco.Web.Scheduling } catch (Exception ex) { - _logger.Error(ex, "{LogPrefix} {Name} exception occurred", _logPrefix, name); + _logger.Error(ex, "{LogPrefix} {Name} exception occurred", _logPrefix, name); } } @@ -698,7 +705,7 @@ namespace Umbraco.Web.Scheduling if (_terminating == false) { _terminating = true; - _logger.Info("{LogPrefix} Terminating {Immediate}", _logPrefix, immediate ? immediate.ToString() : string.Empty); + _logger.Info("{LogPrefix} Terminating {Immediate}", _logPrefix, immediate ? immediate.ToString() : string.Empty); onTerminating = true; } } @@ -710,14 +717,20 @@ namespace Umbraco.Web.Scheduling // with a single aspnet thread during shutdown and we don't want to delay other calls to IRegisteredObject.Stop. if (!immediate) { - return Task.Run(StopInitial, CancellationToken.None); + using (ExecutionContext.SuppressFlow()) + { + return Task.Run(StopInitial, CancellationToken.None); + } } else { lock (_locker) { if (_terminated) return Task.CompletedTask; - return Task.Run(StopImmediate, CancellationToken.None); + using (ExecutionContext.SuppressFlow()) + { + return Task.Run(StopImmediate, CancellationToken.None); + } } } } @@ -791,7 +804,7 @@ namespace Umbraco.Web.Scheduling /// private void StopImmediate() { - _logger.Info("{LogPrefix} Canceling tasks", _logPrefix); + _logger.Info("{LogPrefix} Canceling tasks", _logPrefix); try { Shutdown(true, true); // cancel all tasks, wait for the current one to end @@ -825,7 +838,7 @@ namespace Umbraco.Web.Scheduling terminatedSource = _terminatedSource; } - _logger.Info("{LogPrefix} Tasks {TaskStatus}, terminated", + _logger.Info("{LogPrefix} Tasks {TaskStatus}, terminated", _logPrefix, immediate ? "cancelled" : "completed"); diff --git a/src/Umbraco.Web/Scheduling/KeepAlive.cs b/src/Umbraco.Web/Scheduling/KeepAlive.cs index 2dcf9a24a7..5ee10e4c2a 100644 --- a/src/Umbraco.Web/Scheduling/KeepAlive.cs +++ b/src/Umbraco.Web/Scheduling/KeepAlive.cs @@ -70,7 +70,7 @@ namespace Umbraco.Web.Scheduling } catch (Exception ex) { - _logger.Error(ex, "Keep alive failed (at '{keepAlivePingUrl}').", keepAlivePingUrl); + _logger.Error(ex, "Keep alive failed (at '{keepAlivePingUrl}').", keepAlivePingUrl); } } diff --git a/src/Umbraco.Web/Scheduling/ScheduledPublishing.cs b/src/Umbraco.Web/Scheduling/ScheduledPublishing.cs index 97afe25e22..b7ec7ffc0e 100644 --- a/src/Umbraco.Web/Scheduling/ScheduledPublishing.cs +++ b/src/Umbraco.Web/Scheduling/ScheduledPublishing.cs @@ -67,7 +67,7 @@ namespace Umbraco.Web.Scheduling // run var result = _contentService.PerformScheduledPublish(DateTime.Now); foreach (var grouped in result.GroupBy(x => x.Result)) - _logger.Info("Scheduled publishing result: '{StatusCount}' items with status {Status}", grouped.Count(), grouped.Key); + _logger.Info("Scheduled publishing result: '{StatusCount}' items with status {Status}", grouped.Count(), grouped.Key); } finally { diff --git a/src/Umbraco.Web/Scheduling/TempFileCleanup.cs b/src/Umbraco.Web/Scheduling/TempFileCleanup.cs index d2a254dfd6..05f96f0e75 100644 --- a/src/Umbraco.Web/Scheduling/TempFileCleanup.cs +++ b/src/Umbraco.Web/Scheduling/TempFileCleanup.cs @@ -50,7 +50,7 @@ namespace Umbraco.Web.Scheduling dir.Refresh(); //in case it's changed during runtime if (!dir.Exists) { - _logger.Debug("The cleanup folder doesn't exist {Folder}", dir.FullName); + _logger.Debug("The cleanup folder doesn't exist {Folder}", dir.FullName); return; } @@ -66,7 +66,7 @@ namespace Umbraco.Web.Scheduling } catch (Exception ex) { - _logger.Error(ex, "Could not delete temp file {FileName}", file.FullName); + _logger.Error(ex, "Could not delete temp file {FileName}", file.FullName); } } } diff --git a/src/Umbraco.Web/Search/ExamineComponent.cs b/src/Umbraco.Web/Search/ExamineComponent.cs index c9d7b7cf56..11778a64bb 100644 --- a/src/Umbraco.Web/Search/ExamineComponent.cs +++ b/src/Umbraco.Web/Search/ExamineComponent.cs @@ -107,7 +107,7 @@ namespace Umbraco.Web.Search var registeredIndexers = _examineManager.Indexes.OfType().Count(x => x.EnableDefaultEventHandler); - _logger.Info("Adding examine event handlers for {RegisteredIndexers} index providers.", registeredIndexers); + _logger.Info("Adding examine event handlers for {RegisteredIndexers} index providers.", registeredIndexers); // don't bind event handlers if we're not suppose to listen if (registeredIndexers == 0) @@ -271,10 +271,20 @@ namespace Umbraco.Web.Search break; case MessageType.RefreshByPayload: var payload = (MemberCacheRefresher.JsonPayload[])args.MessageObject; - var members = payload.Select(x => _services.MemberService.GetById(x.Id)); - foreach(var m in members) + foreach(var p in payload) { - ReIndexForMember(m); + if (p.Removed) + { + DeleteIndexForEntity(p.Id, false); + } + else + { + var m = _services.MemberService.GetById(p.Id); + if (m != null) + { + ReIndexForMember(m); + } + } } break; case MessageType.RefreshAll: @@ -629,22 +639,27 @@ namespace Umbraco.Web.Search // perform the ValueSet lookup on a background thread examineComponent._indexItemTaskRunner.Add(new SimpleTask(() => { - // for content we have a different builder for published vs unpublished - // we don't want to build more value sets than is needed so we'll lazily build 2 one for published one for non-published - var builders = new Dictionary>> + // Background thread, wrap the whole thing in an explicit scope since we know + // DB services are used within this logic. + using (examineComponent._scopeProvider.CreateScope(autoComplete: true)) { - [true] = new Lazy>(() => examineComponent._publishedContentValueSetBuilder.GetValueSets(content).ToList()), - [false] = new Lazy>(() => examineComponent._contentValueSetBuilder.GetValueSets(content).ToList()) - }; + // for content we have a different builder for published vs unpublished + // we don't want to build more value sets than is needed so we'll lazily build 2 one for published one for non-published + var builders = new Dictionary>> + { + [true] = new Lazy>(() => examineComponent._publishedContentValueSetBuilder.GetValueSets(content).ToList()), + [false] = new Lazy>(() => examineComponent._contentValueSetBuilder.GetValueSets(content).ToList()) + }; - foreach (var index in examineComponent._examineManager.Indexes.OfType() - //filter the indexers - .Where(x => isPublished || !x.PublishedValuesOnly) - .Where(x => x.EnableDefaultEventHandler)) - { - var valueSet = builders[index.PublishedValuesOnly].Value; - index.IndexItems(valueSet); - } + foreach (var index in examineComponent._examineManager.Indexes.OfType() + //filter the indexers + .Where(x => isPublished || !x.PublishedValuesOnly) + .Where(x => x.EnableDefaultEventHandler)) + { + var valueSet = builders[index.PublishedValuesOnly].Value; + index.IndexItems(valueSet); + } + } })); } } @@ -675,14 +690,19 @@ namespace Umbraco.Web.Search // perform the ValueSet lookup on a background thread examineComponent._indexItemTaskRunner.Add(new SimpleTask(() => { - var valueSet = examineComponent._mediaValueSetBuilder.GetValueSets(media).ToList(); - - foreach (var index in examineComponent._examineManager.Indexes.OfType() - //filter the indexers - .Where(x => isPublished || !x.PublishedValuesOnly) - .Where(x => x.EnableDefaultEventHandler)) + // Background thread, wrap the whole thing in an explicit scope since we know + // DB services are used within this logic. + using (examineComponent._scopeProvider.CreateScope(autoComplete: true)) { - index.IndexItems(valueSet); + var valueSet = examineComponent._mediaValueSetBuilder.GetValueSets(media).ToList(); + + foreach (var index in examineComponent._examineManager.Indexes.OfType() + //filter the indexers + .Where(x => isPublished || !x.PublishedValuesOnly) + .Where(x => x.EnableDefaultEventHandler)) + { + index.IndexItems(valueSet); + } } })); } @@ -712,12 +732,17 @@ namespace Umbraco.Web.Search // perform the ValueSet lookup on a background thread examineComponent._indexItemTaskRunner.Add(new SimpleTask(() => { - var valueSet = examineComponent._memberValueSetBuilder.GetValueSets(member).ToList(); - foreach (var index in examineComponent._examineManager.Indexes.OfType() - //filter the indexers - .Where(x => x.EnableDefaultEventHandler)) + // Background thread, wrap the whole thing in an explicit scope since we know + // DB services are used within this logic. + using (examineComponent._scopeProvider.CreateScope(autoComplete: true)) { - index.IndexItems(valueSet); + var valueSet = examineComponent._memberValueSetBuilder.GetValueSets(member).ToList(); + foreach (var index in examineComponent._examineManager.Indexes.OfType() + //filter the indexers + .Where(x => x.EnableDefaultEventHandler)) + { + index.IndexItems(valueSet); + } } })); } diff --git a/src/Umbraco.Web/Search/UmbracoTreeSearcher.cs b/src/Umbraco.Web/Search/UmbracoTreeSearcher.cs index 410b654e32..134d482b40 100644 --- a/src/Umbraco.Web/Search/UmbracoTreeSearcher.cs +++ b/src/Umbraco.Web/Search/UmbracoTreeSearcher.cs @@ -5,6 +5,8 @@ using System.Text; using System.Text.RegularExpressions; using Examine; using Umbraco.Core; +using Umbraco.Core.Cache; +using Umbraco.Core.Composing; using Umbraco.Core.Mapping; using Umbraco.Core.Models; using Umbraco.Core.Models.Entities; @@ -30,14 +32,29 @@ namespace Umbraco.Web.Search private readonly UmbracoMapper _mapper; private readonly ISqlContext _sqlContext; private readonly IUmbracoTreeSearcherFields _umbracoTreeSearcherFields; + private readonly AppCaches _appCaches; - - public UmbracoTreeSearcher(IExamineManager examineManager, + [Obsolete("Use constructor specifying all dependencies instead")] + public UmbracoTreeSearcher( + IExamineManager examineManager, UmbracoContext umbracoContext, ILocalizationService languageService, IEntityService entityService, UmbracoMapper mapper, - ISqlContext sqlContext,IUmbracoTreeSearcherFields umbracoTreeSearcherFields) + ISqlContext sqlContext, + IUmbracoTreeSearcherFields umbracoTreeSearcherFields) + : this(examineManager, umbracoContext, languageService, entityService, mapper, sqlContext, umbracoTreeSearcherFields, Current.AppCaches) + { } + + public UmbracoTreeSearcher( + IExamineManager examineManager, + UmbracoContext umbracoContext, + ILocalizationService languageService, + IEntityService entityService, + UmbracoMapper mapper, + ISqlContext sqlContext, + IUmbracoTreeSearcherFields umbracoTreeSearcherFields, + AppCaches appCaches) { _examineManager = examineManager ?? throw new ArgumentNullException(nameof(examineManager)); _umbracoContext = umbracoContext; @@ -46,6 +63,7 @@ namespace Umbraco.Web.Search _mapper = mapper; _sqlContext = sqlContext; _umbracoTreeSearcherFields = umbracoTreeSearcherFields; + _appCaches = appCaches; } /// @@ -112,13 +130,13 @@ namespace Umbraco.Web.Search case UmbracoEntityTypes.Media: type = "media"; fields.AddRange(_umbracoTreeSearcherFields.GetBackOfficeMediaFields()); - var allMediaStartNodes = _umbracoContext.Security.CurrentUser.CalculateMediaStartNodeIds(_entityService); + var allMediaStartNodes = _umbracoContext.Security.CurrentUser.CalculateMediaStartNodeIds(_entityService, _appCaches); AppendPath(sb, UmbracoObjectTypes.Media, allMediaStartNodes, searchFrom, ignoreUserStartNodes, _entityService); break; case UmbracoEntityTypes.Document: type = "content"; fields.AddRange(_umbracoTreeSearcherFields.GetBackOfficeDocumentFields()); - var allContentStartNodes = _umbracoContext.Security.CurrentUser.CalculateContentStartNodeIds(_entityService); + var allContentStartNodes = _umbracoContext.Security.CurrentUser.CalculateContentStartNodeIds(_entityService, _appCaches); AppendPath(sb, UmbracoObjectTypes.Document, allContentStartNodes, searchFrom, ignoreUserStartNodes, _entityService); break; default: @@ -462,11 +480,13 @@ namespace Umbraco.Web.Search var defaultLang = _languageService.GetDefaultLanguageIsoCode(); foreach (var result in results) { - var entity = _mapper.Map(result, context => { - if(culture != null) { - context.SetCulture(culture); - } + var entity = _mapper.Map(result, context => + { + if (culture != null) + { + context.SetCulture(culture); } + } ); var intId = entity.Id.TryConvertTo(); diff --git a/src/Umbraco.Web/Security/AppBuilderExtensions.cs b/src/Umbraco.Web/Security/AppBuilderExtensions.cs index 8f33f10eea..23afc36f93 100644 --- a/src/Umbraco.Web/Security/AppBuilderExtensions.cs +++ b/src/Umbraco.Web/Security/AppBuilderExtensions.cs @@ -169,11 +169,31 @@ namespace Umbraco.Web.Security // Enables the application to validate the security stamp when the user // logs in. This is a security feature which is used when you // change a password or add an external login to your account. - OnValidateIdentity = SecurityStampValidator - .OnValidateIdentity( + OnValidateIdentity = context => + { + // capture the current ticket for the request + var identity = context.Identity; + + return SecurityStampValidator + .OnValidateIdentity( + // This will re-verify the security stamp at a throttled 30 mins + // (the standard/default set in aspnet identity). + // This ensures that if the security stamp has changed - i.e. passwords, + // external logins, or other security profile data changed behind the + // scenes while being logged in, that they are logged out and have + // to re-verify their identity. TimeSpan.FromMinutes(30), - (manager, user) => manager.GenerateUserIdentityAsync(user), - identity => identity.GetUserId()), + async (manager, user) => + { + var regenerated = await manager.GenerateUserIdentityAsync(user); + + // Keep any custom claims from the original identity + regenerated.MergeClaimsFromBackOfficeIdentity(identity); + + return regenerated; + }, + identity => identity.GetUserId())(context); + } }; diff --git a/src/Umbraco.Web/Security/BackOfficeClaimsIdentityFactory.cs b/src/Umbraco.Web/Security/BackOfficeClaimsIdentityFactory.cs index d61d2ea711..2883ae8faf 100644 --- a/src/Umbraco.Web/Security/BackOfficeClaimsIdentityFactory.cs +++ b/src/Umbraco.Web/Security/BackOfficeClaimsIdentityFactory.cs @@ -27,12 +27,6 @@ namespace Umbraco.Web.Security { var baseIdentity = await base.CreateAsync(manager, user, authenticationType); - // now we can flow any custom claims that the actual user has currently assigned which could be done in the OnExternalLogin callback - foreach (var claim in user.Claims) - { - baseIdentity.AddClaim(new Claim(claim.ClaimType, claim.ClaimValue)); - } - var umbracoIdentity = new UmbracoBackOfficeIdentity(baseIdentity, user.Id, user.UserName, @@ -40,12 +34,16 @@ namespace Umbraco.Web.Security user.CalculatedContentStartNodeIds, user.CalculatedMediaStartNodeIds, user.Culture, - //NOTE - there is no session id assigned here, this is just creating the identity, a session id will be generated when the cookie is written + // NOTE - there is no session id assigned here, this is just creating the identity, a session id will be generated when the cookie is written Guid.NewGuid().ToString(), user.SecurityStamp, user.AllowedSections, user.Roles.Select(x => x.RoleId).ToArray()); + // now we can flow any custom claims that the actual user has currently + // assigned which could be done in the OnExternalLogin callback + umbracoIdentity.MergeClaimsFromBackOfficeIdentity(user); + return umbracoIdentity; } } diff --git a/src/Umbraco.Web/Security/ClaimsIdentityExtensions.cs b/src/Umbraco.Web/Security/ClaimsIdentityExtensions.cs new file mode 100644 index 0000000000..aa8549462d --- /dev/null +++ b/src/Umbraco.Web/Security/ClaimsIdentityExtensions.cs @@ -0,0 +1,34 @@ +using System.Linq; +using System.Security.Claims; +using Umbraco.Core.Models.Identity; +using Constants = Umbraco.Core.Constants; + +namespace Umbraco.Web.Security +{ + internal static class ClaimsIdentityExtensions + { + // Ignore these Claims when merging, these claims are dynamically added whenever the ticket + // is re-issued and we don't want to merge old values of these. + private static readonly string[] IgnoredClaims = new[] { ClaimTypes.CookiePath, Constants.Security.SessionIdClaimType }; + + internal static void MergeClaimsFromBackOfficeIdentity(this ClaimsIdentity destination, ClaimsIdentity source) + { + foreach (var claim in source.Claims + .Where(claim => !IgnoredClaims.Contains(claim.Type)) + .Where(claim => !destination.HasClaim(claim.Type, claim.Value))) + { + destination.AddClaim(new Claim(claim.Type, claim.Value)); + } + } + + internal static void MergeClaimsFromBackOfficeIdentity(this ClaimsIdentity destination, BackOfficeIdentityUser source) + { + foreach (var claim in source.Claims + .Where(claim => !IgnoredClaims.Contains(claim.ClaimType)) + .Where(claim => !destination.HasClaim(claim.ClaimType, claim.ClaimValue))) + { + destination.AddClaim(new Claim(claim.ClaimType, claim.ClaimValue)); + } + } + } +} diff --git a/src/Umbraco.Web/Security/MembershipHelper.cs b/src/Umbraco.Web/Security/MembershipHelper.cs index e655b87767..49ed6f6f99 100644 --- a/src/Umbraco.Web/Security/MembershipHelper.cs +++ b/src/Umbraco.Web/Security/MembershipHelper.cs @@ -315,7 +315,7 @@ namespace Umbraco.Web.Security if (member == null) { //this should not happen - Current.Logger.Warn("The member validated but then no member was returned with the username {Username}", username); + Current.Logger.Warn("The member validated but then no member was returned with the username {Username}", username); return false; } //Log them in diff --git a/src/Umbraco.Web/Security/Providers/UmbracoMembershipProvider.cs b/src/Umbraco.Web/Security/Providers/UmbracoMembershipProvider.cs index bf9ee654c4..0e2fd01106 100644 --- a/src/Umbraco.Web/Security/Providers/UmbracoMembershipProvider.cs +++ b/src/Umbraco.Web/Security/Providers/UmbracoMembershipProvider.cs @@ -3,7 +3,6 @@ using System.Collections.Specialized; using System.Configuration.Provider; using System.Linq; using System.Text; -using System.Web; using System.Web.Configuration; using System.Web.Security; using Umbraco.Core; @@ -14,7 +13,6 @@ using Umbraco.Core.Persistence.Querying; using Umbraco.Core.Security; using Umbraco.Core.Services; using Umbraco.Web.Composing; -using Umbraco.Core.Models.Identity; namespace Umbraco.Web.Security.Providers { @@ -149,7 +147,7 @@ namespace Umbraco.Web.Security.Providers if (MemberService.Exists(username)) { status = MembershipCreateStatus.DuplicateUserName; - Current.Logger.Warn>("Cannot create member as username already exists: {Username}", username); + Current.Logger.Warn, string>("Cannot create member as username already exists: {Username}", username); return null; } @@ -157,7 +155,7 @@ namespace Umbraco.Web.Security.Providers if (MemberService.GetByEmail(email) != null && RequiresUniqueEmail) { status = MembershipCreateStatus.DuplicateEmail; - Current.Logger.Warn>("Cannot create member as a member with the same email address exists: {Email}", email); + Current.Logger.Warn, string>("Cannot create member as a member with the same email address exists: {Email}", email); return null; } @@ -357,7 +355,7 @@ namespace Umbraco.Web.Security.Providers member.LastLoginDate = now; member.UpdateDate = now; } - + } return ConvertToMembershipUser(member); @@ -525,7 +523,7 @@ namespace Umbraco.Web.Security.Providers if (member == null) { - Current.Logger.Info("Login attempt failed for username {Username} from IP address {IpAddress}, the user does not exist", username, GetCurrentRequestIpAddress()); + Current.Logger.Info("Login attempt failed for username {Username} from IP address {IpAddress}, the user does not exist", username, GetCurrentRequestIpAddress()); return new ValidateUserResult { @@ -535,7 +533,7 @@ namespace Umbraco.Web.Security.Providers if (member.IsApproved == false) { - Current.Logger.Info("Login attempt failed for username {Username} from IP address {IpAddress}, the user is not approved", username, GetCurrentRequestIpAddress()); + Current.Logger.Info("Login attempt failed for username {Username} from IP address {IpAddress}, the user is not approved", username, GetCurrentRequestIpAddress()); return new ValidateUserResult { @@ -545,7 +543,7 @@ namespace Umbraco.Web.Security.Providers } if (member.IsLockedOut) { - Current.Logger.Info("Login attempt failed for username {Username} from IP address {IpAddress}, the user is locked", username, GetCurrentRequestIpAddress()); + Current.Logger.Info("Login attempt failed for username {Username} from IP address {IpAddress}, the user is locked", username, GetCurrentRequestIpAddress()); return new ValidateUserResult { @@ -571,11 +569,11 @@ namespace Umbraco.Web.Security.Providers member.IsLockedOut = true; member.LastLockoutDate = DateTime.Now; - Current.Logger.Info("Login attempt failed for username {Username} from IP address {IpAddress}, the user is now locked out, max invalid password attempts exceeded", username, GetCurrentRequestIpAddress()); + Current.Logger.Info("Login attempt failed for username {Username} from IP address {IpAddress}, the user is now locked out, max invalid password attempts exceeded", username, GetCurrentRequestIpAddress()); } else { - Current.Logger.Info("Login attempt failed for username {Username} from IP address {IpAddress}", username, GetCurrentRequestIpAddress()); + Current.Logger.Info("Login attempt failed for username {Username} from IP address {IpAddress}", username, GetCurrentRequestIpAddress()); } requiresFullSave = true; @@ -591,7 +589,7 @@ namespace Umbraco.Web.Security.Providers member.LastLoginDate = DateTime.Now; - Current.Logger.Info("Login attempt succeeded for username {Username} from IP address {IpAddress}", username, GetCurrentRequestIpAddress()); + Current.Logger.Info("Login attempt succeeded for username {Username} from IP address {IpAddress}", username, GetCurrentRequestIpAddress()); } // don't raise events for this! It just sets the member dates, if we do raise events this will @@ -604,11 +602,21 @@ namespace Umbraco.Web.Security.Providers { // when upgrading from 7.2 to 7.3 trying to save will throw if (UmbracoVersion.Current >= new Version(7, 3, 0, 0)) - MemberService.Save(member, false); + { + // We need to raise event to ensure caches are updated. (e.g. the cache that uses username as key). + // Even that this is a heavy operation, because indexes are updates, we consider that okay, as it + // is still cheap to do a successful login. + MemberService.Save(member, true); + } + } else { - // set the last login date without full save (fast, no locks) + // set the last login date without full save (fast, no locks). + // We do not update caches. This is to the best of our knowledge okay, as this info are only stored + // because it is required by the membership provider. + // If we one day have to revisit this, we will most likely need to spilt the events in membership info + // saved and umbraco info saved. We don't want to update indexes etc when it is just membership info that is saved MemberService.SetLastLogin(member.Username, member.LastLoginDate); } diff --git a/src/Umbraco.Web/Services/IconService.cs b/src/Umbraco.Web/Services/IconService.cs index 206af296f9..fad53103c0 100644 --- a/src/Umbraco.Web/Services/IconService.cs +++ b/src/Umbraco.Web/Services/IconService.cs @@ -1,8 +1,9 @@ +using System; using System.Collections.Generic; using System.IO; using System.Linq; -using Ganss.XSS; using Umbraco.Core; +using Umbraco.Core.Cache; using Umbraco.Core.Configuration; using Umbraco.Core.IO; using Umbraco.Core.Models; @@ -13,39 +14,42 @@ namespace Umbraco.Web.Services public class IconService : IIconService { private readonly IGlobalSettings _globalSettings; + private readonly IAppPolicyCache _cache; - public IconService(IGlobalSettings globalSettings) + public IconService(IGlobalSettings globalSettings, AppCaches appCaches) { _globalSettings = globalSettings; + _cache = appCaches.RuntimeCache; } - /// - public IList GetAllIcons() - { - var icons = new List(); - var directory = new DirectoryInfo(IOHelper.MapPath($"{_globalSettings.IconsPath}/")); - var iconNames = directory.GetFiles("*.svg"); + public IReadOnlyDictionary GetIcons() => GetIconDictionary(); - iconNames.OrderBy(f => f.Name).ToList().ForEach(iconInfo => - { - var icon = GetIcon(iconInfo); - - if (icon != null) - { - icons.Add(icon); - } - }); - - return icons; - } + /// + public IList GetAllIcons() => + GetIconDictionary() + .Select(x => new IconModel { Name = x.Key, SvgString = x.Value }) + .ToList(); /// public IconModel GetIcon(string iconName) { - return string.IsNullOrWhiteSpace(iconName) - ? null - : CreateIconModel(iconName.StripFileExtension(), IOHelper.MapPath($"{_globalSettings.IconsPath}/{iconName}.svg")); + if (iconName.IsNullOrWhiteSpace()) + { + return null; + } + + var allIconModels = GetIconDictionary(); + if (allIconModels.ContainsKey(iconName)) + { + return new IconModel + { + Name = iconName, + SvgString = allIconModels[iconName] + }; + } + + return null; } /// @@ -68,20 +72,14 @@ namespace Umbraco.Web.Services /// private IconModel CreateIconModel(string iconName, string iconPath) { - var sanitizer = new HtmlSanitizer(); - sanitizer.AllowedAttributes.UnionWith(Constants.SvgSanitizer.Attributes); - sanitizer.AllowedCssProperties.UnionWith(Constants.SvgSanitizer.Attributes); - sanitizer.AllowedTags.UnionWith(Constants.SvgSanitizer.Tags); - try { var svgContent = System.IO.File.ReadAllText(iconPath); - var sanitizedString = sanitizer.Sanitize(svgContent); var svg = new IconModel { Name = iconName, - SvgString = sanitizedString + SvgString = svgContent }; return svg; @@ -91,5 +89,52 @@ namespace Umbraco.Web.Services return null; } } + + private IEnumerable GetAllIconsFiles() + { + var icons = new HashSet(new CaseInsensitiveFileInfoComparer()); + + // add icons from plugins + var appPluginsDirectoryPath = IOHelper.MapPath(SystemDirectories.AppPlugins); + if (Directory.Exists(appPluginsDirectoryPath)) + { + var appPlugins = new DirectoryInfo(appPluginsDirectoryPath); + + // iterate sub directories of app plugins + foreach (var dir in appPlugins.EnumerateDirectories()) + { + var iconPath = IOHelper.MapPath($"{SystemDirectories.AppPlugins}/{dir.Name}{SystemDirectories.AppPluginIcons}"); + if (Directory.Exists(iconPath)) + { + var dirIcons = new DirectoryInfo(iconPath).EnumerateFiles("*.svg", SearchOption.TopDirectoryOnly); + icons.UnionWith(dirIcons); + } + } + } + + // add icons from IconsPath if not already added from plugins + var coreIconsDirectory = new DirectoryInfo(IOHelper.MapPath($"{_globalSettings.IconsPath}/")); + var coreIcons = coreIconsDirectory.GetFiles("*.svg"); + + icons.UnionWith(coreIcons); + + return icons; + } + + private class CaseInsensitiveFileInfoComparer : IEqualityComparer + { + public bool Equals(FileInfo one, FileInfo two) => StringComparer.InvariantCultureIgnoreCase.Equals(one.Name, two.Name); + + public int GetHashCode(FileInfo item) => StringComparer.InvariantCultureIgnoreCase.GetHashCode(item.Name); + } + + private IReadOnlyDictionary GetIconDictionary() => _cache.GetCacheItem( + $"{typeof(IconService).FullName}.{nameof(GetIconDictionary)}", + () => GetAllIconsFiles() + .Select(GetIcon) + .Where(i => i != null) + .GroupBy(i => i.Name, StringComparer.OrdinalIgnoreCase) + .ToDictionary(g => g.Key, g => g.First().SvgString, StringComparer.OrdinalIgnoreCase) + ); } } diff --git a/src/Umbraco.Web/Templates/HtmlUrlParser.cs b/src/Umbraco.Web/Templates/HtmlUrlParser.cs index 0692b75fa7..721e8c04f4 100644 --- a/src/Umbraco.Web/Templates/HtmlUrlParser.cs +++ b/src/Umbraco.Web/Templates/HtmlUrlParser.cs @@ -36,7 +36,7 @@ namespace Umbraco.Web.Templates { // find all relative URLs (ie. URLs that contain ~) var tags = ResolveUrlPattern.Matches(text); - _logger.Debug(typeof(IOHelper), "After regex: {Duration} matched: {TagsCount}", timer.Stopwatch.ElapsedMilliseconds, tags.Count); + _logger.Debug(typeof(IOHelper), "After regex: {Duration} matched: {TagsCount}", timer.Stopwatch.ElapsedMilliseconds, tags.Count); foreach (Match tag in tags) { var url = ""; diff --git a/src/Umbraco.Web/Trees/ContentTreeController.cs b/src/Umbraco.Web/Trees/ContentTreeController.cs index ead265e7c5..d82166b9a3 100644 --- a/src/Umbraco.Web/Trees/ContentTreeController.cs +++ b/src/Umbraco.Web/Trees/ContentTreeController.cs @@ -47,7 +47,7 @@ namespace Umbraco.Web.Trees private int[] _userStartNodes; protected override int[] UserStartNodes - => _userStartNodes ?? (_userStartNodes = Security.CurrentUser.CalculateContentStartNodeIds(Services.EntityService)); + => _userStartNodes ?? (_userStartNodes = Security.CurrentUser.CalculateContentStartNodeIds(Services.EntityService, AppCaches)); public ContentTreeController(UmbracoTreeSearcher treeSearcher, ActionCollection actions, IGlobalSettings globalSettings, IUmbracoContextAccessor umbracoContextAccessor, ISqlContext sqlContext, ServiceContext services, AppCaches appCaches, IProfilingLogger logger, IRuntimeState runtimeState, UmbracoHelper umbracoHelper) : base(globalSettings, umbracoContextAccessor, sqlContext, services, appCaches, logger, runtimeState, umbracoHelper) { @@ -165,7 +165,7 @@ namespace Umbraco.Web.Trees } //if the user has no path access for this node, all they can do is refresh - if (!Security.CurrentUser.HasContentPathAccess(item, Services.EntityService)) + if (!Security.CurrentUser.HasContentPathAccess(item, Services.EntityService, AppCaches)) { var menu = new MenuItemCollection(); menu.Items.Add(new RefreshNode(Services.TextService, true)); diff --git a/src/Umbraco.Web/Trees/ContentTreeControllerBase.cs b/src/Umbraco.Web/Trees/ContentTreeControllerBase.cs index 684a3fdab8..3285b97f04 100644 --- a/src/Umbraco.Web/Trees/ContentTreeControllerBase.cs +++ b/src/Umbraco.Web/Trees/ContentTreeControllerBase.cs @@ -126,12 +126,12 @@ namespace Umbraco.Web.Trees switch (RecycleBinId) { case Constants.System.RecycleBinMedia: - startNodeIds = Security.CurrentUser.CalculateMediaStartNodeIds(Services.EntityService); - startNodePaths = Security.CurrentUser.GetMediaStartNodePaths(Services.EntityService); + startNodeIds = Security.CurrentUser.CalculateMediaStartNodeIds(Services.EntityService, AppCaches); + startNodePaths = Security.CurrentUser.GetMediaStartNodePaths(Services.EntityService, AppCaches); break; case Constants.System.RecycleBinContent: - startNodeIds = Security.CurrentUser.CalculateContentStartNodeIds(Services.EntityService); - startNodePaths = Security.CurrentUser.GetContentStartNodePaths(Services.EntityService); + startNodeIds = Security.CurrentUser.CalculateContentStartNodeIds(Services.EntityService, AppCaches); + startNodePaths = Security.CurrentUser.GetContentStartNodePaths(Services.EntityService, AppCaches); break; default: throw new NotSupportedException("Path access is only determined on content or media"); @@ -175,7 +175,7 @@ namespace Umbraco.Web.Trees // TODO: in the future we could return a validation statement so we can have some UI to notify the user they don't have access if (ignoreUserStartNodes == false && HasPathAccess(id, queryStrings) == false) { - Logger.Warn("User {Username} does not have access to node with id {Id}", Security.CurrentUser.Username, id); + Logger.Warn("User {Username} does not have access to node with id {Id}", Security.CurrentUser.Username, id); return nodes; } @@ -291,8 +291,8 @@ namespace Umbraco.Web.Trees { if (entity == null) return false; return RecycleBinId == Constants.System.RecycleBinContent - ? Security.CurrentUser.HasContentPathAccess(entity, Services.EntityService) - : Security.CurrentUser.HasMediaPathAccess(entity, Services.EntityService); + ? Security.CurrentUser.HasContentPathAccess(entity, Services.EntityService, AppCaches) + : Security.CurrentUser.HasMediaPathAccess(entity, Services.EntityService, AppCaches); } /// diff --git a/src/Umbraco.Web/Trees/DataTypeTreeController.cs b/src/Umbraco.Web/Trees/DataTypeTreeController.cs index c0727d6e78..98c9b1812e 100644 --- a/src/Umbraco.Web/Trees/DataTypeTreeController.cs +++ b/src/Umbraco.Web/Trees/DataTypeTreeController.cs @@ -46,7 +46,7 @@ namespace Umbraco.Web.Trees .OrderBy(entity => entity.Name) .Select(dt => { - var node = CreateTreeNode(dt, Constants.ObjectTypes.DataType, id, queryStrings, "icon-folder", dt.HasChildren); + var node = CreateTreeNode(dt, Constants.ObjectTypes.DataType, id, queryStrings, Constants.Icons.Folder, dt.HasChildren); node.Path = dt.Path; node.NodeType = "container"; // TODO: This isn't the best way to ensure a no operation process for clicking a node but it works for now. diff --git a/src/Umbraco.Web/Trees/DictionaryTreeController.cs b/src/Umbraco.Web/Trees/DictionaryTreeController.cs index f21f0b2868..6540eb4a63 100644 --- a/src/Umbraco.Web/Trees/DictionaryTreeController.cs +++ b/src/Umbraco.Web/Trees/DictionaryTreeController.cs @@ -67,7 +67,7 @@ namespace Umbraco.Web.Trees id, queryStrings, x.ItemKey, - "icon-book-alt", + Constants.Icons.Dictionary, Services.LocalizationService.GetDictionaryItemChildren(x.Key).Any()))); } else @@ -83,7 +83,7 @@ namespace Umbraco.Web.Trees id, queryStrings, x.ItemKey, - "icon-book-alt", + Constants.Icons.Dictionary, Services.LocalizationService.GetDictionaryItemChildren(x.Key).Any()))); } diff --git a/src/Umbraco.Web/Trees/LanguageTreeController.cs b/src/Umbraco.Web/Trees/LanguageTreeController.cs index ac2c0571e0..863f94a9f5 100644 --- a/src/Umbraco.Web/Trees/LanguageTreeController.cs +++ b/src/Umbraco.Web/Trees/LanguageTreeController.cs @@ -34,7 +34,7 @@ namespace Umbraco.Web.Trees //this will load in a custom UI instead of the dashboard for the root node root.RoutePath = $"{Constants.Applications.Settings}/{Constants.Trees.Languages}/overview"; - root.Icon = "icon-globe"; + root.Icon = Constants.Icons.Language; root.HasChildren = false; root.MenuUrl = null; diff --git a/src/Umbraco.Web/Trees/LogViewerTreeController.cs b/src/Umbraco.Web/Trees/LogViewerTreeController.cs index 7452828d00..964852d4bd 100644 --- a/src/Umbraco.Web/Trees/LogViewerTreeController.cs +++ b/src/Umbraco.Web/Trees/LogViewerTreeController.cs @@ -33,8 +33,8 @@ namespace Umbraco.Web.Trees var root = base.CreateRootNode(queryStrings); //this will load in a custom UI instead of the dashboard for the root node - root.RoutePath = string.Format("{0}/{1}/{2}", Constants.Applications.Settings, Constants.Trees.LogViewer, "overview"); - root.Icon = "icon-box-alt"; + root.RoutePath = $"{Constants.Applications.Settings}/{Constants.Trees.LogViewer}/overview"; + root.Icon = Constants.Icons.LogViewer; root.HasChildren = false; root.MenuUrl = null; diff --git a/src/Umbraco.Web/Trees/MediaTreeController.cs b/src/Umbraco.Web/Trees/MediaTreeController.cs index cc71b92d15..43b5a83282 100644 --- a/src/Umbraco.Web/Trees/MediaTreeController.cs +++ b/src/Umbraco.Web/Trees/MediaTreeController.cs @@ -50,7 +50,7 @@ namespace Umbraco.Web.Trees private int[] _userStartNodes; protected override int[] UserStartNodes - => _userStartNodes ?? (_userStartNodes = Security.CurrentUser.CalculateMediaStartNodeIds(Services.EntityService)); + => _userStartNodes ?? (_userStartNodes = Security.CurrentUser.CalculateMediaStartNodeIds(Services.EntityService, AppCaches)); /// /// Creates a tree node for a content item based on an UmbracoEntity @@ -117,7 +117,7 @@ namespace Umbraco.Web.Trees } //if the user has no path access for this node, all they can do is refresh - if (!Security.CurrentUser.HasMediaPathAccess(item, Services.EntityService)) + if (!Security.CurrentUser.HasMediaPathAccess(item, Services.EntityService, AppCaches)) { menu.Items.Add(new RefreshNode(Services.TextService, true)); return menu; diff --git a/src/Umbraco.Web/Trees/MemberTreeController.cs b/src/Umbraco.Web/Trees/MemberTreeController.cs index 0bf1787402..616ee7d579 100644 --- a/src/Umbraco.Web/Trees/MemberTreeController.cs +++ b/src/Umbraco.Web/Trees/MemberTreeController.cs @@ -129,10 +129,12 @@ namespace Umbraco.Web.Trees if (_isUmbracoProvider) { - nodes.AddRange(Services.MemberTypeService.GetAll() - .Select(memberType => - CreateTreeNode(memberType.Alias, id, queryStrings, memberType.Name, memberType.Icon.IfNullOrWhiteSpace(Constants.Icons.Member), true, - queryStrings.GetRequiredValue("application") + TreeAlias.EnsureStartsWith('/') + "/list/" + memberType.Alias))); + nodes.AddRange( + Services.MemberTypeService.GetAll() + .OrderBy(x => x.Name) + .Select(memberType => + CreateTreeNode(memberType.Alias, id, queryStrings, memberType.Name, memberType.Icon.IfNullOrWhiteSpace(Constants.Icons.Member), true, + queryStrings.GetRequiredValue("application") + TreeAlias.EnsureStartsWith('/') + "/list/" + memberType.Alias))); } } diff --git a/src/Umbraco.Web/Trees/MemberTypeAndGroupTreeControllerBase.cs b/src/Umbraco.Web/Trees/MemberTypeAndGroupTreeControllerBase.cs index 5e71266bca..61b9b3e063 100644 --- a/src/Umbraco.Web/Trees/MemberTypeAndGroupTreeControllerBase.cs +++ b/src/Umbraco.Web/Trees/MemberTypeAndGroupTreeControllerBase.cs @@ -13,6 +13,10 @@ namespace Umbraco.Web.Trees protected override TreeNodeCollection GetTreeNodes(string id, FormDataCollection queryStrings) { var nodes = new TreeNodeCollection(); + + // if the request is for folders only then just return + if (queryStrings["foldersonly"].IsNullOrWhiteSpace() == false && queryStrings["foldersonly"] == "1") return nodes; + nodes.AddRange(GetTreeNodesFromService(id, queryStrings)); return nodes; } @@ -30,7 +34,13 @@ namespace Umbraco.Web.Trees } else { - //delete member type/group + var memberType = Services.MemberTypeService.Get(int.Parse(id)); + if (memberType != null) + { + menu.Items.Add(Services.TextService, opensDialog: true); + } + + // delete member type/group menu.Items.Add(Services.TextService, opensDialog: true); } diff --git a/src/Umbraco.Web/Trees/MemberTypeTreeController.cs b/src/Umbraco.Web/Trees/MemberTypeTreeController.cs index 5db9088f20..85f61d5fed 100644 --- a/src/Umbraco.Web/Trees/MemberTypeTreeController.cs +++ b/src/Umbraco.Web/Trees/MemberTypeTreeController.cs @@ -29,6 +29,7 @@ namespace Umbraco.Web.Trees root.HasChildren = Services.MemberTypeService.GetAll().Any(); return root; } + protected override IEnumerable GetTreeNodesFromService(string id, FormDataCollection queryStrings) { return Services.MemberTypeService.GetAll() diff --git a/src/Umbraco.Web/Trees/PackagesTreeController.cs b/src/Umbraco.Web/Trees/PackagesTreeController.cs index 9b1bf98823..b6921771b6 100644 --- a/src/Umbraco.Web/Trees/PackagesTreeController.cs +++ b/src/Umbraco.Web/Trees/PackagesTreeController.cs @@ -23,7 +23,7 @@ namespace Umbraco.Web.Trees //this will load in a custom UI instead of the dashboard for the root node root.RoutePath = $"{Constants.Applications.Packages}/{Constants.Trees.Packages}/repo"; - root.Icon = "icon-box"; + root.Icon = Constants.Icons.Packages; root.HasChildren = false; return root; diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index 8890d9cf25..a6cbefa825 100644 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -63,11 +63,8 @@ - + - - 4.0.217 - 2.7.0.100 @@ -257,6 +254,8 @@ + + @@ -288,6 +287,7 @@ + @@ -1306,4 +1306,4 @@ - \ No newline at end of file + diff --git a/src/Umbraco.Web/UmbracoApplicationBase.cs b/src/Umbraco.Web/UmbracoApplicationBase.cs index 32a949e972..c9a4e18dd6 100644 --- a/src/Umbraco.Web/UmbracoApplicationBase.cs +++ b/src/Umbraco.Web/UmbracoApplicationBase.cs @@ -132,7 +132,7 @@ namespace Umbraco.Web BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.GetField, null, runtime, null); - Current.Logger.Info("Application shutdown. Details: {ShutdownReason}\r\n\r\n_shutDownMessage={ShutdownMessage}\r\n\r\n_shutDownStack={ShutdownStack}", + Current.Logger.Info("Application shutdown. Details: {ShutdownReason}\r\n\r\n_shutDownMessage={ShutdownMessage}\r\n\r\n_shutDownStack={ShutdownStack}", HostingEnvironment.ShutdownReason, shutDownMessage, shutDownStack); @@ -140,7 +140,7 @@ namespace Umbraco.Web catch (Exception) { //if for some reason that fails, then log the normal output - Current.Logger.Info("Application shutdown. Reason: {ShutdownReason}", HostingEnvironment.ShutdownReason); + Current.Logger.Info("Application shutdown. Reason: {ShutdownReason}", HostingEnvironment.ShutdownReason); } Current.Logger.DisposeIfDisposable(); @@ -195,7 +195,7 @@ namespace Umbraco.Web } catch (Exception ex) { - Current.Logger.Error(ex, "Error in {Name} handler.", name); + Current.Logger.Error(ex, "Error in {Name} handler.", name); throw; } } diff --git a/src/Umbraco.Web/UmbracoInjectedModule.cs b/src/Umbraco.Web/UmbracoInjectedModule.cs index cc130a7b56..c2ee4ce535 100644 --- a/src/Umbraco.Web/UmbracoInjectedModule.cs +++ b/src/Umbraco.Web/UmbracoInjectedModule.cs @@ -328,7 +328,7 @@ namespace Umbraco.Web } catch (Exception ex) { - _logger.Error("Could not dispose item with key " + k, ex); + _logger.Error(ex, "Could not dispose item with key {Key}", k); } try { @@ -336,7 +336,7 @@ namespace Umbraco.Web } catch (Exception ex) { - _logger.Error("Could not dispose item key " + k, ex); + _logger.Error(ex, "Could not dispose item key {Key}", k); } } } diff --git a/src/Umbraco.Web/UmbracoModule.cs b/src/Umbraco.Web/UmbracoModule.cs index 2ec4141f5a..bfa5361a8a 100644 --- a/src/Umbraco.Web/UmbracoModule.cs +++ b/src/Umbraco.Web/UmbracoModule.cs @@ -50,7 +50,7 @@ namespace Umbraco.Web var end = false; var response = context.Response; - logger.Debug("Response status: Redirect={Redirect}, Is404={Is404}, StatusCode={ResponseStatusCode}", + logger.Debug("Response status: Redirect={Redirect}, Is404={Is404}, StatusCode={ResponseStatusCode}", pcr.IsRedirect ? (pcr.IsRedirectPermanent ? "permanent" : "redirect") : "none", pcr.Is404 ? "true" : "false", pcr.ResponseStatusCode); diff --git a/src/Umbraco.Web/WebApi/AngularJsonMediaTypeFormatter.cs b/src/Umbraco.Web/WebApi/AngularJsonMediaTypeFormatter.cs index 0e7cf6453a..874afce0e1 100644 --- a/src/Umbraco.Web/WebApi/AngularJsonMediaTypeFormatter.cs +++ b/src/Umbraco.Web/WebApi/AngularJsonMediaTypeFormatter.cs @@ -47,8 +47,8 @@ namespace Umbraco.Web.WebApi { //write the special encoding for angular json to the start // (see: http://docs.angularjs.org/api/ng.$http) - streamWriter.Write(XsrfPrefix); - streamWriter.Flush(); + await streamWriter.WriteAsync(XsrfPrefix); + await streamWriter.FlushAsync(); await base.WriteToStreamAsync(type, value, writeStream, content, transportContext); } } diff --git a/src/Umbraco.Web/WebApi/Filters/AdminUsersAuthorizeAttribute.cs b/src/Umbraco.Web/WebApi/Filters/AdminUsersAuthorizeAttribute.cs index 96226622b0..3494a5c0c7 100644 --- a/src/Umbraco.Web/WebApi/Filters/AdminUsersAuthorizeAttribute.cs +++ b/src/Umbraco.Web/WebApi/Filters/AdminUsersAuthorizeAttribute.cs @@ -50,7 +50,7 @@ namespace Umbraco.Web.WebApi.Filters if (userIds.Length == 0) return base.IsAuthorized(actionContext); var users = Current.Services.UserService.GetUsersById(userIds); - var authHelper = new UserEditorAuthorizationHelper(Current.Services.ContentService, Current.Services.MediaService, Current.Services.UserService, Current.Services.EntityService); + var authHelper = new UserEditorAuthorizationHelper(Current.Services.ContentService, Current.Services.MediaService, Current.Services.UserService, Current.Services.EntityService, Current.AppCaches); return users.All(user => authHelper.IsAuthorized(Current.UmbracoContext.Security.CurrentUser, user, null, null, null) != false); } } diff --git a/src/Umbraco.Web/WebApi/Filters/CheckIfUserTicketDataIsStaleAttribute.cs b/src/Umbraco.Web/WebApi/Filters/CheckIfUserTicketDataIsStaleAttribute.cs index 2a57ec10b2..3bbee7ca41 100644 --- a/src/Umbraco.Web/WebApi/Filters/CheckIfUserTicketDataIsStaleAttribute.cs +++ b/src/Umbraco.Web/WebApi/Filters/CheckIfUserTicketDataIsStaleAttribute.cs @@ -85,12 +85,12 @@ namespace Umbraco.Web.WebApi.Filters () => user.Groups.Select(x => x.Alias).UnsortedSequenceEqual(identity.Roles) == false, () => { - var startContentIds = UserExtensions.CalculateContentStartNodeIds(user, Current.Services.EntityService); + var startContentIds = UserExtensions.CalculateContentStartNodeIds(user, Current.Services.EntityService, Current.AppCaches); return startContentIds.UnsortedSequenceEqual(identity.StartContentNodes) == false; }, () => { - var startMediaIds = UserExtensions.CalculateMediaStartNodeIds(user, Current.Services.EntityService); + var startMediaIds = UserExtensions.CalculateMediaStartNodeIds(user, Current.Services.EntityService, Current.AppCaches); return startMediaIds.UnsortedSequenceEqual(identity.StartMediaNodes) == false; } }; diff --git a/src/Umbraco.Web/WebApi/Filters/EnsureUserPermissionForContentAttribute.cs b/src/Umbraco.Web/WebApi/Filters/EnsureUserPermissionForContentAttribute.cs index 550b4dcdf1..0f08d353a6 100644 --- a/src/Umbraco.Web/WebApi/Filters/EnsureUserPermissionForContentAttribute.cs +++ b/src/Umbraco.Web/WebApi/Filters/EnsureUserPermissionForContentAttribute.cs @@ -115,11 +115,13 @@ namespace Umbraco.Web.WebApi.Filters nodeId = _nodeId.Value; } - var permissionResult = ContentPermissionsHelper.CheckPermissions(nodeId, + var permissionResult = ContentPermissionsHelper.CheckPermissions( + nodeId, Current.UmbracoContext.Security.CurrentUser, Current.Services.UserService, Current.Services.ContentService, Current.Services.EntityService, + Current.AppCaches, out var contentItem, _permissionToCheck.HasValue ? new[] { _permissionToCheck.Value } : null); diff --git a/src/Umbraco.Web/WebApi/Filters/EnsureUserPermissionForMediaAttribute.cs b/src/Umbraco.Web/WebApi/Filters/EnsureUserPermissionForMediaAttribute.cs index 3c60f43be0..4506fcf14d 100644 --- a/src/Umbraco.Web/WebApi/Filters/EnsureUserPermissionForMediaAttribute.cs +++ b/src/Umbraco.Web/WebApi/Filters/EnsureUserPermissionForMediaAttribute.cs @@ -124,6 +124,7 @@ namespace Umbraco.Web.WebApi.Filters Current.UmbracoContext.Security.CurrentUser, Current.Services.MediaService, Current.Services.EntityService, + Current.AppCaches, nodeId)) { base.OnActionExecuting(actionContext); diff --git a/src/Umbraco.Web/WebApi/Filters/FileUploadCleanupFilterAttribute.cs b/src/Umbraco.Web/WebApi/Filters/FileUploadCleanupFilterAttribute.cs index 1646c8a426..a5c0c2bc17 100644 --- a/src/Umbraco.Web/WebApi/Filters/FileUploadCleanupFilterAttribute.cs +++ b/src/Umbraco.Web/WebApi/Filters/FileUploadCleanupFilterAttribute.cs @@ -57,7 +57,7 @@ namespace Umbraco.Web.WebApi.Filters } catch (System.Exception ex) { - Current.Logger.Error(ex, "Could not delete temp file {FileName}", f.TempFilePath); + Current.Logger.Error(ex, "Could not delete temp file {FileName}", f.TempFilePath); } } } @@ -111,7 +111,7 @@ namespace Umbraco.Web.WebApi.Filters tempFolders.Add(dir); } - Current.Logger.Debug("Removing temp file {FileName}", f.TempFilePath); + Current.Logger.Debug("Removing temp file {FileName}", f.TempFilePath); try { @@ -119,7 +119,7 @@ namespace Umbraco.Web.WebApi.Filters } catch (System.Exception ex) { - Current.Logger.Error(ex, "Could not delete temp file {FileName}", f.TempFilePath); + Current.Logger.Error(ex, "Could not delete temp file {FileName}", f.TempFilePath); } //clear out the temp path so it's not returned in the response @@ -138,12 +138,12 @@ namespace Umbraco.Web.WebApi.Filters } else { - Current.Logger.Warn("The actionExecutedContext.Request.Content.Value is not IHaveUploadedFiles, it is {ObjectType}", objectContent.Value.GetType()); + Current.Logger.Warn("The actionExecutedContext.Request.Content.Value is not IHaveUploadedFiles, it is {ObjectType}", objectContent.Value.GetType()); } } else { - Current.Logger.Warn("The actionExecutedContext.Request.Content is not ObjectContent, it is {RequestObjectType}", actionExecutedContext.Request.Content.GetType()); + Current.Logger.Warn("The actionExecutedContext.Request.Content is not ObjectContent, it is {RequestObjectType}", actionExecutedContext.Request.Content.GetType()); } } } diff --git a/src/Umbraco.Web/WebApi/Filters/FilterAllowedOutgoingContentAttribute.cs b/src/Umbraco.Web/WebApi/Filters/FilterAllowedOutgoingContentAttribute.cs index 31e0b22ce1..24fd60cd26 100644 --- a/src/Umbraco.Web/WebApi/Filters/FilterAllowedOutgoingContentAttribute.cs +++ b/src/Umbraco.Web/WebApi/Filters/FilterAllowedOutgoingContentAttribute.cs @@ -9,7 +9,7 @@ using Umbraco.Core; using Umbraco.Web.Composing; using Umbraco.Core.Models; using Umbraco.Web.Actions; - +using Umbraco.Core.Cache; namespace Umbraco.Web.WebApi.Filters { @@ -21,52 +21,58 @@ namespace Umbraco.Web.WebApi.Filters { private readonly IUserService _userService; private readonly IEntityService _entityService; + private readonly AppCaches _appCaches; private readonly char _permissionToCheck; public FilterAllowedOutgoingContentAttribute(Type outgoingType) - : this(outgoingType, Current.Services.UserService, Current.Services.EntityService) + : this(outgoingType, ActionBrowse.ActionLetter, string.Empty, Current.Services.UserService, Current.Services.EntityService, Current.AppCaches) { - _permissionToCheck = ActionBrowse.ActionLetter; } public FilterAllowedOutgoingContentAttribute(Type outgoingType, char permissionToCheck) - : this(outgoingType, Current.Services.UserService, Current.Services.EntityService) + : this(outgoingType, permissionToCheck, string.Empty, Current.Services.UserService, Current.Services.EntityService, Current.AppCaches) { - _permissionToCheck = permissionToCheck; } public FilterAllowedOutgoingContentAttribute(Type outgoingType, string propertyName) - : this(outgoingType, propertyName, Current.Services.UserService, Current.Services.EntityService) + : this(outgoingType, ActionBrowse.ActionLetter, propertyName, Current.Services.UserService, Current.Services.EntityService, Current.AppCaches) { - _permissionToCheck = ActionBrowse.ActionLetter; } public FilterAllowedOutgoingContentAttribute(Type outgoingType, IUserService userService, IEntityService entityService) - : base(outgoingType) + : this(outgoingType, ActionBrowse.ActionLetter, string.Empty, userService, entityService, Current.AppCaches) + { + } + + public FilterAllowedOutgoingContentAttribute(Type outgoingType, IUserService userService, IEntityService entityService, AppCaches appCaches) + : this(outgoingType, ActionBrowse.ActionLetter, string.Empty, userService, entityService, appCaches) { - _userService = userService ?? throw new ArgumentNullException(nameof(userService)); - _entityService = entityService ?? throw new ArgumentNullException(nameof(entityService)); - _permissionToCheck = ActionBrowse.ActionLetter; } public FilterAllowedOutgoingContentAttribute(Type outgoingType, char permissionToCheck, IUserService userService, IEntityService entityService) - : base(outgoingType) + : this(outgoingType, permissionToCheck, string.Empty, userService, entityService, Current.AppCaches) { - _userService = userService ?? throw new ArgumentNullException(nameof(userService)); - _entityService = entityService ?? throw new ArgumentNullException(nameof(entityService)); - _userService = userService; - _entityService = entityService; - _permissionToCheck = permissionToCheck; } public FilterAllowedOutgoingContentAttribute(Type outgoingType, string propertyName, IUserService userService, IEntityService entityService) + : this(outgoingType, ActionBrowse.ActionLetter, propertyName, userService, entityService, Current.AppCaches) + { + } + + public FilterAllowedOutgoingContentAttribute(Type outgoingType, string propertyName, IUserService userService, IEntityService entityService, AppCaches appCaches) + : this(outgoingType, ActionBrowse.ActionLetter, propertyName, userService, entityService, appCaches) + { + } + + private FilterAllowedOutgoingContentAttribute(Type outgoingType, char permissionToCheck, string propertyName, IUserService userService, IEntityService entityService, AppCaches appCaches) : base(outgoingType, propertyName) { _userService = userService ?? throw new ArgumentNullException(nameof(userService)); _entityService = entityService ?? throw new ArgumentNullException(nameof(entityService)); + _appCaches = appCaches; _userService = userService; _entityService = entityService; - _permissionToCheck = ActionBrowse.ActionLetter; + _permissionToCheck = permissionToCheck; } protected override void FilterItems(IUser user, IList items) @@ -78,7 +84,7 @@ namespace Umbraco.Web.WebApi.Filters protected override int[] GetUserStartNodes(IUser user) { - return user.CalculateContentStartNodeIds(_entityService); + return user.CalculateContentStartNodeIds(_entityService, _appCaches); } protected override int RecycleBinId @@ -110,7 +116,7 @@ namespace Umbraco.Web.WebApi.Filters if (nodePermission.Contains(_permissionToCheck.ToString(CultureInfo.InvariantCulture)) == false) { toRemove.Add(item); - } + } } foreach (var item in toRemove) { diff --git a/src/Umbraco.Web/WebApi/Filters/FilterAllowedOutgoingMediaAttribute.cs b/src/Umbraco.Web/WebApi/Filters/FilterAllowedOutgoingMediaAttribute.cs index 21dc60e6cc..5e308bd3c1 100644 --- a/src/Umbraco.Web/WebApi/Filters/FilterAllowedOutgoingMediaAttribute.cs +++ b/src/Umbraco.Web/WebApi/Filters/FilterAllowedOutgoingMediaAttribute.cs @@ -39,7 +39,7 @@ namespace Umbraco.Web.WebApi.Filters protected virtual int[] GetUserStartNodes(IUser user) { - return user.CalculateMediaStartNodeIds(Current.Services.EntityService); + return user.CalculateMediaStartNodeIds(Current.Services.EntityService, Current.AppCaches); } protected virtual int RecycleBinId => Constants.System.RecycleBinMedia; diff --git a/src/Umbraco.Web/WebApi/UnhandledExceptionLogger.cs b/src/Umbraco.Web/WebApi/UnhandledExceptionLogger.cs index 1dd43f5b2a..983c377e80 100644 --- a/src/Umbraco.Web/WebApi/UnhandledExceptionLogger.cs +++ b/src/Umbraco.Web/WebApi/UnhandledExceptionLogger.cs @@ -29,7 +29,7 @@ namespace Umbraco.Web.WebApi var requestUrl = context.ExceptionContext?.ControllerContext?.Request?.RequestUri?.AbsoluteUri; var controllerType = context.ExceptionContext?.ActionContext?.ControllerContext?.Controller?.GetType(); - _logger.Error(controllerType, context.Exception, "Unhandled controller exception occurred for request '{RequestUrl}'", requestUrl); + _logger.Error(controllerType, context.Exception, "Unhandled controller exception occurred for request '{RequestUrl}'", requestUrl); } }