diff --git a/.github/CODE_OF_CONDUCT.md b/.github/CODE_OF_CONDUCT.md index 1526c54656..e97b03e7e3 100644 --- a/.github/CODE_OF_CONDUCT.md +++ b/.github/CODE_OF_CONDUCT.md @@ -1,32 +1,92 @@ -# Code Of Conduct - -Our informal code of conduct concentrates on the values we, as Umbraco HQ, have set for ourselves and for our community. We expect you to be a friend. -Instead of listing out all the exact "do's" and "don't's" we want to challenge you to think about our values and apply them: +# Umbraco Code of Conduct -If there's a need to talk to Umbraco HQ about anything, please make sure to send a mail to [Sebastiaan Janssen - sj@umbraco.dk](mailto:sj@umbraco.dk). +## Preamble -## Be a Friend +We are the friendly CMS. And our friendliness stems from our values. That's why we have set for ourselves, Umbraco HQ, and the community, five values to guide us in everything we do: -We welcome and thank you for registering at Our Umbraco. Find below the values that govern Umbraco and which you accept by using Our Umbraco. +* Trust - We believe in and empower people +* Respect - We treat others as we would like to be treated +* Open - We share our thoughts and knowledge +* Hungry - We want to do things better, best is next +* Friendly - We want to build long-lasting relationships -## Trust +With these values in mind, we want to offer the Umbraco community a code of conduct that specifies a baseline standard of behavior so that people with different social values and communication styles can work together. -Assume positive intent and try to understand before being understood. +This code of conduct is based on the widely used Contributor Covenant, as described in [https://www.contributor-covenant.org/](https://www.contributor-covenant.org/) -## Respect +## Our Pledge -Treat others as you would like to be treated. +We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation. -This also goes for treating the HQ with respect. For example: don’t promote products on [our.umbraco.com](https://our.umbraco.com) that directly compete with our commercial offerings which enables us to work for a sustainable Umbraco. +We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community. -## Open +## Our Standards +Examples of behavior that contributes to a positive environment for our community include: -Be honest and straightforward. Tell it as it is. Share thoughts and knowledge and engage in collaboration. +* Demonstrating empathy and kindness toward other people +* Being respectful of differing opinions, viewpoints, and experiences +* Giving and gracefully accepting constructive feedback +* Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience +* Focusing on what is best not just for us as individuals, but for the overall community -## Hungry +Examples of unacceptable behavior include: -Don't rest on your laurels and never accept the status quo. Contribute and give back to fellow Umbracians. +* The use of sexualized language or imagery, and sexual attention or advances of any kind +* Trolling, insulting or derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or email address, without their explicit permission +* Other conduct which could reasonably be considered inappropriate in a professional setting -## Friendly +## Enforcement Responsibilities -Don’t judge upon mistakes made but rather upon the speed and quality with which mistakes are corrected. Friendly posts and contributions generate smiles and build long lasting relationships. \ No newline at end of file +Community leaders (e.g. Meetup & festival organizers, moderators, maintainers, ...) are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful. + +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. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior, may be reported at [conduct@umbraco.com](mailto:conduct@umbraco.com). All complaints will be reviewed and investigated promptly and fairly. + +Or alternatively, you can reach out directly to any of the team members behind the address above: + +* Sebastiaan Janssen (He, Him - Languages spoken: English, Dutch, Danish(Read)) [sebastiaan@umbraco.com](mailto:sebastiaan@umbraco.com) +* Ilham Boulghallat (She, Her - Languages spoken: English, French, Arabic) [ilham@umbraco.com](mailto:ilham@umbraco.com) +* Arnold Visser (He, Him - Languages spoken: English, Dutch) [arnold@umbraco.com](mailto:arnold@umbraco.com) + +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: + +**1. Correction** +Community Impact: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community. + +Consequence: A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested. + +**2. Warning** +Community Impact: A violation through a single incident or series of actions. + +Consequence: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban. + +**3. Temporary Ban** +Community Impact: A serious violation of community standards, including sustained inappropriate behavior. + +Consequence: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban. + +**4. Permanent Ban** +Community Impact: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals. + +Consequence: A permanent ban from any sort of public interaction within the community. + +## Attribution +This Code of Conduct is adapted from the Contributor Covenant, version 2.0, available at [https://www.contributor-covenant.org/version/2/0/code_of_conduct.html](https://www.contributor-covenant.org/version/2/0/code_of_conduct.html). + +This Code of Conduct will be maintained and reviewed by the team listed above. \ No newline at end of file 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/src/Umbraco.Core/Cache/CacheKeys.cs b/src/Umbraco.Core/Cache/CacheKeys.cs index 9f082df104..6a17356a68 100644 --- a/src/Umbraco.Core/Cache/CacheKeys.cs +++ b/src/Umbraco.Core/Cache/CacheKeys.cs @@ -14,5 +14,10 @@ public const string MacroFromAliasCacheKey = "macroFromAlias_"; public const string UserGroupGetByAliasCacheKeyPrefix = "UserGroupRepository_GetByAlias_"; + + 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/Cache/MemberCacheRefresher.cs b/src/Umbraco.Core/Cache/MemberCacheRefresher.cs index 9fd2ed8fda..0932725fe4 100644 --- a/src/Umbraco.Core/Cache/MemberCacheRefresher.cs +++ b/src/Umbraco.Core/Cache/MemberCacheRefresher.cs @@ -22,14 +22,16 @@ namespace Umbraco.Cms.Core.Cache public class JsonPayload { //[JsonConstructor] - public JsonPayload(int id, string username) + public JsonPayload(int id, string username, bool removed) { Id = id; Username = username; + Removed = removed; } public int Id { get; } public string Username { get; } + public bool Removed { get; } } #region Define @@ -54,13 +56,13 @@ namespace Umbraco.Cms.Core.Cache public override void Refresh(int id) { - ClearCache(new JsonPayload(id, null)); + ClearCache(new JsonPayload(id, null, false)); base.Refresh(id); } public override void Remove(int id) { - ClearCache(new JsonPayload(id, null)); + ClearCache(new JsonPayload(id, null, false)); base.Remove(id); } diff --git a/src/Umbraco.Core/Cache/UserCacheRefresher.cs b/src/Umbraco.Core/Cache/UserCacheRefresher.cs index 0e8b749e50..6cb3eb7f88 100644 --- a/src/Umbraco.Core/Cache/UserCacheRefresher.cs +++ b/src/Umbraco.Core/Cache/UserCacheRefresher.cs @@ -40,7 +40,14 @@ namespace Umbraco.Cms.Core.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.Core/Composing/TypeFinder.cs b/src/Umbraco.Core/Composing/TypeFinder.cs index 4ec46bbda0..7d0a8fdb09 100644 --- a/src/Umbraco.Core/Composing/TypeFinder.cs +++ b/src/Umbraco.Core/Composing/TypeFinder.cs @@ -22,8 +22,8 @@ namespace Umbraco.Cms.Core.Composing private readonly object _localFilteredAssemblyCacheLocker = new object(); private readonly List _notifiedLoadExceptionAssemblies = new List(); private static readonly ConcurrentDictionary TypeNamesCache = new ConcurrentDictionary(); - private readonly string[] _assembliesAcceptingLoadExceptions; + private readonly ITypeFinderConfig _typeFinderConfig; // used for benchmark tests internal bool QueryWithReferencingAssemblies = true; @@ -32,17 +32,37 @@ namespace Umbraco.Cms.Core.Composing _logger = logger ?? throw new ArgumentNullException(nameof(logger)); _assemblyProvider = assemblyProvider; _runtimeHash = runtimeHash; - _assembliesAcceptingLoadExceptions = typeFinderConfig?.AssembliesAcceptingLoadExceptions.Where(x => !x.IsNullOrWhiteSpace()).ToArray() ?? Array.Empty(); + _typeFinderConfig = typeFinderConfig; + } + + private string[] _assembliesAcceptingLoadExceptions = null; + + private string[] AssembliesAcceptingLoadExceptions + { + get + { + if (_assembliesAcceptingLoadExceptions is not null) + { + return _assembliesAcceptingLoadExceptions; + } + + _assembliesAcceptingLoadExceptions = + _typeFinderConfig?.AssembliesAcceptingLoadExceptions.Where(x => !x.IsNullOrWhiteSpace()).ToArray() ?? + Array.Empty(); + + return _assembliesAcceptingLoadExceptions; + } } + private bool AcceptsLoadExceptions(Assembly a) { - if (_assembliesAcceptingLoadExceptions.Length == 0) + if (AssembliesAcceptingLoadExceptions.Length == 0) return false; - if (_assembliesAcceptingLoadExceptions.Length == 1 && _assembliesAcceptingLoadExceptions[0] == "*") + if (AssembliesAcceptingLoadExceptions.Length == 1 && AssembliesAcceptingLoadExceptions[0] == "*") return true; var name = a.GetName().Name; // simple name of the assembly - return _assembliesAcceptingLoadExceptions.Any(pattern => + return AssembliesAcceptingLoadExceptions.Any(pattern => { if (pattern.Length > name.Length) return false; // pattern longer than name if (pattern.Length == name.Length) return pattern.InvariantEquals(name); // same length, must be identical diff --git a/src/Umbraco.Core/Configuration/GlobalSettingsExtensions.cs b/src/Umbraco.Core/Configuration/GlobalSettingsExtensions.cs index a0fd308490..560835a7e4 100644 --- a/src/Umbraco.Core/Configuration/GlobalSettingsExtensions.cs +++ b/src/Umbraco.Core/Configuration/GlobalSettingsExtensions.cs @@ -1,4 +1,5 @@ using System; +using Umbraco.Cms.Core; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Hosting; @@ -53,7 +54,7 @@ namespace Umbraco.Extensions if (path.StartsWith(hostingEnvironment.ApplicationVirtualPath)) // beware of TrimStart, see U4-2518 path = path.Substring(hostingEnvironment.ApplicationVirtualPath.Length); - return path.TrimStart('~').TrimStart('/').Replace('/', '-').Trim().ToLower(); + return path.TrimStart(Constants.CharArrays.Tilde).TrimStart(Constants.CharArrays.ForwardSlash).Replace('/', '-').Trim().ToLower(); } } } diff --git a/src/Umbraco.Core/Configuration/Models/ContentSettings.cs b/src/Umbraco.Core/Configuration/Models/ContentSettings.cs index 6738956686..42fccd0ca5 100644 --- a/src/Umbraco.Core/Configuration/Models/ContentSettings.cs +++ b/src/Umbraco.Core/Configuration/Models/ContentSettings.cs @@ -180,7 +180,7 @@ namespace Umbraco.Cms.Core.Configuration.Models /// /// Gets or sets a value for the collection of file extensions that are disallowed for upload. /// - public IEnumerable DisallowedUploadFiles { get; set; } = new[] { "ashx", "aspx", "ascx", "config", "cshtml", "vbhtml", "asmx", "air", "axd" }; + public IEnumerable DisallowedUploadFiles { get; set; } = new[] { "ashx", "aspx", "ascx", "config", "cshtml", "vbhtml", "asmx", "air", "axd", "xamlx" }; /// /// Gets or sets a value for the collection of file extensions that are allowed for upload. diff --git a/src/Umbraco.Core/Configuration/Models/GlobalSettings.cs b/src/Umbraco.Core/Configuration/Models/GlobalSettings.cs index 45abc39268..8ac4c0462e 100644 --- a/src/Umbraco.Core/Configuration/Models/GlobalSettings.cs +++ b/src/Umbraco.Core/Configuration/Models/GlobalSettings.cs @@ -1,6 +1,8 @@ // Copyright (c) Umbraco. // See LICENSE for more details. +using System; + namespace Umbraco.Cms.Core.Configuration.Models { /// @@ -25,9 +27,9 @@ namespace Umbraco.Cms.Core.Configuration.Models public string ReservedPaths { get; set; } = StaticReservedPaths; /// - /// Gets or sets a value for the timeout in minutes. + /// Gets or sets a value for the timeout /// - public int TimeOutInMinutes { get; set; } = 20; + public TimeSpan TimeOut{ get; set; } = TimeSpan.FromMinutes(7); /// /// Gets or sets a value for the default UI language. @@ -134,5 +136,14 @@ namespace Umbraco.Cms.Core.Configuration.Models /// Gets a value indicating whether SMTP is configured. /// public bool IsSmtpServerConfigured => !string.IsNullOrWhiteSpace(Smtp?.Host); + + /// + /// An int value representing the time in milliseconds to lock the database for a write operation + /// + /// + /// The default value is 5000 milliseconds + /// + /// The timeout in milliseconds. + public TimeSpan SqlWriteLockTimeOut { get; } = TimeSpan.FromMilliseconds(5000); } } diff --git a/src/Umbraco.Core/Configuration/Models/Validation/GlobalSettingsValidator.cs b/src/Umbraco.Core/Configuration/Models/Validation/GlobalSettingsValidator.cs index b963bddc06..5a7a0ad2f5 100644 --- a/src/Umbraco.Core/Configuration/Models/Validation/GlobalSettingsValidator.cs +++ b/src/Umbraco.Core/Configuration/Models/Validation/GlobalSettingsValidator.cs @@ -1,6 +1,7 @@ // Copyright (c) Umbraco. // See LICENSE for more details. +using System; using Microsoft.Extensions.Options; namespace Umbraco.Cms.Core.Configuration.Models.Validation @@ -19,10 +20,29 @@ namespace Umbraco.Cms.Core.Configuration.Models.Validation return ValidateOptionsResult.Fail(message); } + if (!ValidateSqlWriteLockTimeOutSetting(options.SqlWriteLockTimeOut, out var message2)) + { + return ValidateOptionsResult.Fail(message2); + } + return ValidateOptionsResult.Success; } private bool ValidateSmtpSetting(SmtpSettings value, out string message) => ValidateOptionalEntry($"{Constants.Configuration.ConfigGlobal}:{nameof(GlobalSettings.Smtp)}", value, "A valid From email address is required", out message); + + private bool ValidateSqlWriteLockTimeOutSetting(TimeSpan configuredTimeOut, out string message) { + // Only apply this setting if it's not excessively high or low + const int minimumTimeOut = 100; + const int maximumTimeOut = 20000; + if (configuredTimeOut.TotalMilliseconds < minimumTimeOut || configuredTimeOut.TotalMilliseconds > maximumTimeOut) // between 0.1 and 20 seconds + { + message = $"The `{Constants.Configuration.ConfigGlobal}:{nameof(GlobalSettings.SqlWriteLockTimeOut)}` setting is not between the minimum of {minimumTimeOut} ms and maximum of {maximumTimeOut} ms"; + return false; + } + + message = string.Empty; + return true; + } } } diff --git a/src/Umbraco.Core/Constants-CharArrays.cs b/src/Umbraco.Core/Constants-CharArrays.cs new file mode 100644 index 0000000000..0d1722f7eb --- /dev/null +++ b/src/Umbraco.Core/Constants-CharArrays.cs @@ -0,0 +1,132 @@ +namespace Umbraco.Cms.Core +{ + public static partial class Constants + { + /// + /// Char Arrays to avoid allocations + /// + public static class CharArrays + { + /// + /// Char array containing only / + /// + public static readonly char[] ForwardSlash = new char[] { '/' }; + + /// + /// Char array containing only \ + /// + public static readonly char[] Backslash = new char[] { '\\' }; + + /// + /// Char array containing only ' + /// + public static readonly char[] SingleQuote = new char[] { '\'' }; + + /// + /// Char array containing only " + /// + public static readonly char[] DoubleQuote = new char[] { '\"' }; + + + /// + /// Char array containing ' " + /// + public static readonly char[] DoubleQuoteSingleQuote = new char[] { '\"', '\'' }; + + /// + /// Char array containing only _ + /// + public static readonly char[] Underscore = new char[] { '_' }; + + /// + /// Char array containing \n \r + /// + public static readonly char[] LineFeedCarriageReturn = new char[] { '\n', '\r' }; + + + /// + /// Char array containing \n + /// + public static readonly char[] LineFeed = new char[] { '\n' }; + + /// + /// Char array containing only , + /// + public static readonly char[] Comma = new char[] { ',' }; + + /// + /// Char array containing only & + /// + public static readonly char[] Ampersand = new char[] { '&' }; + + /// + /// Char array containing only \0 + /// + public static readonly char[] NullTerminator = new char[] { '\0' }; + + /// + /// Char array containing only . + /// + public static readonly char[] Period = new char[] { '.' }; + + /// + /// Char array containing only ~ + /// + public static readonly char[] Tilde = new char[] { '~' }; + /// + /// Char array containing ~ / + /// + public static readonly char[] TildeForwardSlash = new char[] { '~', '/' }; + + /// + /// Char array containing only ? + /// + public static readonly char[] QuestionMark = new char[] { '?' }; + + /// + /// Char array containing ? & + /// + public static readonly char[] QuestionMarkAmpersand = new char[] { '?', '&' }; + + /// + /// Char array containing XML 1.1 whitespace chars + /// + public static readonly char[] XmlWhitespaceChars = new char[] { ' ', '\t', '\r', '\n' }; + + /// + /// Char array containing only the Space char + /// + public static readonly char[] Space = new char[] { ' ' }; + + /// + /// Char array containing only ; + /// + public static readonly char[] Semicolon = new char[] { ';' }; + + /// + /// Char array containing a comma and a space + /// + public static readonly char[] CommaSpace = new char[] { ',', ' ' }; + + /// + /// Char array containing _ - + /// + public static readonly char[] UnderscoreDash = new char[] { '_', '-' }; + + /// + /// Char array containing = + /// + public static readonly char[] EqualsChar = new char[] { '=' }; + + /// + /// Char array containing > + /// + public static readonly char[] GreaterThan = new char[] { '>' }; + + /// + /// Char array containing | + /// + public static readonly char[] VerticalTab = new char[] { '|' }; + } + } +} diff --git a/src/Umbraco.Core/Constants-Icons.cs b/src/Umbraco.Core/Constants-Icons.cs index 73051f5e95..7885d89679 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/Editors/UserEditorAuthorizationHelper.cs b/src/Umbraco.Core/Editors/UserEditorAuthorizationHelper.cs index 0ecbdbd4ab..53faa69835 100644 --- a/src/Umbraco.Core/Editors/UserEditorAuthorizationHelper.cs +++ b/src/Umbraco.Core/Editors/UserEditorAuthorizationHelper.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; +using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.Membership; using Umbraco.Cms.Core.Security; @@ -16,12 +17,14 @@ namespace Umbraco.Cms.Core.Editors private readonly IContentService _contentService; private readonly IMediaService _mediaService; private readonly IEntityService _entityService; + private readonly AppCaches _appCaches; - public UserEditorAuthorizationHelper(IContentService contentService, IMediaService mediaService, IEntityService entityService) + public UserEditorAuthorizationHelper(IContentService contentService, IMediaService mediaService, IEntityService entityService, AppCaches appCaches) { _contentService = contentService; _mediaService = mediaService; _entityService = entityService; + _appCaches = appCaches; } /// @@ -115,7 +118,7 @@ namespace Umbraco.Cms.Core.Editors { if (contentId == Constants.System.Root) { - var hasAccess = ContentPermissions.HasPathAccess("-1", currentUser.CalculateContentStartNodeIds(_entityService), Constants.System.RecycleBinContent); + var hasAccess = ContentPermissions.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"); } @@ -123,7 +126,7 @@ namespace Umbraco.Cms.Core.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); } @@ -136,7 +139,7 @@ namespace Umbraco.Cms.Core.Editors { if (mediaId == Constants.System.Root) { - var hasAccess = ContentPermissions.HasPathAccess("-1", currentUser.CalculateMediaStartNodeIds(_entityService), Constants.System.RecycleBinMedia); + var hasAccess = ContentPermissions.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"); } @@ -144,7 +147,7 @@ namespace Umbraco.Cms.Core.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.Core/Extensions/DictionaryExtensions.cs b/src/Umbraco.Core/Extensions/DictionaryExtensions.cs index 12e8de726f..b524961f7e 100644 --- a/src/Umbraco.Core/Extensions/DictionaryExtensions.cs +++ b/src/Umbraco.Core/Extensions/DictionaryExtensions.cs @@ -10,6 +10,7 @@ using System.Linq; using System.Net; using System.Text; using System.Threading.Tasks; +using Umbraco.Cms.Core; namespace Umbraco.Extensions { @@ -257,7 +258,7 @@ namespace Umbraco.Extensions { builder.Append(String.Format("{0}={1}&", WebUtility.UrlEncode(i.Key), i.Value == null ? string.Empty : WebUtility.UrlEncode(i.Value.ToString()))); } - return builder.ToString().TrimEnd('&'); + return builder.ToString().TrimEnd(Constants.CharArrays.Ampersand); } /// The get entry ignore case. diff --git a/src/Umbraco.Core/Extensions/StringExtensions.cs b/src/Umbraco.Core/Extensions/StringExtensions.cs index 70c959d09f..452e409d34 100644 --- a/src/Umbraco.Core/Extensions/StringExtensions.cs +++ b/src/Umbraco.Core/Extensions/StringExtensions.cs @@ -40,7 +40,7 @@ namespace Umbraco.Extensions /// public static int[] GetIdsFromPathReversed(this string path) { - var nodeIds = path.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries) + var nodeIds = path.Split(Constants.CharArrays.Comma, StringSplitOptions.RemoveEmptyEntries) .Select(x => x.TryConvertTo()) .Where(x => x.Success) .Select(x => x.Result) @@ -190,7 +190,7 @@ namespace Umbraco.Extensions //remove any prefixed '&' or '?' for (var i = 0; i < queryStrings.Length; i++) { - queryStrings[i] = queryStrings[i].TrimStart('?', '&').TrimEnd('&'); + queryStrings[i] = queryStrings[i].TrimStart(Constants.CharArrays.QuestionMarkAmpersand).TrimEnd(Constants.CharArrays.Ampersand); } var nonEmpty = queryStrings.Where(x => !x.IsNullOrWhiteSpace()).ToArray(); @@ -1100,7 +1100,7 @@ namespace Umbraco.Extensions { return false; } - var idCheckList = csv.Split(new[] { "," }, StringSplitOptions.RemoveEmptyEntries); + var idCheckList = csv.Split(Constants.CharArrays.Comma, StringSplitOptions.RemoveEmptyEntries); return idCheckList.Contains(value); } @@ -1115,7 +1115,7 @@ namespace Umbraco.Extensions fileName = fileName.StripFileExtension(); // underscores and dashes to spaces - fileName = fileName.ReplaceMany(new[] { '_', '-' }, ' '); + fileName = fileName.ReplaceMany(Constants.CharArrays.UnderscoreDash, ' '); // any other conversions ? @@ -1123,7 +1123,7 @@ namespace Umbraco.Extensions fileName = CultureInfo.InvariantCulture.TextInfo.ToTitleCase(fileName); // Replace multiple consecutive spaces with a single space - fileName = string.Join(" ", fileName.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries)); + fileName = string.Join(" ", fileName.Split(Constants.CharArrays.Space, StringSplitOptions.RemoveEmptyEntries)); return fileName; } diff --git a/src/Umbraco.Core/Extensions/UdiGetterExtensions.cs b/src/Umbraco.Core/Extensions/UdiGetterExtensions.cs index b164effdd6..70dd11ff33 100644 --- a/src/Umbraco.Core/Extensions/UdiGetterExtensions.cs +++ b/src/Umbraco.Core/Extensions/UdiGetterExtensions.cs @@ -158,7 +158,7 @@ namespace Umbraco.Extensions public static StringUdi GetUdi(this Stylesheet entity) { if (entity == null) throw new ArgumentNullException("entity"); - return new StringUdi(Constants.UdiEntityType.Stylesheet, entity.Path.TrimStart('/')).EnsureClosed(); + return new StringUdi(Constants.UdiEntityType.Stylesheet, entity.Path.TrimStart(Constants.CharArrays.ForwardSlash)).EnsureClosed(); } /// @@ -169,7 +169,7 @@ namespace Umbraco.Extensions public static StringUdi GetUdi(this Script entity) { if (entity == null) throw new ArgumentNullException("entity"); - return new StringUdi(Constants.UdiEntityType.Script, entity.Path.TrimStart('/')).EnsureClosed(); + return new StringUdi(Constants.UdiEntityType.Script, entity.Path.TrimStart(Constants.CharArrays.ForwardSlash)).EnsureClosed(); } /// @@ -208,7 +208,7 @@ namespace Umbraco.Extensions ? Constants.UdiEntityType.PartialViewMacro : Constants.UdiEntityType.PartialView; - return new StringUdi(entityType, entity.Path.TrimStart('/')).EnsureClosed(); + return new StringUdi(entityType, entity.Path.TrimStart(Constants.CharArrays.ForwardSlash)).EnsureClosed(); } /// diff --git a/src/Umbraco.Core/Extensions/UriExtensions.cs b/src/Umbraco.Core/Extensions/UriExtensions.cs index 5527fc890e..858069edcf 100644 --- a/src/Umbraco.Core/Extensions/UriExtensions.cs +++ b/src/Umbraco.Core/Extensions/UriExtensions.cs @@ -2,6 +2,7 @@ // See LICENSE for more details. using System; +using Umbraco.Cms.Core; namespace Umbraco.Extensions { @@ -129,12 +130,12 @@ namespace Umbraco.Extensions if (uri.IsAbsoluteUri) { if (path != "/") - uri = new Uri(uri.GetLeftPart(UriPartial.Authority) + path.TrimEnd('/') + uri.Query); + uri = new Uri(uri.GetLeftPart(UriPartial.Authority) + path.TrimEnd(Constants.CharArrays.ForwardSlash) + uri.Query); } else { if (path != "/") - uri = new Uri(path.TrimEnd('/') + uri.Query, UriKind.Relative); + uri = new Uri(path.TrimEnd(Constants.CharArrays.ForwardSlash) + uri.Query, UriKind.Relative); } return uri; } diff --git a/src/Umbraco.Core/GuidUdi.cs b/src/Umbraco.Core/GuidUdi.cs index 904d6140f2..74f6e2dbc8 100644 --- a/src/Umbraco.Core/GuidUdi.cs +++ b/src/Umbraco.Core/GuidUdi.cs @@ -33,7 +33,7 @@ namespace Umbraco.Cms.Core : base(uriValue) { Guid guid; - if (Guid.TryParse(uriValue.AbsolutePath.TrimStart('/'), out guid) == false) + if (Guid.TryParse(uriValue.AbsolutePath.TrimStart(Constants.CharArrays.ForwardSlash), out guid) == false) throw new FormatException("URI \"" + uriValue + "\" is not a GUID entity ID."); Guid = guid; diff --git a/src/Umbraco.Core/IO/IOHelper.cs b/src/Umbraco.Core/IO/IOHelper.cs index e799bbdbe8..d0f190868b 100644 --- a/src/Umbraco.Core/IO/IOHelper.cs +++ b/src/Umbraco.Core/IO/IOHelper.cs @@ -31,7 +31,7 @@ namespace Umbraco.Cms.Core.IO retval = virtualPath.Replace("~", _hostingEnvironment.ApplicationVirtualPath); if (virtualPath.StartsWith("/") && !PathStartsWith(virtualPath, _hostingEnvironment.ApplicationVirtualPath)) - retval = _hostingEnvironment.ApplicationVirtualPath + "/" + virtualPath.TrimStart('/'); + retval = _hostingEnvironment.ApplicationVirtualPath + "/" + virtualPath.TrimStart(Constants.CharArrays.ForwardSlash); return retval; } @@ -58,14 +58,14 @@ namespace Umbraco.Cms.Core.IO { var result = (!string.IsNullOrEmpty(path) && (path.StartsWith("~") || PathStartsWith(path, _hostingEnvironment.ApplicationVirtualPath))) ? _hostingEnvironment.MapPathWebRoot(path) - : _hostingEnvironment.MapPathWebRoot("~/" + path.TrimStart('/')); + : _hostingEnvironment.MapPathWebRoot("~/" + path.TrimStart(Constants.CharArrays.ForwardSlash)); if (result != null) return result; } var dirSepChar = Path.DirectorySeparatorChar; var root = Assembly.GetExecutingAssembly().GetRootDirectorySafe(); - var newPath = path.TrimStart('~', '/').Replace('/', dirSepChar); + var newPath = path.TrimStart(Constants.CharArrays.TildeForwardSlash).Replace('/', dirSepChar); var retval = root + dirSepChar.ToString(CultureInfo.InvariantCulture) + newPath; return retval; @@ -141,7 +141,7 @@ namespace Umbraco.Cms.Core.IO public bool VerifyFileExtension(string filePath, IEnumerable validFileExtensions) { var ext = Path.GetExtension(filePath); - return ext != null && validFileExtensions.Contains(ext.TrimStart('.')); + return ext != null && validFileExtensions.Contains(ext.TrimStart(Constants.CharArrays.Period)); } public abstract bool PathStartsWith(string path, string root, params char[] separators); diff --git a/src/Umbraco.Core/IO/PhysicalFileSystem.cs b/src/Umbraco.Core/IO/PhysicalFileSystem.cs index 898a7f0ce4..e517f0be63 100644 --- a/src/Umbraco.Core/IO/PhysicalFileSystem.cs +++ b/src/Umbraco.Core/IO/PhysicalFileSystem.cs @@ -52,7 +52,7 @@ namespace Umbraco.Cms.Core.IO _rootPath = EnsureDirectorySeparatorChar(rootPath).TrimEnd(Path.DirectorySeparatorChar); _rootPathFwd = EnsureUrlSeparatorChar(_rootPath); - _rootUrl = EnsureUrlSeparatorChar(rootUrl).TrimEnd('/'); + _rootUrl = EnsureUrlSeparatorChar(rootUrl).TrimEnd(Constants.CharArrays.ForwardSlash); } /// @@ -257,12 +257,12 @@ namespace Umbraco.Cms.Core.IO // if it starts with the root URL, strip it and trim the starting slash to make it relative // eg "/Media/1234/img.jpg" => "1234/img.jpg" if (_ioHelper.PathStartsWith(path, _rootUrl, '/')) - return path.Substring(_rootUrl.Length).TrimStart('/'); + return path.Substring(_rootUrl.Length).TrimStart(Constants.CharArrays.ForwardSlash); // if it starts with the root path, strip it and trim the starting slash to make it relative // eg "c:/websites/test/root/Media/1234/img.jpg" => "1234/img.jpg" if (_ioHelper.PathStartsWith(path, _rootPathFwd, '/')) - return path.Substring(_rootPathFwd.Length).TrimStart('/'); + return path.Substring(_rootPathFwd.Length).TrimStart(Constants.CharArrays.ForwardSlash); // unchanged - what else? return path; @@ -324,7 +324,7 @@ namespace Umbraco.Cms.Core.IO /// All separators are forward-slashes. public string GetUrl(string path) { - path = EnsureUrlSeparatorChar(path).Trim('/'); + path = EnsureUrlSeparatorChar(path).Trim(Constants.CharArrays.ForwardSlash); return _rootUrl + "/" + path; } diff --git a/src/Umbraco.Core/IO/ShadowFileSystem.cs b/src/Umbraco.Core/IO/ShadowFileSystem.cs index 97f2cac668..cc4e792d98 100644 --- a/src/Umbraco.Core/IO/ShadowFileSystem.cs +++ b/src/Umbraco.Core/IO/ShadowFileSystem.cs @@ -182,7 +182,7 @@ namespace Umbraco.Cms.Core.IO if (Nodes.TryGetValue(normPath, out sf) && sf.IsExist && (sf.IsDir || overrideIfExists == false)) throw new InvalidOperationException(string.Format("A file at path '{0}' already exists", path)); - var parts = normPath.Split('/'); + var parts = normPath.Split(Constants.CharArrays.ForwardSlash); for (var i = 0; i < parts.Length - 1; i++) { var dirPath = string.Join("/", parts.Take(i + 1)); @@ -297,7 +297,7 @@ namespace Umbraco.Cms.Core.IO if (Nodes.TryGetValue(normPath, out sf) && sf.IsExist && (sf.IsDir || overrideIfExists == false)) throw new InvalidOperationException(string.Format("A file at path '{0}' already exists", path)); - var parts = normPath.Split('/'); + var parts = normPath.Split(Constants.CharArrays.ForwardSlash); for (var i = 0; i < parts.Length - 1; i++) { var dirPath = string.Join("/", parts.Take(i + 1)); diff --git a/src/Umbraco.Core/Media/Exif/ExifPropertyFactory.cs b/src/Umbraco.Core/Media/Exif/ExifPropertyFactory.cs index 8d1b1af490..f47cab1c35 100644 --- a/src/Umbraco.Core/Media/Exif/ExifPropertyFactory.cs +++ b/src/Umbraco.Core/Media/Exif/ExifPropertyFactory.cs @@ -44,7 +44,7 @@ namespace Umbraco.Cms.Core.Media.Exif return new ExifDateTime(ExifTag.DateTime, ExifBitConverter.ToDateTime(value)); else if (tag == 0x9c9b || tag == 0x9c9c || // Windows tags tag == 0x9c9d || tag == 0x9c9e || tag == 0x9c9f) - return new WindowsByteString(etag, Encoding.Unicode.GetString(value).TrimEnd('\0')); + return new WindowsByteString(etag, Encoding.Unicode.GetString(value).TrimEnd(Constants.CharArrays.NullTerminator)); } else if (ifd == IFD.EXIF) { @@ -75,7 +75,7 @@ namespace Umbraco.Cms.Core.Media.Exif hasenc = false; } - string val = (hasenc ? enc.GetString(value, 8, value.Length - 8) : enc.GetString(value)).Trim('\0'); + string val = (hasenc ? enc.GetString(value, 8, value.Length - 8) : enc.GetString(value)).Trim(Constants.CharArrays.NullTerminator); return new ExifEncodedString(ExifTag.UserComment, val, enc); } diff --git a/src/Umbraco.Core/Media/Exif/MathEx.cs b/src/Umbraco.Core/Media/Exif/MathEx.cs index 8cac15f5b4..dfad9ae7de 100644 --- a/src/Umbraco.Core/Media/Exif/MathEx.cs +++ b/src/Umbraco.Core/Media/Exif/MathEx.cs @@ -694,7 +694,7 @@ namespace Umbraco.Cms.Core.Media.Exif if (s == null) throw new ArgumentNullException("s"); - string[] sa = s.Split('/'); + string[] sa = s.Split(Constants.CharArrays.ForwardSlash); int numerator = 1; int denominator = 1; @@ -1322,7 +1322,7 @@ namespace Umbraco.Cms.Core.Media.Exif if (s == null) throw new ArgumentNullException("s"); - string[] sa = s.Split('/'); + string[] sa = s.Split(Constants.CharArrays.ForwardSlash); uint numerator = 1; uint denominator = 1; diff --git a/src/Umbraco.Core/Media/UploadAutoFillProperties.cs b/src/Umbraco.Core/Media/UploadAutoFillProperties.cs index 105b0ce074..dc5529d25f 100644 --- a/src/Umbraco.Core/Media/UploadAutoFillProperties.cs +++ b/src/Umbraco.Core/Media/UploadAutoFillProperties.cs @@ -71,7 +71,7 @@ namespace Umbraco.Cms.Core.Media { using (var filestream = _mediaFileSystem.OpenFile(filepath)) { - var extension = (Path.GetExtension(filepath) ?? "").TrimStart('.'); + var extension = (Path.GetExtension(filepath) ?? "").TrimStart(Constants.CharArrays.Period); var size = _imageUrlGenerator.IsSupportedImageFormat(extension) ? (ImageSize?)_imageDimensionExtractor.GetDimensions(filestream) : null; SetProperties(content, autoFillConfig, size, filestream.Length, extension, culture, segment); } @@ -105,7 +105,7 @@ namespace Umbraco.Cms.Core.Media } else { - var extension = (Path.GetExtension(filepath) ?? "").TrimStart('.'); + var extension = (Path.GetExtension(filepath) ?? "").TrimStart(Constants.CharArrays.Period); var size = _imageUrlGenerator.IsSupportedImageFormat(extension) ? (ImageSize?)_imageDimensionExtractor.GetDimensions(filestream) : null; SetProperties(content, autoFillConfig, size, filestream.Length, extension, culture, segment); } diff --git a/src/Umbraco.Core/Models/Mapping/ContentTypeMapDefinition.cs b/src/Umbraco.Core/Models/Mapping/ContentTypeMapDefinition.cs index 162032216d..3716e9c6a9 100644 --- a/src/Umbraco.Core/Models/Mapping/ContentTypeMapDefinition.cs +++ b/src/Umbraco.Core/Models/Mapping/ContentTypeMapDefinition.cs @@ -592,7 +592,7 @@ namespace Umbraco.Cms.Core.Models.Mapping return Enumerable.Empty(); var aliases = new List(); - var ancestorIds = parent.Path.Split(',').Select(int.Parse); + var ancestorIds = parent.Path.Split(Constants.CharArrays.Comma).Select(int.Parse); // loop through all content types and return ordered aliases of ancestors var allContentTypes = _contentTypeService.GetAll().ToArray(); foreach (var ancestorId in ancestorIds) diff --git a/src/Umbraco.Core/Models/Mapping/UserMapDefinition.cs b/src/Umbraco.Core/Models/Mapping/UserMapDefinition.cs index 3631629c7b..767ed8d58a 100644 --- a/src/Umbraco.Core/Models/Mapping/UserMapDefinition.cs +++ b/src/Umbraco.Core/Models/Mapping/UserMapDefinition.cs @@ -282,8 +282,8 @@ namespace Umbraco.Cms.Core.Models.Mapping { target.AvailableCultures = _textService.GetSupportedCultures().ToDictionary(x => x.Name, x => x.DisplayName); target.Avatars = source.GetUserAvatarUrls(_appCaches.RuntimeCache, _mediaFileSystem, _imageUrlGenerator); - 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; @@ -336,8 +336,8 @@ namespace Umbraco.Cms.Core.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.Core/Models/Membership/User.cs b/src/Umbraco.Core/Models/Membership/User.cs index 3a9dae19d2..7806a7dd52 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.Cms.Core.Configuration.Models; @@ -382,11 +383,10 @@ namespace Umbraco.Cms.Core.Models.Membership } } - /// - /// 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 diff --git a/src/Umbraco.Core/Models/UserExtensions.cs b/src/Umbraco.Core/Models/UserExtensions.cs index e944107f3f..6fb74c3b86 100644 --- a/src/Umbraco.Core/Models/UserExtensions.cs +++ b/src/Umbraco.Core/Models/UserExtensions.cs @@ -90,48 +90,48 @@ namespace Umbraco.Cms.Core.Models - public static bool HasContentRootAccess(this IUser user, IEntityService entityService) + internal static bool HasContentRootAccess(this IUser user, IEntityService entityService, AppCaches appCaches) { - return ContentPermissions.HasPathAccess(Constants.System.RootString, user.CalculateContentStartNodeIds(entityService), Constants.System.RecycleBinContent); + return ContentPermissions.HasPathAccess(Constants.System.RootString, user.CalculateContentStartNodeIds(entityService, appCaches), Constants.System.RecycleBinContent); } - public static bool HasContentBinAccess(this IUser user, IEntityService entityService) + internal static bool HasContentBinAccess(this IUser user, IEntityService entityService, AppCaches appCaches) { - return ContentPermissions.HasPathAccess(Constants.System.RecycleBinContentString, user.CalculateContentStartNodeIds(entityService), Constants.System.RecycleBinContent); + return ContentPermissions.HasPathAccess(Constants.System.RecycleBinContentString, user.CalculateContentStartNodeIds(entityService, appCaches), Constants.System.RecycleBinContent); } - public static bool HasMediaRootAccess(this IUser user, IEntityService entityService) + internal static bool HasMediaRootAccess(this IUser user, IEntityService entityService, AppCaches appCaches) { - return ContentPermissions.HasPathAccess(Constants.System.RootString, user.CalculateMediaStartNodeIds(entityService), Constants.System.RecycleBinMedia); + return ContentPermissions.HasPathAccess(Constants.System.RootString, user.CalculateMediaStartNodeIds(entityService, appCaches), Constants.System.RecycleBinMedia); } - public static bool HasMediaBinAccess(this IUser user, IEntityService entityService) + internal static bool HasMediaBinAccess(this IUser user, IEntityService entityService, AppCaches appCaches) { - return ContentPermissions.HasPathAccess(Constants.System.RecycleBinMediaString, user.CalculateMediaStartNodeIds(entityService), Constants.System.RecycleBinMedia); + return ContentPermissions.HasPathAccess(Constants.System.RecycleBinMediaString, user.CalculateMediaStartNodeIds(entityService, appCaches), Constants.System.RecycleBinMedia); } - public static bool HasPathAccess(this IUser user, IContent content, IEntityService entityService) + public static bool HasPathAccess(this IUser user, IContent content, IEntityService entityService, AppCaches appCaches) { if (content == null) throw new ArgumentNullException(nameof(content)); - return ContentPermissions.HasPathAccess(content.Path, user.CalculateContentStartNodeIds(entityService), Constants.System.RecycleBinContent); + return ContentPermissions.HasPathAccess(content.Path, user.CalculateContentStartNodeIds(entityService, appCaches), Constants.System.RecycleBinContent); } - public static bool HasPathAccess(this IUser user, IMedia media, IEntityService entityService) + public static bool HasPathAccess(this IUser user, IMedia media, IEntityService entityService, AppCaches appCaches) { if (media == null) throw new ArgumentNullException(nameof(media)); - return ContentPermissions.HasPathAccess(media.Path, user.CalculateMediaStartNodeIds(entityService), Constants.System.RecycleBinMedia); + return ContentPermissions.HasPathAccess(media.Path, user.CalculateMediaStartNodeIds(entityService, appCaches), Constants.System.RecycleBinMedia); } - public static bool HasContentPathAccess(this IUser user, IUmbracoEntity entity, IEntityService entityService) + public static bool HasContentPathAccess(this IUser user, IUmbracoEntity entity, IEntityService entityService, AppCaches appCaches) { if (entity == null) throw new ArgumentNullException(nameof(entity)); - return ContentPermissions.HasPathAccess(entity.Path, user.CalculateContentStartNodeIds(entityService), Constants.System.RecycleBinContent); + return ContentPermissions.HasPathAccess(entity.Path, user.CalculateContentStartNodeIds(entityService, appCaches), Constants.System.RecycleBinContent); } - public static bool HasMediaPathAccess(this IUser user, IUmbracoEntity entity, IEntityService entityService) + public static bool HasMediaPathAccess(this IUser user, IUmbracoEntity entity, IEntityService entityService, AppCaches appCaches) { if (entity == null) throw new ArgumentNullException(nameof(entity)); - return ContentPermissions.HasPathAccess(entity.Path, user.CalculateMediaStartNodeIds(entityService), Constants.System.RecycleBinMedia); + return ContentPermissions.HasPathAccess(entity.Path, user.CalculateMediaStartNodeIds(entityService, appCaches), Constants.System.RecycleBinMedia); } /// @@ -144,60 +144,72 @@ namespace Umbraco.Cms.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 - public static int[] CalculateContentStartNodeIds(this IUser user, IEntityService entityService) + /// + /// 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) { - const string cacheKey = "AllContentStartNodes"; - //try to look them up from cache so we don't recalculate - var valuesInUserCache = user.FromUserCache(cacheKey); - if (valuesInUserCache != null) return valuesInUserCache; + 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); - 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); - user.ToUserCache(cacheKey, vals); - return vals; + return result; } - // calc. start nodes, combining groups' and user's, and excluding what's in the bin - public static int[] CalculateMediaStartNodeIds(this IUser user, IEntityService entityService) + /// + /// 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) { - const string cacheKey = "AllMediaStartNodes"; - //try to look them up from cache so we don't recalculate - var valuesInUserCache = user.FromUserCache(cacheKey); - if (valuesInUserCache != null) return valuesInUserCache; + 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); - 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); - user.ToUserCache(cacheKey, vals); - return vals; + return result; } - public static string[] GetMediaStartNodePaths(this IUser user, IEntityService entityService) + public static string[] GetMediaStartNodePaths(this IUser user, IEntityService entityService, AppCaches appCaches) { - const string cacheKey = "MediaStartNodePaths"; - //try to look them up from cache so we don't recalculate - var valuesInUserCache = user.FromUserCache(cacheKey); - if (valuesInUserCache != null) return valuesInUserCache; + 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); - var startNodeIds = user.CalculateMediaStartNodeIds(entityService); - var vals = entityService.GetAllPaths(UmbracoObjectTypes.Media, startNodeIds).Select(x => x.Path).ToArray(); - user.ToUserCache(cacheKey, vals); - return vals; + return result; } - public static string[] GetContentStartNodePaths(this IUser user, IEntityService entityService) + 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 = user.FromUserCache(cacheKey); - if (valuesInUserCache != null) return valuesInUserCache; + var cacheKey = CacheKeys.UserContentStartNodePathsPrefix + user.Id; + var runtimeCache = appCaches.IsolatedCaches.GetOrCreate(); + var result = runtimeCache.GetCacheItem(cacheKey, () => + { + var startNodeIds = user.CalculateContentStartNodeIds(entityService, appCaches); + var vals = entityService.GetAllPaths(UmbracoObjectTypes.Document, startNodeIds).Select(x => x.Path).ToArray(); + return vals; + }, TimeSpan.FromMinutes(2), true); - var startNodeIds = user.CalculateContentStartNodeIds(entityService); - var vals = entityService.GetAllPaths(UmbracoObjectTypes.Document, startNodeIds).Select(x => x.Path).ToArray(); - user.ToUserCache(cacheKey, vals); - return vals; + return result; } private static bool StartsWithPath(string test, string path) diff --git a/src/Umbraco.Core/Packaging/PackageDefinitionXmlParser.cs b/src/Umbraco.Core/Packaging/PackageDefinitionXmlParser.cs index 14e6790f3c..dc62bc84f6 100644 --- a/src/Umbraco.Core/Packaging/PackageDefinitionXmlParser.cs +++ b/src/Umbraco.Core/Packaging/PackageDefinitionXmlParser.cs @@ -48,14 +48,14 @@ namespace Umbraco.Cms.Core.Packaging ContentLoadChildNodes = xml.Element("content")?.AttributeValue("loadChildNodes") ?? false, MediaUdis = xml.Element("media")?.Elements("nodeUdi").Select(x => (GuidUdi)UdiParser.Parse(x.Value)).ToList() ?? new List(), MediaLoadChildNodes = xml.Element("media")?.AttributeValue("loadChildNodes") ?? false, - Macros = xml.Element("macros")?.Value.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries).ToList() ?? new List(), - Templates = xml.Element("templates")?.Value.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries).ToList() ?? new List(), - Stylesheets = xml.Element("stylesheets")?.Value.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries).ToList() ?? new List(), - DocumentTypes = xml.Element("documentTypes")?.Value.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries).ToList() ?? new List(), + Macros = xml.Element("macros")?.Value.Split(Constants.CharArrays.Comma, StringSplitOptions.RemoveEmptyEntries).ToList() ?? new List(), + Templates = xml.Element("templates")?.Value.Split(Constants.CharArrays.Comma, StringSplitOptions.RemoveEmptyEntries).ToList() ?? new List(), + Stylesheets = xml.Element("stylesheets")?.Value.Split(Constants.CharArrays.Comma, StringSplitOptions.RemoveEmptyEntries).ToList() ?? new List(), + DocumentTypes = xml.Element("documentTypes")?.Value.Split(Constants.CharArrays.Comma, StringSplitOptions.RemoveEmptyEntries).ToList() ?? new List(), MediaTypes = xml.Element("mediaTypes")?.Value.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries).ToList() ?? new List(), - Languages = xml.Element("languages")?.Value.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries).ToList() ?? new List(), - DictionaryItems = xml.Element("dictionaryitems")?.Value.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries).ToList() ?? new List(), - DataTypes = xml.Element("datatypes")?.Value.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries).ToList() ?? new List(), + Languages = xml.Element("languages")?.Value.Split(Constants.CharArrays.Comma, StringSplitOptions.RemoveEmptyEntries).ToList() ?? new List(), + DictionaryItems = xml.Element("dictionaryitems")?.Value.Split(Constants.CharArrays.Comma, StringSplitOptions.RemoveEmptyEntries).ToList() ?? new List(), + DataTypes = xml.Element("datatypes")?.Value.Split(Constants.CharArrays.Comma, StringSplitOptions.RemoveEmptyEntries).ToList() ?? new List(), Files = xml.Element("files")?.Elements("file").Select(x => x.Value).ToList() ?? new List() }; diff --git a/src/Umbraco.Core/PropertyEditors/ListViewConfiguration.cs b/src/Umbraco.Core/PropertyEditors/ListViewConfiguration.cs index 9b720e4fd8..96af838710 100644 --- a/src/Umbraco.Core/PropertyEditors/ListViewConfiguration.cs +++ b/src/Umbraco.Core/PropertyEditors/ListViewConfiguration.cs @@ -68,6 +68,9 @@ namespace Umbraco.Cms.Core.PropertyEditors [ConfigurationField("showContentFirst", "Show Content App First", "boolean", Description = "Enable this to show the content app by default instead of the list view app")] public bool ShowContentFirst { get; set; } + [ConfigurationField("useInfiniteEditor", "Edit in Infinite Editor", "boolean", Description = "Enable this to use infinite editing to edit the content of the list view")] + public bool UseInfiniteEditor { get; set; } + [DataContract] public class Property { diff --git a/src/Umbraco.Core/PropertyEditors/ValueConverters/MediaPickerValueConverter.cs b/src/Umbraco.Core/PropertyEditors/ValueConverters/MediaPickerValueConverter.cs index ff5fed786c..f6523da44f 100644 --- a/src/Umbraco.Core/PropertyEditors/ValueConverters/MediaPickerValueConverter.cs +++ b/src/Umbraco.Core/PropertyEditors/ValueConverters/MediaPickerValueConverter.cs @@ -55,7 +55,7 @@ namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters if (source == null) return null; var nodeIds = source.ToString() - .Split(new[] { "," }, StringSplitOptions.RemoveEmptyEntries) + .Split(Constants.CharArrays.Comma, StringSplitOptions.RemoveEmptyEntries) .Select(UdiParser.Parse) .ToArray(); return nodeIds; diff --git a/src/Umbraco.Core/PropertyEditors/ValueConverters/MultiNodeTreePickerValueConverter.cs b/src/Umbraco.Core/PropertyEditors/ValueConverters/MultiNodeTreePickerValueConverter.cs index 4a00f20737..bcaa89b97e 100644 --- a/src/Umbraco.Core/PropertyEditors/ValueConverters/MultiNodeTreePickerValueConverter.cs +++ b/src/Umbraco.Core/PropertyEditors/ValueConverters/MultiNodeTreePickerValueConverter.cs @@ -52,7 +52,7 @@ namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters if (propertyType.EditorAlias.Equals(Constants.PropertyEditors.Aliases.MultiNodeTreePicker)) { var nodeIds = source.ToString() - .Split(new[] { "," }, StringSplitOptions.RemoveEmptyEntries) + .Split(Constants.CharArrays.Comma, StringSplitOptions.RemoveEmptyEntries) .Select(UdiParser.Parse) .ToArray(); return nodeIds; diff --git a/src/Umbraco.Core/PropertyEditors/ValueConverters/SliderValueConverter.cs b/src/Umbraco.Core/PropertyEditors/ValueConverters/SliderValueConverter.cs index a1f3f82f43..67671ee662 100644 --- a/src/Umbraco.Core/PropertyEditors/ValueConverters/SliderValueConverter.cs +++ b/src/Umbraco.Core/PropertyEditors/ValueConverters/SliderValueConverter.cs @@ -33,7 +33,7 @@ namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters if (IsRangeDataType(propertyType.DataType.Id)) { - var rangeRawValues = source.ToString().Split(','); + var rangeRawValues = source.ToString().Split(Constants.CharArrays.Comma); var minimumAttempt = rangeRawValues[0].TryConvertTo(); var maximumAttempt = rangeRawValues[1].TryConvertTo(); diff --git a/src/Umbraco.Core/Routing/AliasUrlProvider.cs b/src/Umbraco.Core/Routing/AliasUrlProvider.cs index 0eb7eea0a2..8b4c633158 100644 --- a/src/Umbraco.Core/Routing/AliasUrlProvider.cs +++ b/src/Umbraco.Core/Routing/AliasUrlProvider.cs @@ -90,7 +90,7 @@ namespace Umbraco.Cms.Core.Routing yield break; var umbracoUrlName = node.Value(_publishedValueFallback, Constants.Conventions.Content.UrlAlias); - var aliases = umbracoUrlName?.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries); + var aliases = umbracoUrlName?.Split(Constants.CharArrays.Comma, StringSplitOptions.RemoveEmptyEntries); if (aliases == null || aliases.Any() == false) yield break; @@ -117,7 +117,7 @@ namespace Umbraco.Cms.Core.Routing ? node.Value(_publishedValueFallback,Constants.Conventions.Content.UrlAlias, culture: domainUri.Culture) : node.Value(_publishedValueFallback, Constants.Conventions.Content.UrlAlias); - var aliases = umbracoUrlName?.Split(new [] {','}, StringSplitOptions.RemoveEmptyEntries); + var aliases = umbracoUrlName?.Split(Constants.CharArrays.Comma, StringSplitOptions.RemoveEmptyEntries); if (aliases == null || aliases.Any() == false) continue; @@ -138,8 +138,8 @@ namespace Umbraco.Cms.Core.Routing string CombinePaths(string path1, string path2) { - string path = path1.TrimEnd('/') + path2; - return path == "/" ? path : path.TrimEnd('/'); + string path = path1.TrimEnd(Constants.CharArrays.ForwardSlash) + path2; + return path == "/" ? path : path.TrimEnd(Constants.CharArrays.ForwardSlash); } #endregion diff --git a/src/Umbraco.Core/Routing/DefaultUrlProvider.cs b/src/Umbraco.Core/Routing/DefaultUrlProvider.cs index 97c7deec9c..2b1693e03f 100644 --- a/src/Umbraco.Core/Routing/DefaultUrlProvider.cs +++ b/src/Umbraco.Core/Routing/DefaultUrlProvider.cs @@ -180,8 +180,8 @@ namespace Umbraco.Cms.Core.Routing string CombinePaths(string path1, string path2) { - string path = path1.TrimEnd('/') + path2; - return path == "/" ? path : path.TrimEnd('/'); + string path = path1.TrimEnd(Constants.CharArrays.ForwardSlash) + path2; + return path == "/" ? path : path.TrimEnd(Constants.CharArrays.ForwardSlash); } #endregion diff --git a/src/Umbraco.Core/Routing/DomainUtilities.cs b/src/Umbraco.Core/Routing/DomainUtilities.cs index a6cd90a3c2..76266f9704 100644 --- a/src/Umbraco.Core/Routing/DomainUtilities.cs +++ b/src/Umbraco.Core/Routing/DomainUtilities.cs @@ -328,7 +328,7 @@ namespace Umbraco.Cms.Core.Routing { var stopNodeId = rootNodeId ?? -1; - return path.Split(',') + return path.Split(Constants.CharArrays.Comma) .Reverse() .Select(int.Parse) .TakeWhile(id => id != stopNodeId) @@ -349,7 +349,7 @@ namespace Umbraco.Cms.Core.Routing { var stopNodeId = rootNodeId ?? -1; - return path.Split(',') + return path.Split(Constants.CharArrays.Comma) .Reverse() .Select(int.Parse) .TakeWhile(id => id != stopNodeId) diff --git a/src/Umbraco.Core/Routing/PublishedRouter.cs b/src/Umbraco.Core/Routing/PublishedRouter.cs index c138232ef5..f41580894f 100644 --- a/src/Umbraco.Core/Routing/PublishedRouter.cs +++ b/src/Umbraco.Core/Routing/PublishedRouter.cs @@ -413,6 +413,16 @@ namespace Umbraco.Cms.Core.Routing _logger.LogDebug("Finder {ContentFinderType}", finder.GetType().FullName); return finder.TryFindContent(request); }); + + _profilingLogger.DebugDuration( + //TODO make structured logging working in profillingLogger + string.Format("Found? {Found} Content: {PublishedContentId}, Template: {TemplateAlias}, Domain: {Domain}, Culture: {Culture}, StatusCode: {StatusCode}", + found, + request.HasPublishedContent() ? request.PublishedContent.Id : "NULL", + request.HasTemplate() ? request.Template?.Alias : "NULL", + request.HasDomain() ? request.Domain.ToString() : "NULL", + request.Culture ?? "NULL", + request.ResponseStatusCode)); } } diff --git a/src/Umbraco.Core/Routing/UriUtility.cs b/src/Umbraco.Core/Routing/UriUtility.cs index 4d349021c4..07adfc1587 100644 --- a/src/Umbraco.Core/Routing/UriUtility.cs +++ b/src/Umbraco.Core/Routing/UriUtility.cs @@ -43,7 +43,7 @@ namespace Umbraco.Cms.Core.Routing public string ToAbsolute(string url) { //return ResolveUrl(url); - url = url.TrimStart('~'); + url = url.TrimStart(Constants.CharArrays.Tilde); return _appPathPrefix + url; } @@ -104,7 +104,7 @@ namespace Umbraco.Cms.Core.Routing if (path != "/") { - path = path.TrimEnd('/'); + path = path.TrimEnd(Constants.CharArrays.ForwardSlash); } return uri.Rewrite(path); diff --git a/src/Umbraco.Core/Routing/UrlProviderExtensions.cs b/src/Umbraco.Core/Routing/UrlProviderExtensions.cs index 80f17e3c12..6dfdc89583 100644 --- a/src/Umbraco.Core/Routing/UrlProviderExtensions.cs +++ b/src/Umbraco.Core/Routing/UrlProviderExtensions.cs @@ -151,7 +151,7 @@ namespace Umbraco.Extensions // got a URL, deal with collisions, add URL default: // detect collisions, etc - Attempt hasCollision = await DetectCollisionAsync(content, url, culture, umbracoContext, publishedRouter, textService, variationContextAccessor, uriUtility); + Attempt hasCollision = await DetectCollisionAsync(logger, content, url, culture, umbracoContext, publishedRouter, textService, variationContextAccessor, uriUtility); if (hasCollision) { result.Add(hasCollision.Result); @@ -187,7 +187,7 @@ namespace Umbraco.Extensions 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 { @@ -196,10 +196,10 @@ namespace Umbraco.Extensions } } - private static async Task> DetectCollisionAsync(IContent content, string url, string culture, IUmbracoContext umbracoContext, IPublishedRouter publishedRouter, ILocalizedTextService textService, IVariationContextAccessor variationContextAccessor, UriUtility uriUtility) + private static async Task> DetectCollisionAsync(ILogger logger, IContent content, string url, string culture, IUmbracoContext umbracoContext, IPublishedRouter publishedRouter, ILocalizedTextService textService, IVariationContextAccessor variationContextAccessor, UriUtility uriUtility) { // test for collisions on the 'main' URL - var uri = new Uri(url.TrimEnd('/'), UriKind.RelativeOrAbsolute); + var uri = new Uri(url.TrimEnd(Constants.CharArrays.ForwardSlash), UriKind.RelativeOrAbsolute); if (uri.IsAbsoluteUri == false) { uri = uri.MakeAbsolute(umbracoContext.CleanedUmbracoUrl); @@ -211,6 +211,16 @@ namespace Umbraco.Extensions if (!pcr.HasPublishedContent()) { + var logMsg = nameof(DetectCollisionAsync) + " did not resolve a content item for original url: {Url}, translated to {TranslatedUrl} and culture: {Culture}"; + if (pcr.IgnorePublishedContentCollisions) + { + logger.LogDebug(logMsg, url, uri, culture); + } + else + { + logger.LogDebug(logMsg, url, uri, culture); + } + var urlInfo = UrlInfo.Message(textService.Localize("content/routeErrorCannotRoute"), culture); return Attempt.Succeed(urlInfo); } diff --git a/src/Umbraco.Core/Security/ContentPermissions.cs b/src/Umbraco.Core/Security/ContentPermissions.cs index d137b3628e..3d3ae55a62 100644 --- a/src/Umbraco.Core/Security/ContentPermissions.cs +++ b/src/Umbraco.Core/Security/ContentPermissions.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Globalization; using System.Linq; +using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.Entities; using Umbraco.Cms.Core.Models.Membership; @@ -18,6 +19,7 @@ namespace Umbraco.Cms.Core.Security private readonly IUserService _userService; private readonly IContentService _contentService; private readonly IEntityService _entityService; + private readonly AppCaches _appCaches; public enum ContentAccess { @@ -29,11 +31,13 @@ namespace Umbraco.Cms.Core.Security public ContentPermissions( IUserService userService, IContentService contentService, - IEntityService entityService) + IEntityService entityService, + AppCaches appCaches) { _userService = userService; _contentService = contentService; _entityService = entityService; + _appCaches = appCaches; } public ContentAccess CheckPermissions( @@ -50,7 +54,7 @@ namespace Umbraco.Cms.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; @@ -78,7 +82,7 @@ namespace Umbraco.Cms.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; @@ -119,16 +123,16 @@ namespace Umbraco.Cms.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; @@ -170,16 +174,16 @@ namespace Umbraco.Cms.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/Security/MediaPermissions.cs b/src/Umbraco.Core/Security/MediaPermissions.cs index e74144133d..724049d6b9 100644 --- a/src/Umbraco.Core/Security/MediaPermissions.cs +++ b/src/Umbraco.Core/Security/MediaPermissions.cs @@ -1,4 +1,5 @@ using System; +using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.Membership; using Umbraco.Cms.Core.Services; @@ -12,6 +13,7 @@ namespace Umbraco.Cms.Core.Security { private readonly IMediaService _mediaService; private readonly IEntityService _entityService; + private readonly AppCaches _appCaches; public enum MediaAccess { @@ -20,10 +22,11 @@ namespace Umbraco.Cms.Core.Security NotFound } - public MediaPermissions(IMediaService mediaService, IEntityService entityService) + public MediaPermissions(IMediaService mediaService, IEntityService entityService, AppCaches appCaches) { _mediaService = mediaService; _entityService = entityService; + _appCaches = appCaches; } /// @@ -52,10 +55,10 @@ namespace Umbraco.Cms.Core.Security } 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 ? MediaAccess.Granted : MediaAccess.Denied; } @@ -66,7 +69,7 @@ namespace Umbraco.Cms.Core.Security if (media == null) return MediaAccess.NotFound; - var hasPathAccess = user.HasPathAccess(media, _entityService); + var hasPathAccess = user.HasPathAccess(media, _entityService, _appCaches); return hasPathAccess ? MediaAccess.Granted : MediaAccess.Denied; } diff --git a/src/Umbraco.Core/Services/ContentServiceExtensions.cs b/src/Umbraco.Core/Services/ContentServiceExtensions.cs index f6b236439b..ffa0a38489 100644 --- a/src/Umbraco.Core/Services/ContentServiceExtensions.cs +++ b/src/Umbraco.Core/Services/ContentServiceExtensions.cs @@ -47,7 +47,7 @@ namespace Umbraco.Extensions var matches = AnchorRegex.Matches(rteContent); foreach (Match match in matches) { - result.Add(match.Value.Split('\"')[1]); + result.Add(match.Value.Split(Constants.CharArrays.DoubleQuote)[1]); } return result; } diff --git a/src/Umbraco.Core/Services/DashboardService.cs b/src/Umbraco.Core/Services/DashboardService.cs index 3f806bcc43..d4116f5dd8 100644 --- a/src/Umbraco.Core/Services/DashboardService.cs +++ b/src/Umbraco.Core/Services/DashboardService.cs @@ -82,7 +82,7 @@ namespace Umbraco.Cms.Core.Services if (grantBySectionRules.Length > 0) { var allowedSections = sectionService.GetAllowedSections(user.Id).Select(x => x.Alias).ToArray(); - var wantedSections = grantBySectionRules.SelectMany(g => g.Value.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries)).ToArray(); + var wantedSections = grantBySectionRules.SelectMany(g => g.Value.Split(Constants.CharArrays.Comma, StringSplitOptions.RemoveEmptyEntries)).ToArray(); if (wantedSections.Intersect(allowedSections).Any()) hasAccess = true; @@ -93,7 +93,7 @@ namespace Umbraco.Cms.Core.Services if (hasAccess == false && grantRules.Any()) { assignedUserGroups = user.Groups.Select(x => x.Alias).ToArray(); - var wantedUserGroups = grantRules.SelectMany(g => g.Value.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries)).ToArray(); + var wantedUserGroups = grantRules.SelectMany(g => g.Value.Split(Constants.CharArrays.Comma, StringSplitOptions.RemoveEmptyEntries)).ToArray(); if (wantedUserGroups.Intersect(assignedUserGroups).Any()) hasAccess = true; @@ -107,7 +107,7 @@ namespace Umbraco.Cms.Core.Services // check if this item has any deny arguments, if so check if the user is in one of the denied user groups, if so they will // be denied to see it no matter what assignedUserGroups = assignedUserGroups ?? user.Groups.Select(x => x.Alias).ToArray(); - var deniedUserGroups = denyRules.SelectMany(g => g.Value.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries)).ToArray(); + var deniedUserGroups = denyRules.SelectMany(g => g.Value.Split(Constants.CharArrays.Comma, StringSplitOptions.RemoveEmptyEntries)).ToArray(); if (deniedUserGroups.Intersect(assignedUserGroups).Any()) hasAccess = false; diff --git a/src/Umbraco.Core/Services/UserServiceExtensions.cs b/src/Umbraco.Core/Services/UserServiceExtensions.cs index 7206f74964..c06711a91e 100644 --- a/src/Umbraco.Core/Services/UserServiceExtensions.cs +++ b/src/Umbraco.Core/Services/UserServiceExtensions.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using Umbraco.Cms.Core; using Umbraco.Cms.Core.Models.Membership; using Umbraco.Cms.Core.Services; @@ -10,7 +11,7 @@ namespace Umbraco.Extensions { public static EntityPermission GetPermissions(this IUserService userService, IUser user, string path) { - var ids = path.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries) + var ids = path.Split(Constants.CharArrays.Comma, StringSplitOptions.RemoveEmptyEntries) .Select(x => x.TryConvertTo()) .Where(x => x.Success) .Select(x => x.Result) diff --git a/src/Umbraco.Core/StringUdi.cs b/src/Umbraco.Core/StringUdi.cs index f2b138a938..3435c81780 100644 --- a/src/Umbraco.Core/StringUdi.cs +++ b/src/Umbraco.Core/StringUdi.cs @@ -33,7 +33,7 @@ namespace Umbraco.Cms.Core public StringUdi(Uri uriValue) : base(uriValue) { - Id = Uri.UnescapeDataString(uriValue.AbsolutePath.TrimStart('/')); + Id = Uri.UnescapeDataString(uriValue.AbsolutePath.TrimStart(Constants.CharArrays.ForwardSlash)); } private static string EscapeUriString(string s) @@ -46,7 +46,7 @@ namespace Umbraco.Cms.Core // we want to preserve the / and the unreserved // so... - return string.Join("/", s.Split('/').Select(Uri.EscapeDataString)); + return string.Join("/", s.Split(Constants.CharArrays.ForwardSlash).Select(Uri.EscapeDataString)); } /// diff --git a/src/Umbraco.Core/Strings/Css/StylesheetRule.cs b/src/Umbraco.Core/Strings/Css/StylesheetRule.cs index c132c5d592..b6ffeaa57e 100644 --- a/src/Umbraco.Core/Strings/Css/StylesheetRule.cs +++ b/src/Umbraco.Core/Strings/Css/StylesheetRule.cs @@ -28,7 +28,7 @@ namespace Umbraco.Cms.Core.Strings.Css { // since we already have a string builder in play here, we'll append to it the "hard" way // instead of using string interpolation (for increased performance) - foreach (var style in Styles.Split(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries)) + foreach (var style in Styles.Split(Constants.CharArrays.Semicolon, StringSplitOptions.RemoveEmptyEntries)) { sb.Append("\t").Append(style.StripNewLines().Trim()).Append(";").Append(Environment.NewLine); } diff --git a/src/Umbraco.Core/Strings/Diff.cs b/src/Umbraco.Core/Strings/Diff.cs index 8486875ad1..b0cf7100de 100644 --- a/src/Umbraco.Core/Strings/Diff.cs +++ b/src/Umbraco.Core/Strings/Diff.cs @@ -229,7 +229,7 @@ namespace Umbraco.Cms.Core.Strings // strip off all cr, only use lf as text line separator. aText = aText.Replace("\r", ""); - var lines = aText.Split('\n'); + var lines = aText.Split(Constants.CharArrays.LineFeed); var codes = new int[lines.Length]; diff --git a/src/Umbraco.Core/Trees/TreeNode.cs b/src/Umbraco.Core/Trees/TreeNode.cs index c09f7559ca..4e509da259 100644 --- a/src/Umbraco.Core/Trees/TreeNode.cs +++ b/src/Umbraco.Core/Trees/TreeNode.cs @@ -103,8 +103,19 @@ namespace Umbraco.Cms.Core.Trees { get { - // TODO: Is this ever actually used? If not remove, if so, add setter. return string.Empty; + + //TODO Figure out how to do this, without the model has to know a bout services and config. + // + // if (IconIsClass) + // return string.Empty; + // + // //absolute path with or without tilde + // if (Icon.StartsWith("~") || Icon.StartsWith("/")) + // return IOHelper.ResolveUrl("~" + Icon.TrimStart(Constants.CharArrays.Tilde)); + // + // //legacy icon path + // return string.Format("{0}images/umbraco/{1}", Current.Configs.Global().Path.EnsureEndsWith("/"), Icon); } } diff --git a/src/Umbraco.Core/UdiRange.cs b/src/Umbraco.Core/UdiRange.cs index 50f5b88189..250eef7e71 100644 --- a/src/Umbraco.Core/UdiRange.cs +++ b/src/Umbraco.Core/UdiRange.cs @@ -70,7 +70,7 @@ namespace Umbraco.Cms.Core } var udiUri = uri.Query == string.Empty ? uri : new UriBuilder(uri) { Query = string.Empty }.Uri; - return new UdiRange(Udi.Create(udiUri), uri.Query.TrimStart('?')); + return new UdiRange(Udi.Create(udiUri), uri.Query.TrimStart(Constants.CharArrays.QuestionMark)); } public override string ToString() diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index 1166bc1270..2c03750771 100644 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -3,7 +3,7 @@ netstandard2.0 Umbraco.Cms.Core - Umbraco CMS + Umbraco CMS diff --git a/src/Umbraco.Core/UriUtilityCore.cs b/src/Umbraco.Core/UriUtilityCore.cs index 8716865a9e..d63692b30a 100644 --- a/src/Umbraco.Core/UriUtilityCore.cs +++ b/src/Umbraco.Core/UriUtilityCore.cs @@ -45,7 +45,7 @@ namespace Umbraco.Cms.Core var pos = Math.Min(pos1, pos2); var path = pos > 0 ? uri.Substring(0, pos) : uri; - path = path.TrimEnd('/'); + path = path.TrimEnd(Constants.CharArrays.ForwardSlash); if (pos > 0) path += uri.Substring(pos); diff --git a/src/Umbraco.Core/Xml/XmlHelper.cs b/src/Umbraco.Core/Xml/XmlHelper.cs index ab171659fb..6cbb888965 100644 --- a/src/Umbraco.Core/Xml/XmlHelper.cs +++ b/src/Umbraco.Core/Xml/XmlHelper.cs @@ -53,7 +53,7 @@ namespace Umbraco.Cms.Core.Xml public static bool IsXmlWhitespace(string s) { // as per xml 1.1 specs - anything else is significant whitespace - s = s.Trim(' ', '\t', '\r', '\n'); + s = s.Trim(Constants.CharArrays.XmlWhitespaceChars); return s.Length == 0; } diff --git a/src/Umbraco.Examine.Lucene/BackOfficeExamineSearcher.cs b/src/Umbraco.Examine.Lucene/BackOfficeExamineSearcher.cs index c9fab6b6fc..3da6b854f8 100644 --- a/src/Umbraco.Examine.Lucene/BackOfficeExamineSearcher.cs +++ b/src/Umbraco.Examine.Lucene/BackOfficeExamineSearcher.cs @@ -8,10 +8,14 @@ using System.Text; using System.Text.RegularExpressions; using Examine; using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Cache; +using Umbraco.Cms.Core.Mapping; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.ContentEditing; +using Umbraco.Cms.Core.Routing; using Umbraco.Cms.Core.Security; using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Web; using Umbraco.Extensions; using Constants = Umbraco.Cms.Core.Constants; @@ -24,18 +28,27 @@ namespace Umbraco.Cms.Infrastructure.Examine private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor; private readonly IEntityService _entityService; private readonly IUmbracoTreeSearcherFields _treeSearcherFields; + private readonly AppCaches _appCaches; + private readonly UmbracoMapper _umbracoMapper; + private readonly IPublishedUrlProvider _publishedUrlProvider; public BackOfficeExamineSearcher(IExamineManager examineManager, - ILocalizationService languageService, - IBackOfficeSecurityAccessor backOfficeSecurityAccessor, - IEntityService entityService, - IUmbracoTreeSearcherFields treeSearcherFields) + ILocalizationService languageService, + IBackOfficeSecurityAccessor backOfficeSecurityAccessor, + IEntityService entityService, + IUmbracoTreeSearcherFields treeSearcherFields, + AppCaches appCaches, + UmbracoMapper umbracoMapper, + IPublishedUrlProvider publishedUrlProvider) { _examineManager = examineManager; _languageService = languageService; _backOfficeSecurityAccessor = backOfficeSecurityAccessor; _entityService = entityService; _treeSearcherFields = treeSearcherFields; + _appCaches = appCaches; + _umbracoMapper = umbracoMapper; + _publishedUrlProvider = publishedUrlProvider; } public IEnumerable Search(string query, UmbracoEntityTypes entityType, int pageSize, long pageIndex, out long totalFound, string searchFrom = null, bool ignoreUserStartNodes = false) @@ -71,7 +84,7 @@ namespace Umbraco.Cms.Infrastructure.Examine type = "media"; fields.AddRange(_treeSearcherFields.GetBackOfficeMediaFields()); var allMediaStartNodes = currentUser != null - ? currentUser.CalculateMediaStartNodeIds(_entityService) + ? currentUser.CalculateMediaStartNodeIds(_entityService, _appCaches) : Array.Empty(); AppendPath(sb, UmbracoObjectTypes.Media, allMediaStartNodes, searchFrom, ignoreUserStartNodes, _entityService); break; @@ -79,7 +92,7 @@ namespace Umbraco.Cms.Infrastructure.Examine type = "content"; fields.AddRange(_treeSearcherFields.GetBackOfficeDocumentFields()); var allContentStartNodes = currentUser != null - ? currentUser.CalculateContentStartNodeIds(_entityService) + ? currentUser.CalculateContentStartNodeIds(_entityService, _appCaches) : Array.Empty(); AppendPath(sb, UmbracoObjectTypes.Document, allContentStartNodes, searchFrom, ignoreUserStartNodes, _entityService); break; @@ -128,7 +141,7 @@ namespace Umbraco.Cms.Infrastructure.Examine if (surroundedByQuotes) { //strip quotes, escape string, the replace again - query = query.Trim('\"', '\''); + query = query.Trim(Constants.CharArrays.DoubleQuoteSingleQuote); query = Lucene.Net.QueryParsers.QueryParser.Escape(query); @@ -162,7 +175,7 @@ namespace Umbraco.Cms.Infrastructure.Examine } else { - var trimmed = query.Trim(new[] { '\"', '\'' }); + var trimmed = query.Trim(Constants.CharArrays.DoubleQuoteSingleQuote); //nothing to search if (searchFrom.IsNullOrWhiteSpace() && trimmed.IsNullOrWhiteSpace()) @@ -175,7 +188,7 @@ namespace Umbraco.Cms.Infrastructure.Examine { query = Lucene.Net.QueryParsers.QueryParser.Escape(query); - var querywords = query.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries); + var querywords = query.Split(Constants.CharArrays.Space, StringSplitOptions.RemoveEmptyEntries); sb.Append("+("); @@ -341,5 +354,85 @@ namespace Umbraco.Cms.Infrastructure.Examine sb.Append(path); sb.Append("\\,*"); } + + /// + /// Returns a collection of entities for media based on search results + /// + /// + /// + private IEnumerable MemberFromSearchResults(IEnumerable results) + { + //add additional data + foreach (var result in results) + { + var m = _umbracoMapper.Map(result); + + //if no icon could be mapped, it will be set to document, so change it to picture + if (m.Icon == Constants.Icons.DefaultIcon) + { + m.Icon = Constants.Icons.Member; + } + + if (result.Values.ContainsKey("email") && result.Values["email"] != null) + { + m.AdditionalData["Email"] = result.Values["email"]; + } + if (result.Values.ContainsKey(UmbracoExamineFieldNames.NodeKeyFieldName) && result.Values[UmbracoExamineFieldNames.NodeKeyFieldName] != null) + { + if (Guid.TryParse(result.Values[UmbracoExamineFieldNames.NodeKeyFieldName], out var key)) + { + m.Key = key; + } + } + + yield return m; + } + } + + /// + /// Returns a collection of entities for media based on search results + /// + /// + /// + private IEnumerable MediaFromSearchResults(IEnumerable results) + => _umbracoMapper.Map>(results); + + /// + /// Returns a collection of entities for content based on search results + /// + /// + /// + private IEnumerable ContentFromSearchResults(IEnumerable results, string culture = null) + { + var defaultLang = _languageService.GetDefaultLanguageIsoCode(); + foreach (var result in results) + { + var entity = _umbracoMapper.Map(result, context => + { + if (culture != null) + { + context.SetCulture(culture); + } + } + ); + + var intId = entity.Id.TryConvertTo(); + if (intId.Success) + { + //if it varies by culture, return the default language URL + if (result.Values.TryGetValue(UmbracoExamineFieldNames.VariesByCultureFieldName, out var varies) && varies == "y") + { + entity.AdditionalData["Url"] = _publishedUrlProvider.GetUrl(intId.Result, culture: culture ?? defaultLang); + } + else + { + entity.AdditionalData["Url"] = _publishedUrlProvider.GetUrl(intId.Result); + } + } + + yield return entity; + } + } + } } diff --git a/src/Umbraco.Infrastructure/Cache/DistributedCacheBinder.cs b/src/Umbraco.Infrastructure/Cache/DistributedCacheBinder.cs index 7f0b33cfc6..ed24221e25 100644 --- a/src/Umbraco.Infrastructure/Cache/DistributedCacheBinder.cs +++ b/src/Umbraco.Infrastructure/Cache/DistributedCacheBinder.cs @@ -43,7 +43,6 @@ namespace Umbraco.Cms.Core.Cache private static readonly Lazy CandidateHandlers = new Lazy(() => { - var underscore = new[] { '_' }; return typeof(DistributedCacheBinder) .GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic) @@ -51,7 +50,7 @@ namespace Umbraco.Cms.Core.Cache { if (x.Name.Contains("_") == false) return null; - var parts = x.Name.Split(underscore, StringSplitOptions.RemoveEmptyEntries).Length; + var parts = x.Name.Split(Constants.CharArrays.Underscore, StringSplitOptions.RemoveEmptyEntries).Length; if (parts != 2) return null; var parameters = x.GetParameters(); diff --git a/src/Umbraco.Infrastructure/Cache/DistributedCacheExtensions.cs b/src/Umbraco.Infrastructure/Cache/DistributedCacheExtensions.cs index e28af2f49d..a0ba0ff128 100644 --- a/src/Umbraco.Infrastructure/Cache/DistributedCacheExtensions.cs +++ b/src/Umbraco.Infrastructure/Cache/DistributedCacheExtensions.cs @@ -133,13 +133,13 @@ namespace Umbraco.Extensions public static void RefreshMemberCache(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, false))); } 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, true))); } #endregion diff --git a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs index f42e88b7df..6900fbc42b 100644 --- a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs +++ b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs @@ -191,8 +191,8 @@ namespace Umbraco.Cms.Infrastructure.DependencyInjection { builder.Services.AddUnique(factory => { - var globalSettings = factory.GetRequiredService>().Value; - var connectionStrings = factory.GetRequiredService>().Value; + var globalSettings = factory.GetRequiredService>(); + var connectionStrings = factory.GetRequiredService>(); var hostingEnvironment = factory.GetRequiredService(); var dbCreator = factory.GetRequiredService(); @@ -200,7 +200,7 @@ namespace Umbraco.Cms.Infrastructure.DependencyInjection var isWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows); var loggerFactory = factory.GetRequiredService(); - return globalSettings.MainDomLock.Equals("SqlMainDomLock") || isWindows == false + return globalSettings.Value.MainDomLock.Equals("SqlMainDomLock") || isWindows == false ? (IMainDomLock)new SqlMainDomLock(loggerFactory.CreateLogger(), loggerFactory, globalSettings, connectionStrings, dbCreator, hostingEnvironment, databaseSchemaCreatorFactory) : new MainDomSemaphoreLock(loggerFactory.CreateLogger(), hostingEnvironment); }); diff --git a/src/Umbraco.Infrastructure/Examine/ContentValueSetValidator.cs b/src/Umbraco.Infrastructure/Examine/ContentValueSetValidator.cs index 010ccdf149..463e8dee26 100644 --- a/src/Umbraco.Infrastructure/Examine/ContentValueSetValidator.cs +++ b/src/Umbraco.Infrastructure/Examine/ContentValueSetValidator.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using System.Linq; using Examine; +using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Core.Services; using Umbraco.Extensions; using Constants = Umbraco.Cms.Core.Constants; @@ -13,9 +14,9 @@ namespace Umbraco.Cms.Infrastructure.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; } @@ -51,25 +52,38 @@ namespace Umbraco.Cms.Infrastructure.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) { @@ -77,6 +91,7 @@ namespace Umbraco.Cms.Infrastructure.Examine SupportProtectedContent = supportProtectedContent; ParentId = parentId; _publicAccessService = publicAccessService; + _scopeProvider = scopeProvider; } public override ValueSetValidationResult Validate(ValueSet valueSet) @@ -101,7 +116,7 @@ namespace Umbraco.Cms.Infrastructure.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($"{UmbracoExamineFieldNames.PublishedFieldName}_")).ToList()) + foreach (var publishField in valueSet.Values.Where(x => x.Key.StartsWith($"{UmbracoExamineFieldNames.PublishedFieldName}_")).ToList()) { if (publishField.Value.Count <= 0 || !publishField.Value[0].Equals("y")) { @@ -132,7 +147,7 @@ namespace Umbraco.Cms.Infrastructure.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.Infrastructure/Examine/UmbracoIndexConfig.cs b/src/Umbraco.Infrastructure/Examine/UmbracoIndexConfig.cs index 2c282a1924..49607b5851 100644 --- a/src/Umbraco.Infrastructure/Examine/UmbracoIndexConfig.cs +++ b/src/Umbraco.Infrastructure/Examine/UmbracoIndexConfig.cs @@ -1,24 +1,28 @@ using Examine; +using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Core.Services; namespace Umbraco.Cms.Infrastructure.Examine { public class UmbracoIndexConfig : IUmbracoIndexConfig { - public UmbracoIndexConfig(IPublicAccessService publicAccessService) + + public UmbracoIndexConfig(IPublicAccessService publicAccessService, IScopeProvider scopeProvider) { + ScopeProvider = scopeProvider; PublicAccessService = publicAccessService; } protected IPublicAccessService PublicAccessService { get; } + protected IScopeProvider ScopeProvider { get; } public IContentValueSetValidator GetContentValueSetValidator() { - return new ContentValueSetValidator(false, true, PublicAccessService); + return new ContentValueSetValidator(false, true, PublicAccessService, ScopeProvider); } public IContentValueSetValidator GetPublishedContentValueSetValidator() { - return new ContentValueSetValidator(true, false, PublicAccessService); + return new ContentValueSetValidator(true, false, PublicAccessService, ScopeProvider); } /// diff --git a/src/Umbraco.Infrastructure/HostedServices/KeepAlive.cs b/src/Umbraco.Infrastructure/HostedServices/KeepAlive.cs index ad64319f5e..c9b4aeec82 100644 --- a/src/Umbraco.Infrastructure/HostedServices/KeepAlive.cs +++ b/src/Umbraco.Infrastructure/HostedServices/KeepAlive.cs @@ -6,6 +6,7 @@ using System.Net.Http; using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; +using Umbraco.Cms.Core; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Hosting; using Umbraco.Cms.Core.Logging; @@ -96,7 +97,7 @@ namespace Umbraco.Cms.Infrastructure.HostedServices return; } - keepAlivePingUrl = keepAlivePingUrl.Replace("{umbracoApplicationUrl}", umbracoAppUrl.TrimEnd('/')); + keepAlivePingUrl = keepAlivePingUrl.Replace("{umbracoApplicationUrl}", umbracoAppUrl.TrimEnd(Constants.CharArrays.ForwardSlash)); } var request = new HttpRequestMessage(HttpMethod.Get, keepAlivePingUrl); diff --git a/src/Umbraco.Infrastructure/Logging/Viewer/LogViewerConfig.cs b/src/Umbraco.Infrastructure/Logging/Viewer/LogViewerConfig.cs index 13b295f4bc..da17bad085 100644 --- a/src/Umbraco.Infrastructure/Logging/Viewer/LogViewerConfig.cs +++ b/src/Umbraco.Infrastructure/Logging/Viewer/LogViewerConfig.cs @@ -17,7 +17,7 @@ namespace Umbraco.Cms.Core.Logging.Viewer public LogViewerConfig(IHostingEnvironment hostingEnvironment) { _hostingEnvironment = hostingEnvironment; - var trimmedPath = _pathToSearches.TrimStart('~', '/').Replace('/', Path.DirectorySeparatorChar); + var trimmedPath = _pathToSearches.TrimStart(Constants.CharArrays.TildeForwardSlash).Replace('/', Path.DirectorySeparatorChar); var absolutePath = Path.Combine(_hostingEnvironment.ApplicationPhysicalPath, trimmedPath); _searchesConfig = new FileInfo(absolutePath); } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/PropertyEditorsMigrationBase.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/PropertyEditorsMigrationBase.cs index 68ad810619..b5e50d2248 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/PropertyEditorsMigrationBase.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/PropertyEditorsMigrationBase.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; using Microsoft.Extensions.Logging; using Newtonsoft.Json; +using Umbraco.Cms.Core; using Umbraco.Cms.Core.IO; using Umbraco.Cms.Core.PropertyEditors; using Umbraco.Cms.Infrastructure.Persistence.Dtos; @@ -31,7 +32,7 @@ namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0 protected int[] ConvertStringValues(string val) { - var splitVals = val.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries); + var splitVals = val.Split(Constants.CharArrays.Comma, StringSplitOptions.RemoveEmptyEntries); var intVals = splitVals .Select(x => int.TryParse(x, out var i) ? i : int.MinValue) diff --git a/src/Umbraco.Infrastructure/Models/PathValidationExtensions.cs b/src/Umbraco.Infrastructure/Models/PathValidationExtensions.cs index d805eba9d5..8758d17d07 100644 --- a/src/Umbraco.Infrastructure/Models/PathValidationExtensions.cs +++ b/src/Umbraco.Infrastructure/Models/PathValidationExtensions.cs @@ -27,7 +27,7 @@ namespace Umbraco.Cms.Core.Models if (entity.Path.IsNullOrWhiteSpace()) throw new InvalidDataException($"The content item {entity.NodeId} has an empty path: {entity.Path} with parentID: {entity.ParentId}"); - var pathParts = entity.Path.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries); + var pathParts = entity.Path.Split(Constants.CharArrays.Comma, StringSplitOptions.RemoveEmptyEntries); if (pathParts.Length < 2) { //a path cannot be less than 2 parts, at a minimum it must be root (-1) and it's own id @@ -55,7 +55,7 @@ namespace Umbraco.Cms.Core.Models if (entity.Path.IsNullOrWhiteSpace()) return false; - var pathParts = entity.Path.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries); + var pathParts = entity.Path.Split(Constants.CharArrays.Comma, StringSplitOptions.RemoveEmptyEntries); if (pathParts.Length < 2) { //a path cannot be less than 2 parts, at a minimum it must be root (-1) and it's own id diff --git a/src/Umbraco.Infrastructure/ModelsBuilder/Building/TextBuilder.cs b/src/Umbraco.Infrastructure/ModelsBuilder/Building/TextBuilder.cs index 7acfbcf26a..40a3aaf9f2 100644 --- a/src/Umbraco.Infrastructure/ModelsBuilder/Building/TextBuilder.cs +++ b/src/Umbraco.Infrastructure/ModelsBuilder/Building/TextBuilder.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; using System.Text; using System.Text.RegularExpressions; +using Umbraco.Cms.Core; using Umbraco.Cms.Core.Configuration.Models; namespace Umbraco.Cms.Infrastructure.ModelsBuilder.Building @@ -447,7 +448,7 @@ namespace Umbraco.Cms.Infrastructure.ModelsBuilder.Building { WriteNonGenericClrType(sb, type.Substring(0, p)); sb.Append("<"); - var args = type.Substring(p + 1).TrimEnd('>').Split(','); // fixme will NOT work with nested generic types + var args = type.Substring(p + 1).TrimEnd(Constants.CharArrays.GreaterThan).Split(Constants.CharArrays.Comma); // fixme will NOT work with nested generic types for (var i = 0; i < args.Length; i++) { if (i > 0) sb.Append(", "); diff --git a/src/Umbraco.Infrastructure/Packaging/PackageDataInstallation.cs b/src/Umbraco.Infrastructure/Packaging/PackageDataInstallation.cs index ead0791f08..16007069c6 100644 --- a/src/Umbraco.Infrastructure/Packaging/PackageDataInstallation.cs +++ b/src/Umbraco.Infrastructure/Packaging/PackageDataInstallation.cs @@ -439,12 +439,28 @@ namespace Umbraco.Cms.Core.Packaging int.Parse(sortOrder), template?.Id); + // 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; @@ -452,14 +468,30 @@ namespace Umbraco.Cms.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; } @@ -635,7 +667,7 @@ namespace Umbraco.Cms.Core.Packaging && ((string)infoElement.Element("Master")).IsNullOrWhiteSpace()) { var alias = documentType.Element("Info").Element("Alias").Value; - var folders = foldersAttribute.Value.Split('/'); + var folders = foldersAttribute.Value.Split(Constants.CharArrays.ForwardSlash); var rootFolder = WebUtility.UrlDecode(folders[0]); //level 1 = root level folders, there can only be one with the same name var current = _contentTypeService.GetContainers(rootFolder, 1).FirstOrDefault(); @@ -1095,7 +1127,7 @@ namespace Umbraco.Cms.Core.Packaging if (foldersAttribute != null) { var name = datatypeElement.Attribute("Name").Value; - var folders = foldersAttribute.Value.Split('/'); + var folders = foldersAttribute.Value.Split(Constants.CharArrays.ForwardSlash); var rootFolder = WebUtility.UrlDecode(folders[0]); //there will only be a single result by name for level 1 (root) containers var current = _dataTypeService.GetContainers(rootFolder, 1).FirstOrDefault(); diff --git a/src/Umbraco.Infrastructure/Persistence/DatabaseModelDefinitions/DefinitionFactory.cs b/src/Umbraco.Infrastructure/Persistence/DatabaseModelDefinitions/DefinitionFactory.cs index 986e2d760a..a3ca285918 100644 --- a/src/Umbraco.Infrastructure/Persistence/DatabaseModelDefinitions/DefinitionFactory.cs +++ b/src/Umbraco.Infrastructure/Persistence/DatabaseModelDefinitions/DefinitionFactory.cs @@ -161,7 +161,7 @@ namespace Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions if (string.IsNullOrEmpty(attribute.ForColumns) == false) { - var columns = attribute.ForColumns.Split(',').Select(p => p.Trim()); + var columns = attribute.ForColumns.Split(Constants.CharArrays.Comma).Select(p => p.Trim()); foreach (var column in columns) { definition.Columns.Add(new IndexColumnDefinition {Name = column, Direction = Direction.Ascending}); diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentRepositoryBase.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentRepositoryBase.cs index d3c28b60de..da96e6dfc8 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentRepositoryBase.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentRepositoryBase.cs @@ -525,7 +525,7 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement currentParentIds.Add(node.NodeId); // paths parts without the roots - var pathParts = node.Path.Split(',').Where(x => !rootIds.Contains(int.Parse(x))).ToArray(); + var pathParts = node.Path.Split(Constants.CharArrays.Comma).Where(x => !rootIds.Contains(int.Parse(x))).ToArray(); if (!prevParentIds.Contains(node.ParentId)) { diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentTypeRepositoryBase.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentTypeRepositoryBase.cs index ee2e3aee60..7c8d816ec8 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentTypeRepositoryBase.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentTypeRepositoryBase.cs @@ -5,6 +5,7 @@ using System.Globalization; using System.Linq; using Microsoft.Extensions.Logging; using NPoco; +using Umbraco.Cms.Core; using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Exceptions; @@ -1339,7 +1340,7 @@ WHERE cmsContentType." + aliasColumn + @" LIKE @pattern", /// public bool HasContainerInPath(string contentPath) { - var ids = contentPath.Split(',').Select(int.Parse).ToArray(); + var ids = contentPath.Split(Constants.CharArrays.Comma).Select(int.Parse).ToArray(); return HasContainerInPath(ids); } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DocumentRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DocumentRepository.cs index 43ee275e7f..3500f0458d 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DocumentRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DocumentRepository.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; using Microsoft.Extensions.Logging; using NPoco; +using Umbraco.Cms.Core; using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.Membership; @@ -916,7 +917,7 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement if (content.ParentId == -1) return content.Published; - var ids = content.Path.Split(',').Skip(1).Select(int.Parse); + var ids = content.Path.Split(Constants.CharArrays.Comma).Skip(1).Select(int.Parse); var sql = SqlContext.Sql() .SelectCount(x => x.NodeId) diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/UserRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/UserRepository.cs index 80201360c3..4e4f34bf54 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/UserRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/UserRepository.cs @@ -213,7 +213,7 @@ ORDER BY colName"; return false; //now detect if there's been a timeout - if (DateTime.UtcNow - found.LastValidatedUtc > TimeSpan.FromMinutes(_globalSettings.TimeOutInMinutes)) + if (DateTime.UtcNow - found.LastValidatedUtc > _globalSettings.TimeOut) { //timeout detected, update the record ClearLoginSession(sessionId); diff --git a/src/Umbraco.Infrastructure/Persistence/SqlServerDbProviderFactoryCreator.cs b/src/Umbraco.Infrastructure/Persistence/SqlServerDbProviderFactoryCreator.cs index a036321c38..789331177e 100644 --- a/src/Umbraco.Infrastructure/Persistence/SqlServerDbProviderFactoryCreator.cs +++ b/src/Umbraco.Infrastructure/Persistence/SqlServerDbProviderFactoryCreator.cs @@ -1,5 +1,7 @@ using System; using System.Data.Common; +using Microsoft.Extensions.Options; +using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Infrastructure.Persistence.SqlSyntax; namespace Umbraco.Cms.Infrastructure.Persistence @@ -7,10 +9,12 @@ namespace Umbraco.Cms.Infrastructure.Persistence public class SqlServerDbProviderFactoryCreator : IDbProviderFactoryCreator { private readonly Func _getFactory; + private readonly IOptions _globalSettings; - public SqlServerDbProviderFactoryCreator(Func getFactory) + public SqlServerDbProviderFactoryCreator(Func getFactory, IOptions globalSettings) { _getFactory = getFactory; + _globalSettings = globalSettings; } public DbProviderFactory CreateFactory(string providerName) @@ -25,7 +29,7 @@ namespace Umbraco.Cms.Infrastructure.Persistence return providerName switch { Cms.Core.Constants.DbProviderNames.SqlCe => throw new NotSupportedException("SqlCe is not supported"), - Cms.Core.Constants.DbProviderNames.SqlServer => new SqlServerSyntaxProvider(), + Cms.Core.Constants.DbProviderNames.SqlServer => new SqlServerSyntaxProvider(_globalSettings), _ => throw new InvalidOperationException($"Unknown provider name \"{providerName}\""), }; } diff --git a/src/Umbraco.Infrastructure/Persistence/SqlSyntax/ISqlSyntaxProvider.cs b/src/Umbraco.Infrastructure/Persistence/SqlSyntax/ISqlSyntaxProvider.cs index 37038255a0..6c551648b7 100644 --- a/src/Umbraco.Infrastructure/Persistence/SqlSyntax/ISqlSyntaxProvider.cs +++ b/src/Umbraco.Infrastructure/Persistence/SqlSyntax/ISqlSyntaxProvider.cs @@ -131,6 +131,9 @@ namespace Umbraco.Cms.Infrastructure.Persistence.SqlSyntax /// bool TryGetDefaultConstraint(IDatabase db, string tableName, string columnName, out string constraintName); + void ReadLock(IDatabase db, TimeSpan timeout, int lockId); + void WriteLock(IDatabase db, TimeSpan timeout, int lockId); + void ReadLock(IDatabase db, params int[] lockIds); void WriteLock(IDatabase db, params int[] lockIds); } diff --git a/src/Umbraco.Infrastructure/Persistence/SqlSyntax/MicrosoftSqlSyntaxProviderBase.cs b/src/Umbraco.Infrastructure/Persistence/SqlSyntax/MicrosoftSqlSyntaxProviderBase.cs index 8a80a33ad0..4c75128926 100644 --- a/src/Umbraco.Infrastructure/Persistence/SqlSyntax/MicrosoftSqlSyntaxProviderBase.cs +++ b/src/Umbraco.Infrastructure/Persistence/SqlSyntax/MicrosoftSqlSyntaxProviderBase.cs @@ -1,6 +1,7 @@ using System; using System.Data; using System.Linq; +using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence.Querying; namespace Umbraco.Cms.Infrastructure.Persistence.SqlSyntax @@ -34,7 +35,7 @@ namespace Umbraco.Cms.Infrastructure.Persistence.SqlSyntax if (tableName.Contains(".") == false) return $"[{tableName}]"; - var tableNameParts = tableName.Split(new[] { '.' }, 2); + var tableNameParts = tableName.Split(Constants.CharArrays.Period, 2); return $"[{tableNameParts[0]}].[{tableNameParts[1]}]"; } diff --git a/src/Umbraco.Infrastructure/Persistence/SqlSyntax/SqlServerSyntaxProvider.cs b/src/Umbraco.Infrastructure/Persistence/SqlSyntax/SqlServerSyntaxProvider.cs index 58a283a142..6b2f69de6d 100644 --- a/src/Umbraco.Infrastructure/Persistence/SqlSyntax/SqlServerSyntaxProvider.cs +++ b/src/Umbraco.Infrastructure/Persistence/SqlSyntax/SqlServerSyntaxProvider.cs @@ -4,7 +4,10 @@ using System.Data; using System.Data.SqlClient; using System.Linq; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; using NPoco; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; using Umbraco.Extensions; @@ -15,6 +18,13 @@ namespace Umbraco.Cms.Infrastructure.Persistence.SqlSyntax /// public class SqlServerSyntaxProvider : MicrosoftSqlSyntaxProviderBase { + private readonly IOptions _globalSettings; + + public SqlServerSyntaxProvider(IOptions globalSettings) + { + _globalSettings = globalSettings; + } + public override string ProviderName => Cms.Core.Constants.DatabaseProviders.SqlServer; public ServerVersionInfo ServerVersion { get; private set; } @@ -76,7 +86,7 @@ namespace Umbraco.Cms.Infrastructure.Persistence.SqlSyntax private static VersionName MapProductVersion(string productVersion) { - var firstPart = string.IsNullOrWhiteSpace(productVersion) ? "??" : productVersion.Split('.')[0]; + var firstPart = string.IsNullOrWhiteSpace(productVersion) ? "??" : productVersion.Split(Constants.CharArrays.Period)[0]; switch (firstPart) { case "??": @@ -256,29 +266,50 @@ where tbl.[name]=@0 and col.[name]=@1;", tableName, columnName) return result > 0; } - public override void WriteLock(IDatabase db, params int[] lockIds) - { - WriteLock(db, TimeSpan.FromSeconds(5), 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 = _globalSettings.Value.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) { @@ -287,15 +318,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.Infrastructure/Persistence/SqlSyntax/SqlSyntaxProviderBase.cs b/src/Umbraco.Infrastructure/Persistence/SqlSyntax/SqlSyntaxProviderBase.cs index f926a9d60f..b0afa9d75b 100644 --- a/src/Umbraco.Infrastructure/Persistence/SqlSyntax/SqlSyntaxProviderBase.cs +++ b/src/Umbraco.Infrastructure/Persistence/SqlSyntax/SqlSyntaxProviderBase.cs @@ -6,6 +6,7 @@ using System.Linq; using System.Text; using System.Text.RegularExpressions; using NPoco; +using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; using Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; using Umbraco.Cms.Infrastructure.Persistence.Querying; @@ -239,6 +240,9 @@ namespace Umbraco.Cms.Infrastructure.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) { @@ -407,7 +411,7 @@ namespace Umbraco.Cms.Infrastructure.Persistence.SqlSyntax var columns = string.IsNullOrEmpty(columnDefinition.PrimaryKeyColumns) ? GetQuotedColumnName(columnDefinition.Name) : string.Join(", ", columnDefinition.PrimaryKeyColumns - .Split(new[] { ',', ' ' }, StringSplitOptions.RemoveEmptyEntries) + .Split(Constants.CharArrays.CommaSpace, StringSplitOptions.RemoveEmptyEntries) .Select(GetQuotedColumnName)); var primaryKeyPart = string.Concat("PRIMARY KEY", columnDefinition.IsIndexed ? " CLUSTERED" : " NONCLUSTERED"); diff --git a/src/Umbraco.Infrastructure/Persistence/UmbracoDatabaseFactory.cs b/src/Umbraco.Infrastructure/Persistence/UmbracoDatabaseFactory.cs index 581517326f..944195cb82 100644 --- a/src/Umbraco.Infrastructure/Persistence/UmbracoDatabaseFactory.cs +++ b/src/Umbraco.Infrastructure/Persistence/UmbracoDatabaseFactory.cs @@ -31,7 +31,7 @@ namespace Umbraco.Cms.Infrastructure.Persistence { private readonly IDbProviderFactoryCreator _dbProviderFactoryCreator; private readonly DatabaseSchemaCreatorFactory _databaseSchemaCreatorFactory; - private readonly GlobalSettings _globalSettings; + private readonly IOptions _globalSettings; private readonly Lazy _mappers; private readonly ILogger _logger; private readonly ILoggerFactory _loggerFactory; @@ -70,21 +70,18 @@ namespace Umbraco.Cms.Infrastructure.Persistence #region Constructors - /// - /// Initializes a new instance of the . - /// - /// Used by core runtime. - public UmbracoDatabaseFactory(ILogger logger, ILoggerFactory loggerFactory, IOptions globalSettings, IOptions connectionStrings, Lazy mappers,IDbProviderFactoryCreator dbProviderFactoryCreator, DatabaseSchemaCreatorFactory databaseSchemaCreatorFactory) - : this(logger, loggerFactory, globalSettings.Value, connectionStrings.Value, mappers, dbProviderFactoryCreator, databaseSchemaCreatorFactory) - { - - } - /// /// Initializes a new instance of the . /// /// Used by the other ctor and in tests. - public UmbracoDatabaseFactory(ILogger logger, ILoggerFactory loggerFactory, GlobalSettings globalSettings, ConnectionStrings connectionStrings, Lazy mappers, IDbProviderFactoryCreator dbProviderFactoryCreator, DatabaseSchemaCreatorFactory databaseSchemaCreatorFactory) + public UmbracoDatabaseFactory( + ILogger logger, + ILoggerFactory loggerFactory, + IOptions globalSettings, + IOptions connectionStrings, + Lazy mappers, + IDbProviderFactoryCreator dbProviderFactoryCreator, + DatabaseSchemaCreatorFactory databaseSchemaCreatorFactory) { _globalSettings = globalSettings; @@ -94,7 +91,7 @@ namespace Umbraco.Cms.Infrastructure.Persistence _logger = logger ?? throw new ArgumentNullException(nameof(logger)); _loggerFactory = loggerFactory; - var settings = connectionStrings.UmbracoConnectionString; + var settings = connectionStrings.Value.UmbracoConnectionString; if (settings == null) { @@ -166,7 +163,7 @@ namespace Umbraco.Cms.Infrastructure.Persistence { // replace NPoco database type by a more efficient one - var setting = _globalSettings.DatabaseFactoryServerVersion; + var setting = _globalSettings.Value.DatabaseFactoryServerVersion; var fromSettings = false; if (setting.IsNullOrWhiteSpace() || !setting.StartsWith("SqlServer.") diff --git a/src/Umbraco.Infrastructure/PropertyEditors/MultiUrlPickerValueEditor.cs b/src/Umbraco.Infrastructure/PropertyEditors/MultiUrlPickerValueEditor.cs index 02b268682d..f1e6a16bd4 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/MultiUrlPickerValueEditor.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/MultiUrlPickerValueEditor.cs @@ -143,6 +143,10 @@ namespace Umbraco.Cms.Core.PropertyEditors return base.ToEditor(property, culture, segment); } + private static readonly JsonSerializerSettings LinkDisplayJsonSerializerSettings = new JsonSerializerSettings + { + NullValueHandling = NullValueHandling.Ignore + }; public override object FromEditor(ContentPropertyData editorValue, object currentValue) { @@ -164,11 +168,8 @@ namespace Umbraco.Cms.Core.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) { diff --git a/src/Umbraco.Infrastructure/PropertyEditors/TagsPropertyEditor.cs b/src/Umbraco.Infrastructure/PropertyEditors/TagsPropertyEditor.cs index a849289feb..958cd43d7b 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/TagsPropertyEditor.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/TagsPropertyEditor.cs @@ -73,7 +73,7 @@ namespace Umbraco.Cms.Core.PropertyEditors if (string.IsNullOrWhiteSpace(value) == false) { - return value.Split(new[] { "," }, StringSplitOptions.RemoveEmptyEntries); + return value.Split(Constants.CharArrays.Comma, StringSplitOptions.RemoveEmptyEntries); } return null; diff --git a/src/Umbraco.Infrastructure/PropertyEditors/UploadFileTypeValidator.cs b/src/Umbraco.Infrastructure/PropertyEditors/UploadFileTypeValidator.cs index c8fdc06a42..afa4f48249 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/UploadFileTypeValidator.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/UploadFileTypeValidator.cs @@ -39,7 +39,7 @@ namespace Umbraco.Cms.Core.PropertyEditors yield break; } - var fileNames = selectedFiles?.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries); + var fileNames = selectedFiles?.Split(Constants.CharArrays.Comma, StringSplitOptions.RemoveEmptyEntries); if (fileNames == null || !fileNames.Any()) yield break; diff --git a/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/ImageCropperValueConverter.cs b/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/ImageCropperValueConverter.cs index 20f44ae433..6b3b7e68cb 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/ImageCropperValueConverter.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/ImageCropperValueConverter.cs @@ -35,6 +35,12 @@ namespace Umbraco.Cms.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) { @@ -44,11 +50,7 @@ namespace Umbraco.Cms.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) { diff --git a/src/Umbraco.Infrastructure/Runtime/CoreRuntime.cs b/src/Umbraco.Infrastructure/Runtime/CoreRuntime.cs index 4c05f56d5c..dab1fe662b 100644 --- a/src/Umbraco.Infrastructure/Runtime/CoreRuntime.cs +++ b/src/Umbraco.Infrastructure/Runtime/CoreRuntime.cs @@ -4,11 +4,15 @@ using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Composing; +using Umbraco.Cms.Core.Configuration; using Umbraco.Cms.Core.Events; +using Umbraco.Cms.Core.Exceptions; using Umbraco.Cms.Core.Hosting; using Umbraco.Cms.Core.Logging; using Umbraco.Cms.Core.Runtime; using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Infrastructure.Migrations.Install; +using Umbraco.Cms.Infrastructure.Migrations.Upgrade; using Umbraco.Cms.Infrastructure.Persistence; using Constants = Umbraco.Cms.Core.Constants; @@ -25,6 +29,8 @@ namespace Umbraco.Cms.Infrastructure.Runtime private readonly IUmbracoDatabaseFactory _databaseFactory; private readonly IEventAggregator _eventAggregator; private readonly IHostingEnvironment _hostingEnvironment; + private readonly DatabaseBuilder _databaseBuilder; + private readonly IUmbracoVersion _umbracoVersion; /// /// Initializes a new instance of the class. @@ -38,7 +44,9 @@ namespace Umbraco.Cms.Infrastructure.Runtime IMainDom mainDom, IUmbracoDatabaseFactory databaseFactory, IEventAggregator eventAggregator, - IHostingEnvironment hostingEnvironment) + IHostingEnvironment hostingEnvironment, + DatabaseBuilder databaseBuilder, + IUmbracoVersion umbracoVersion) { State = state; _loggerFactory = loggerFactory; @@ -49,6 +57,8 @@ namespace Umbraco.Cms.Infrastructure.Runtime _databaseFactory = databaseFactory; _eventAggregator = eventAggregator; _hostingEnvironment = hostingEnvironment; + _databaseBuilder = databaseBuilder; + _umbracoVersion = umbracoVersion; _logger = _loggerFactory.CreateLogger(); } @@ -100,10 +110,33 @@ namespace Umbraco.Cms.Infrastructure.Runtime await _eventAggregator.PublishAsync(new UmbracoApplicationStarting(State.Level), cancellationToken); + // if level is Updrade and reason is UpgradeMigrations, that means we need to perform an unattended upgrade + if (State.Reason == RuntimeLevelReason.UpgradeMigrations && State.Level == RuntimeLevel.Upgrade) + { + // do the upgrade + DoUnattendedUpgrade(); + + // upgrade is done, set reason to Run + DetermineRuntimeLevel(); + + } + // create & initialize the components _components.Initialize(); } + private void DoUnattendedUpgrade() + { + var plan = new UmbracoPlan(_umbracoVersion); + using (_profilingLogger.TraceDuration("Starting unattended upgrade.", "Unattended upgrade completed.")) + { + var result = _databaseBuilder.UpgradeSchemaAndData(plan); + if (result.Success == false) + throw new UnattendedInstallException("An error occurred while running the unattended upgrade.\n" + result.Message); + } + + } + private void DoUnattendedInstall() { State.DoUnattendedInstall(); diff --git a/src/Umbraco.Infrastructure/Runtime/SqlMainDomLock.cs b/src/Umbraco.Infrastructure/Runtime/SqlMainDomLock.cs index e8f5072e18..0ecc0f3155 100644 --- a/src/Umbraco.Infrastructure/Runtime/SqlMainDomLock.cs +++ b/src/Umbraco.Infrastructure/Runtime/SqlMainDomLock.cs @@ -7,6 +7,7 @@ using System.Security.Cryptography; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; using NPoco; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Configuration.Models; @@ -28,25 +29,28 @@ namespace Umbraco.Cms.Infrastructure.Runtime private const string MainDomKeyPrefix = "Umbraco.Core.Runtime.SqlMainDom"; private const string UpdatedSuffix = "_updated"; private readonly ILogger _logger; + private readonly IOptions _globalSettings; private readonly IHostingEnvironment _hostingEnvironment; private IUmbracoDatabase _db; private CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource(); - private SqlServerSyntaxProvider _sqlServerSyntax = new SqlServerSyntaxProvider(); + private SqlServerSyntaxProvider _sqlServerSyntax; private bool _mainDomChanging = false; private readonly UmbracoDatabaseFactory _dbFactory; private bool _errorDuringAcquiring; private object _locker = new object(); private bool _hasTable = false; - public SqlMainDomLock(ILogger logger, ILoggerFactory loggerFactory, GlobalSettings globalSettings, ConnectionStrings connectionStrings, IDbProviderFactoryCreator dbProviderFactoryCreator, IHostingEnvironment hostingEnvironment, DatabaseSchemaCreatorFactory databaseSchemaCreatorFactory) + public SqlMainDomLock(ILogger logger, ILoggerFactory loggerFactory, IOptions globalSettings, IOptions connectionStrings, IDbProviderFactoryCreator dbProviderFactoryCreator, IHostingEnvironment hostingEnvironment, DatabaseSchemaCreatorFactory databaseSchemaCreatorFactory) { // unique id for our appdomain, this is more unique than the appdomain id which is just an INT counter to its safer _lockId = Guid.NewGuid().ToString(); _logger = logger; -_hostingEnvironment = hostingEnvironment; + _globalSettings = globalSettings; + _sqlServerSyntax = new SqlServerSyntaxProvider(_globalSettings); + _hostingEnvironment = hostingEnvironment; _dbFactory = new UmbracoDatabaseFactory(loggerFactory.CreateLogger(), loggerFactory, - globalSettings, + _globalSettings, connectionStrings, new Lazy(() => new MapperCollection(Enumerable.Empty())), dbProviderFactoryCreator, diff --git a/src/Umbraco.Infrastructure/RuntimeState.cs b/src/Umbraco.Infrastructure/RuntimeState.cs index b62c30e4d2..4d4df20ab9 100644 --- a/src/Umbraco.Infrastructure/RuntimeState.cs +++ b/src/Umbraco.Infrastructure/RuntimeState.cs @@ -27,6 +27,7 @@ namespace Umbraco.Cms.Core /// /// The initial + /// The initial /// public static RuntimeState Booting() => new RuntimeState() { Level = RuntimeLevel.Boot }; diff --git a/src/Umbraco.Infrastructure/Scoping/IScope.cs b/src/Umbraco.Infrastructure/Scoping/IScope.cs index 7a6a62a6c7..4f55988d2f 100644 --- a/src/Umbraco.Infrastructure/Scoping/IScope.cs +++ b/src/Umbraco.Infrastructure/Scoping/IScope.cs @@ -58,5 +58,19 @@ namespace Umbraco.Cms.Core.Scoping /// /// The lock object identifiers. void WriteLock(params int[] lockIds); + + /// + /// 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); } } diff --git a/src/Umbraco.Infrastructure/Scoping/Scope.cs b/src/Umbraco.Infrastructure/Scoping/Scope.cs index 7d50f5e55a..b49a7a5676 100644 --- a/src/Umbraco.Infrastructure/Scoping/Scope.cs +++ b/src/Umbraco.Infrastructure/Scoping/Scope.cs @@ -511,7 +511,13 @@ namespace Umbraco.Cms.Core.Scoping /// public void ReadLock(params int[] lockIds) => Database.SqlContext.SqlSyntax.ReadLock(Database, lockIds); + /// + public void ReadLock(TimeSpan timeout, int lockId) => Database.SqlContext.SqlSyntax.ReadLock(Database, timeout, lockId); + /// public void WriteLock(params int[] lockIds) => Database.SqlContext.SqlSyntax.WriteLock(Database, lockIds); + + /// + public void WriteLock(TimeSpan timeout, int lockId) => Database.SqlContext.SqlSyntax.WriteLock(Database, timeout, lockId); } } diff --git a/src/Umbraco.Infrastructure/Search/ExamineComponent.cs b/src/Umbraco.Infrastructure/Search/ExamineComponent.cs index 1eb1d3bc29..b0167f3b58 100644 --- a/src/Umbraco.Infrastructure/Search/ExamineComponent.cs +++ b/src/Umbraco.Infrastructure/Search/ExamineComponent.cs @@ -255,10 +255,20 @@ namespace Umbraco.Cms.Infrastructure.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: @@ -614,24 +624,28 @@ namespace Umbraco.Cms.Infrastructure.Search { taskHelper.RunBackgroundTask(async () => { - // 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); + } } }); - } } @@ -663,14 +677,19 @@ namespace Umbraco.Cms.Infrastructure.Search // perform the ValueSet lookup on a background thread taskHelper.RunBackgroundTask(async () => { - 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); + } } }); } @@ -702,12 +721,17 @@ namespace Umbraco.Cms.Infrastructure.Search // perform the ValueSet lookup on a background thread taskHelper.RunBackgroundTask(async () => { - 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.Infrastructure/Security/BackOfficeClaimsPrincipalFactory.cs b/src/Umbraco.Infrastructure/Security/BackOfficeClaimsPrincipalFactory.cs index 505052b514..2bb9b1ab8d 100644 --- a/src/Umbraco.Infrastructure/Security/BackOfficeClaimsPrincipalFactory.cs +++ b/src/Umbraco.Infrastructure/Security/BackOfficeClaimsPrincipalFactory.cs @@ -36,12 +36,6 @@ namespace Umbraco.Cms.Core.Security ClaimsIdentity baseIdentity = await base.GenerateClaimsAsync(user); - // now we can flow any custom claims that the actual user has currently assigned which could be done in the OnExternalLogin callback - foreach (IdentityUserClaim claim in user.Claims) - { - baseIdentity.AddClaim(new Claim(claim.ClaimType, claim.ClaimValue)); - } - baseIdentity.AddRequiredClaims( user.Id, user.UserName, @@ -53,6 +47,10 @@ namespace Umbraco.Cms.Core.Security 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 + baseIdentity.MergeClaimsFromBackOfficeIdentity(user); + return new ClaimsPrincipal(baseIdentity); } diff --git a/src/Umbraco.Infrastructure/Security/BackOfficeUserStore.cs b/src/Umbraco.Infrastructure/Security/BackOfficeUserStore.cs index 41f7f94113..bd05ce0461 100644 --- a/src/Umbraco.Infrastructure/Security/BackOfficeUserStore.cs +++ b/src/Umbraco.Infrastructure/Security/BackOfficeUserStore.cs @@ -9,6 +9,7 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.Options; using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Mapping; using Umbraco.Cms.Core.Models; @@ -33,6 +34,7 @@ namespace Umbraco.Cms.Core.Security private readonly IExternalLoginService _externalLoginService; private readonly GlobalSettings _globalSettings; private readonly UmbracoMapper _mapper; + private readonly AppCaches _appCaches; /// /// Initializes a new instance of the class. @@ -44,7 +46,8 @@ namespace Umbraco.Cms.Core.Security IExternalLoginService externalLoginService, IOptions globalSettings, UmbracoMapper mapper, - IdentityErrorDescriber describer) + IdentityErrorDescriber describer, + AppCaches appCaches) : base(describer) { _scopeProvider = scopeProvider; @@ -53,6 +56,7 @@ namespace Umbraco.Cms.Core.Security _externalLoginService = externalLoginService ?? throw new ArgumentNullException(nameof(externalLoginService)); _globalSettings = globalSettings.Value; _mapper = mapper; + _appCaches = appCaches; _userService = userService; _externalLoginService = externalLoginService; } @@ -685,8 +689,8 @@ namespace Umbraco.Cms.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.Infrastructure/Security/ClaimsIdentityExtensions.cs b/src/Umbraco.Infrastructure/Security/ClaimsIdentityExtensions.cs new file mode 100644 index 0000000000..c9a57786af --- /dev/null +++ b/src/Umbraco.Infrastructure/Security/ClaimsIdentityExtensions.cs @@ -0,0 +1,37 @@ +// Copyright (c) Umbraco. +// See LICENSE for more details. +using System.Linq; +using System.Security.Claims; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Security; + +namespace Umbraco.Extensions +{ + internal static class MergeClaimsIdentityExtensions + { + + // 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.Infrastructure/Security/IdentityMapDefinition.cs b/src/Umbraco.Infrastructure/Security/IdentityMapDefinition.cs index 1a76dec2d5..30d435f345 100644 --- a/src/Umbraco.Infrastructure/Security/IdentityMapDefinition.cs +++ b/src/Umbraco.Infrastructure/Security/IdentityMapDefinition.cs @@ -3,7 +3,7 @@ using System; using Microsoft.Extensions.Options; -using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Mapping; using Umbraco.Cms.Core.Models; @@ -17,13 +17,19 @@ namespace Umbraco.Cms.Core.Security { private readonly ILocalizedTextService _textService; private readonly IEntityService _entityService; - private readonly GlobalSettings _globalSettings; + private readonly IOptions _globalSettings; + private readonly AppCaches _appCaches; - public IdentityMapDefinition(ILocalizedTextService textService, IEntityService entityService, IOptions globalSettings) + public IdentityMapDefinition( + ILocalizedTextService textService, + IEntityService entityService, + IOptions globalSettings, + AppCaches appCaches) { _textService = textService; _entityService = entityService; - _globalSettings = globalSettings.Value; + _globalSettings = globalSettings; + _appCaches = appCaches; } public void DefineMaps(UmbracoMapper mapper) @@ -31,7 +37,7 @@ namespace Umbraco.Cms.Core.Security mapper.Define( (source, context) => { - var target = new BackOfficeIdentityUser(_globalSettings, source.Id, source.Groups); + var target = new BackOfficeIdentityUser(_globalSettings.Value, source.Id, source.Groups); target.DisableChangeTracking(); return target; }, @@ -67,8 +73,8 @@ namespace Umbraco.Cms.Core.Security 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(); @@ -80,7 +86,7 @@ namespace Umbraco.Cms.Core.Security target.PasswordConfig = source.PasswordConfiguration; target.StartContentIds = source.StartContentIds; target.StartMediaIds = source.StartMediaIds; - target.Culture = source.GetUserCulture(_textService, _globalSettings).ToString(); // project CultureInfo to string + target.Culture = source.GetUserCulture(_textService, _globalSettings.Value).ToString(); // project CultureInfo to string target.IsApproved = source.IsApproved; target.SecurityStamp = source.SecurityStamp; target.LockoutEnd = source.IsLockedOut ? DateTime.MaxValue.ToUniversalTime() : (DateTime?)null; diff --git a/src/Umbraco.Infrastructure/Serialization/NoTypeConverterJsonConverter.cs b/src/Umbraco.Infrastructure/Serialization/NoTypeConverterJsonConverter.cs index ebdc84b39a..0b5c9acc49 100644 --- a/src/Umbraco.Infrastructure/Serialization/NoTypeConverterJsonConverter.cs +++ b/src/Umbraco.Infrastructure/Serialization/NoTypeConverterJsonConverter.cs @@ -19,6 +19,7 @@ namespace Umbraco.Cms.Infrastructure.Serialization public 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.Cms.Infrastructure.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.Infrastructure/Services/Implement/ContentService.cs b/src/Umbraco.Infrastructure/Services/Implement/ContentService.cs index 017540ae3f..b6e91c717d 100644 --- a/src/Umbraco.Infrastructure/Services/Implement/ContentService.cs +++ b/src/Umbraco.Infrastructure/Services/Implement/ContentService.cs @@ -512,7 +512,7 @@ namespace Umbraco.Cms.Core.Services.Implement if (content.Path.IsNullOrWhiteSpace()) return Enumerable.Empty(); var rootId = Cms.Core.Constants.System.RootString; - var ids = content.Path.Split(',') + var ids = content.Path.Split(Constants.CharArrays.Comma) .Where(x => x != rootId && x != content.Id.ToString(CultureInfo.InvariantCulture)).Select(int.Parse).ToArray(); if (ids.Any() == false) return new List(); diff --git a/src/Umbraco.Infrastructure/Services/Implement/ContentTypeServiceBaseOfTRepositoryTItemTService.cs b/src/Umbraco.Infrastructure/Services/Implement/ContentTypeServiceBaseOfTRepositoryTItemTService.cs index b23887fb18..d5c051d0a7 100644 --- a/src/Umbraco.Infrastructure/Services/Implement/ContentTypeServiceBaseOfTRepositoryTItemTService.cs +++ b/src/Umbraco.Infrastructure/Services/Implement/ContentTypeServiceBaseOfTRepositoryTItemTService.cs @@ -877,7 +877,7 @@ namespace Umbraco.Cms.Core.Services.Implement public IEnumerable GetContainers(TItem item) { - var ancestorIds = item.Path.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries) + var ancestorIds = item.Path.Split(Constants.CharArrays.Comma, StringSplitOptions.RemoveEmptyEntries) .Select(x => { var asInt = x.TryConvertTo(); diff --git a/src/Umbraco.Infrastructure/Services/Implement/DataTypeService.cs b/src/Umbraco.Infrastructure/Services/Implement/DataTypeService.cs index dacaa7e228..640fa50cc6 100644 --- a/src/Umbraco.Infrastructure/Services/Implement/DataTypeService.cs +++ b/src/Umbraco.Infrastructure/Services/Implement/DataTypeService.cs @@ -115,7 +115,7 @@ namespace Umbraco.Cms.Core.Services.Implement public IEnumerable GetContainers(IDataType dataType) { - var ancestorIds = dataType.Path.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries) + var ancestorIds = dataType.Path.Split(Constants.CharArrays.Comma, StringSplitOptions.RemoveEmptyEntries) .Select(x => { var asInt = x.TryConvertTo(); diff --git a/src/Umbraco.Infrastructure/Services/Implement/EntityXmlSerializer.cs b/src/Umbraco.Infrastructure/Services/Implement/EntityXmlSerializer.cs index bc8b4f25ce..b63040a4a0 100644 --- a/src/Umbraco.Infrastructure/Services/Implement/EntityXmlSerializer.cs +++ b/src/Umbraco.Infrastructure/Services/Implement/EntityXmlSerializer.cs @@ -564,6 +564,13 @@ namespace Umbraco.Cms.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.Infrastructure/Services/Implement/MediaService.cs b/src/Umbraco.Infrastructure/Services/Implement/MediaService.cs index 60061ed9bf..4b9d6f8e5c 100644 --- a/src/Umbraco.Infrastructure/Services/Implement/MediaService.cs +++ b/src/Umbraco.Infrastructure/Services/Implement/MediaService.cs @@ -482,7 +482,7 @@ namespace Umbraco.Cms.Core.Services.Implement if (media.Path.IsNullOrWhiteSpace()) return Enumerable.Empty(); var rootId = Cms.Core.Constants.System.RootString; - var ids = media.Path.Split(',') + var ids = media.Path.Split(Constants.CharArrays.Comma) .Where(x => x != rootId && x != media.Id.ToString(CultureInfo.InvariantCulture)) .Select(int.Parse) .ToArray(); diff --git a/src/Umbraco.Infrastructure/Services/Implement/NotificationService.cs b/src/Umbraco.Infrastructure/Services/Implement/NotificationService.cs index e91aa8ce33..aedad0e56b 100644 --- a/src/Umbraco.Infrastructure/Services/Implement/NotificationService.cs +++ b/src/Umbraco.Infrastructure/Services/Implement/NotificationService.cs @@ -82,7 +82,7 @@ namespace Umbraco.Cms.Core.Services.Implement if (entitiesL.Count == 0) return; //put all entity's paths into a list with the same indices - var paths = entitiesL.Select(x => x.Path.Split(',').Select(int.Parse).ToArray()).ToArray(); + var paths = entitiesL.Select(x => x.Path.Split(Constants.CharArrays.Comma).Select(int.Parse).ToArray()).ToArray(); // lazily get versions var prevVersionDictionary = new Dictionary(); @@ -180,7 +180,7 @@ namespace Umbraco.Cms.Core.Services.Implement /// public IEnumerable FilterUserNotificationsByPath(IEnumerable userNotifications, string path) { - var pathParts = path.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries); + var pathParts = path.Split(Constants.CharArrays.Comma, StringSplitOptions.RemoveEmptyEntries); return userNotifications.Where(r => pathParts.InvariantContains(r.EntityId.ToString(CultureInfo.InvariantCulture))).ToList(); } diff --git a/src/Umbraco.Infrastructure/Services/Implement/PublicAccessService.cs b/src/Umbraco.Infrastructure/Services/Implement/PublicAccessService.cs index 4c8615f442..19df11e798 100644 --- a/src/Umbraco.Infrastructure/Services/Implement/PublicAccessService.cs +++ b/src/Umbraco.Infrastructure/Services/Implement/PublicAccessService.cs @@ -55,7 +55,7 @@ namespace Umbraco.Cms.Core.Services.Implement { //Get all ids in the path for the content item and ensure they all // parse to ints that are not -1. - var ids = contentPath.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries) + var ids = contentPath.Split(Constants.CharArrays.Comma, StringSplitOptions.RemoveEmptyEntries) .Select(x => int.TryParse(x, out int val) ? val : -1) .Where(x => x != -1) .ToList(); @@ -63,12 +63,10 @@ namespace Umbraco.Cms.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.Persistence.SqlCe/SqlCeSyntaxProvider.cs b/src/Umbraco.Persistence.SqlCe/SqlCeSyntaxProvider.cs index c46b2ef6f2..60e456a651 100644 --- a/src/Umbraco.Persistence.SqlCe/SqlCeSyntaxProvider.cs +++ b/src/Umbraco.Persistence.SqlCe/SqlCeSyntaxProvider.cs @@ -2,7 +2,9 @@ using System.Collections.Generic; using System.Data; using System.Linq; +using Microsoft.Extensions.Options; using NPoco; +using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Infrastructure.Persistence; using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; using Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; @@ -17,6 +19,13 @@ namespace Umbraco.Cms.Persistence.SqlCe /// public class SqlCeSyntaxProvider : MicrosoftSqlSyntaxProviderBase { + private readonly IOptions _globalSettings; + + public SqlCeSyntaxProvider(IOptions globalSettings) + { + _globalSettings = globalSettings; + } + public override string ProviderName => Constants.DatabaseProviders.SqlCe; public override Sql SelectTop(Sql sql, int top) @@ -89,7 +98,7 @@ namespace Umbraco.Cms.Persistence.SqlCe string columns = string.IsNullOrEmpty(columnDefinition.PrimaryKeyColumns) ? GetQuotedColumnName(columnDefinition.Name) : string.Join(", ", columnDefinition.PrimaryKeyColumns - .Split(new[]{',', ' '}, StringSplitOptions.RemoveEmptyEntries) + .Split(Constants.CharArrays.CommaSpace, StringSplitOptions.RemoveEmptyEntries) .Select(GetQuotedColumnName)); return string.Format(CreateConstraint, @@ -162,6 +171,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 @@ -169,16 +188,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."); - db.Execute(@"SET LOCK_TIMEOUT 1800;"); - // *not* using a unique 'WHERE IN' query here because the *order* of lockIds is important to avoid deadlocks + var timeout = _globalSettings.Value.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 @@ -186,15 +221,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.PublishedCache.NuCache/ContentCache.cs b/src/Umbraco.PublishedCache.NuCache/ContentCache.cs index e4c43b1067..5428279655 100644 --- a/src/Umbraco.PublishedCache.NuCache/ContentCache.cs +++ b/src/Umbraco.PublishedCache.NuCache/ContentCache.cs @@ -83,7 +83,7 @@ namespace Umbraco.Cms.Infrastructure.PublishedCache var pos = route.IndexOf('/'); var path = pos == 0 ? route : route.Substring(pos); var startNodeId = pos == 0 ? 0 : int.Parse(route.Substring(0, pos)); - var parts = path.Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries); + var parts = path.Split(Constants.CharArrays.ForwardSlash, StringSplitOptions.RemoveEmptyEntries); IPublishedContent content; diff --git a/src/Umbraco.PublishedCache.NuCache/Persistence/NuCacheContentRepository.cs b/src/Umbraco.PublishedCache.NuCache/Persistence/NuCacheContentRepository.cs index 00346233ba..bfde5c07bc 100644 --- a/src/Umbraco.PublishedCache.NuCache/Persistence/NuCacheContentRepository.cs +++ b/src/Umbraco.PublishedCache.NuCache/Persistence/NuCacheContentRepository.cs @@ -719,17 +719,18 @@ AND cmsContentNu.nodeId IS NULL 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.Tests.AcceptanceTest/cypress/integration/Content/content.ts b/src/Umbraco.Tests.AcceptanceTest/cypress/integration/Content/content.ts index ecfe3e95d8..e12ba5ef75 100644 --- a/src/Umbraco.Tests.AcceptanceTest/cypress/integration/Content/content.ts +++ b/src/Umbraco.Tests.AcceptanceTest/cypress/integration/Content/content.ts @@ -570,7 +570,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'); 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/ModelToSqlExpressionHelperBenchmarks.cs b/src/Umbraco.Tests.Benchmarks/ModelToSqlExpressionHelperBenchmarks.cs index 3a0cc8f66e..f54ab96255 100644 --- a/src/Umbraco.Tests.Benchmarks/ModelToSqlExpressionHelperBenchmarks.cs +++ b/src/Umbraco.Tests.Benchmarks/ModelToSqlExpressionHelperBenchmarks.cs @@ -1,7 +1,9 @@ using System; using System.Linq.Expressions; using BenchmarkDotNet.Attributes; +using Microsoft.Extensions.Options; using Moq; +using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Infrastructure.Persistence; using Umbraco.Cms.Infrastructure.Persistence.Mappers; @@ -17,7 +19,7 @@ namespace Umbraco.Tests.Benchmarks protected Lazy MockSqlContext() { var sqlContext = Mock.Of(); - var syntax = new SqlCeSyntaxProvider(); + var syntax = new SqlCeSyntaxProvider(Options.Create(new GlobalSettings())); Mock.Get(sqlContext).Setup(x => x.SqlSyntax).Returns(syntax); return new Lazy(() => sqlContext); } @@ -34,7 +36,7 @@ namespace Umbraco.Tests.Benchmarks _mapperCollection = mapperCollection.Object; } - private readonly ISqlSyntaxProvider _syntaxProvider = new SqlCeSyntaxProvider(); + private readonly ISqlSyntaxProvider _syntaxProvider = new SqlCeSyntaxProvider(Options.Create(new GlobalSettings())); private readonly CachedExpression _cachedExpression; private readonly IMapperCollection _mapperCollection; diff --git a/src/Umbraco.Tests.Benchmarks/SqlTemplatesBenchmark.cs b/src/Umbraco.Tests.Benchmarks/SqlTemplatesBenchmark.cs index 286307aa17..89ada16387 100644 --- a/src/Umbraco.Tests.Benchmarks/SqlTemplatesBenchmark.cs +++ b/src/Umbraco.Tests.Benchmarks/SqlTemplatesBenchmark.cs @@ -1,7 +1,9 @@ using BenchmarkDotNet.Attributes; using BenchmarkDotNet.Configs; using BenchmarkDotNet.Diagnosers; +using Microsoft.Extensions.Options; using NPoco; +using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Infrastructure.Persistence; using Umbraco.Cms.Infrastructure.Persistence.Mappers; using Umbraco.Cms.Persistence.SqlCe; @@ -34,7 +36,7 @@ namespace Umbraco.Tests.Benchmarks var mappers = new NPoco.MapperCollection { new PocoMapper() }; var factory = new FluentPocoDataFactory((type, iPocoDataFactory) => new PocoDataBuilder(type, mappers).Init()); - SqlContext = new SqlContext(new SqlCeSyntaxProvider(), DatabaseType.SQLCe, factory); + SqlContext = new SqlContext(new SqlCeSyntaxProvider(Options.Create(new GlobalSettings())), DatabaseType.SQLCe, factory); SqlTemplates = new SqlTemplates(SqlContext); } diff --git a/src/Umbraco.Tests.Benchmarks/Umbraco.Tests.Benchmarks.csproj b/src/Umbraco.Tests.Benchmarks/Umbraco.Tests.Benchmarks.csproj index fce12ded71..d1d88fc870 100644 --- a/src/Umbraco.Tests.Benchmarks/Umbraco.Tests.Benchmarks.csproj +++ b/src/Umbraco.Tests.Benchmarks/Umbraco.Tests.Benchmarks.csproj @@ -53,6 +53,7 @@ + diff --git a/src/Umbraco.Tests.Common/TestHelpers/TestDatabase.cs b/src/Umbraco.Tests.Common/TestHelpers/TestDatabase.cs index ecc11784b4..188c515bf0 100644 --- a/src/Umbraco.Tests.Common/TestHelpers/TestDatabase.cs +++ b/src/Umbraco.Tests.Common/TestHelpers/TestDatabase.cs @@ -8,10 +8,12 @@ using System.Data; using System.Data.Common; using System.Linq.Expressions; using System.Threading.Tasks; +using Microsoft.Extensions.Options; using Moq; using NPoco; using NPoco.DatabaseTypes; using NPoco.Linq; +using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Infrastructure.Migrations.Install; using Umbraco.Cms.Infrastructure.Persistence; using Umbraco.Cms.Infrastructure.Persistence.SqlSyntax; @@ -37,7 +39,7 @@ namespace Umbraco.Cms.Tests.Common.TestHelpers public TestDatabase(DatabaseType databaseType = null, ISqlSyntaxProvider syntaxProvider = null) { DatabaseType = databaseType ?? new SqlServerDatabaseType(); - SqlContext = new SqlContext(syntaxProvider ?? new SqlServerSyntaxProvider(), DatabaseType, Mock.Of()); + SqlContext = new SqlContext(syntaxProvider ?? new SqlServerSyntaxProvider(Options.Create((new GlobalSettings()))), DatabaseType, Mock.Of()); } /// diff --git a/src/Umbraco.Tests.Integration/Implementations/TestHelper.cs b/src/Umbraco.Tests.Integration/Implementations/TestHelper.cs index 484eea9b95..8e897011d2 100644 --- a/src/Umbraco.Tests.Integration/Implementations/TestHelper.cs +++ b/src/Umbraco.Tests.Integration/Implementations/TestHelper.cs @@ -121,7 +121,7 @@ namespace Umbraco.Cms.Tests.Integration.Implementations public IWebHostEnvironment GetWebHostEnvironment() => _hostEnvironment; public override IDbProviderFactoryCreator DbProviderFactoryCreator => - new SqlServerDbProviderFactoryCreator(DbProviderFactories.GetFactory); + new SqlServerDbProviderFactoryCreator(DbProviderFactories.GetFactory, Options.Create(new GlobalSettings())); public override IBulkSqlInsertProvider BulkSqlInsertProvider => new SqlServerBulkSqlInsertProvider(); diff --git a/src/Umbraco.Tests.Integration/Testing/TestUmbracoDatabaseFactoryProvider.cs b/src/Umbraco.Tests.Integration/Testing/TestUmbracoDatabaseFactoryProvider.cs index cba2c51a30..b53e55a323 100644 --- a/src/Umbraco.Tests.Integration/Testing/TestUmbracoDatabaseFactoryProvider.cs +++ b/src/Umbraco.Tests.Integration/Testing/TestUmbracoDatabaseFactoryProvider.cs @@ -46,8 +46,8 @@ namespace Umbraco.Cms.Tests.Integration.Testing return new UmbracoDatabaseFactory( _loggerFactory.CreateLogger(), _loggerFactory, - _globalSettings.Value, - _connectionStrings.Value, + _globalSettings, + _connectionStrings, _mappers, _dbProviderFactoryCreator, _databaseSchemaCreatorFactory); diff --git a/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/LocksTests.cs b/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/LocksTests.cs index 592de6a89c..518a3912c5 100644 --- a/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/LocksTests.cs +++ b/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/LocksTests.cs @@ -4,9 +4,11 @@ using System.Data.SqlClient; using System.Linq; using System.Text; using System.Threading; +using System.Threading.Tasks; using NPoco; using NUnit.Framework; using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Infrastructure.Persistence.Dtos; using Umbraco.Cms.Tests.Common.Testing; using Umbraco.Cms.Tests.Integration.Testing; @@ -291,6 +293,152 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence Assert.IsNull(e2); } + [Test] + public void Throws_When_Lock_Timeout_Is_Exceeded() + { + var t1 = Task.Run(() => + { + using (var scope = ScopeProvider.CreateScope()) + { + var realScope = (Scope)scope; + + Console.WriteLine("Write lock A"); + // This will acquire right away + realScope.WriteLock(TimeSpan.FromMilliseconds(2000), Constants.Locks.ContentTree); + Thread.Sleep(6000); // Wait longer than the Read Lock B timeout + scope.Complete(); + Console.WriteLine("Finished Write lock A"); + } + }); + + Thread.Sleep(500); // 100% sure task 1 starts first + + var t2 = Task.Run(() => + { + using (var scope = ScopeProvider.CreateScope()) + { + var realScope = (Scope)scope; + + Console.WriteLine("Read lock B"); + + // This will wait for the write lock to release but it isn't going to wait long + // enough so an exception will be thrown. + Assert.Throws(() => + realScope.ReadLock(TimeSpan.FromMilliseconds(3000), Constants.Locks.ContentTree)); + + scope.Complete(); + Console.WriteLine("Finished Read lock B"); + } + }); + + var t3 = Task.Run(() => + { + using (var scope = ScopeProvider.CreateScope()) + { + var realScope = (Scope)scope; + + Console.WriteLine("Write lock C"); + + // This will wait for the write lock to release but it isn't going to wait long + // enough so an exception will be thrown. + Assert.Throws(() => + realScope.WriteLock(TimeSpan.FromMilliseconds(3000), Constants.Locks.ContentTree)); + + scope.Complete(); + Console.WriteLine("Finished Write lock C"); + } + }); + + Task.WaitAll(t1, t2, t3); + } + + [Test] + public void Read_Lock_Waits_For_Write_Lock() + { + var locksCompleted = 0; + + var t1 = Task.Run(() => + { + using (var scope = ScopeProvider.CreateScope()) + { + var realScope = (Scope)scope; + + Console.WriteLine("Write lock A"); + // This will acquire right away + realScope.WriteLock(TimeSpan.FromMilliseconds(2000), Constants.Locks.ContentTree); + Thread.Sleep(4000); // Wait less than the Read Lock B timeout + scope.Complete(); + Interlocked.Increment(ref locksCompleted); + Console.WriteLine("Finished Write lock A"); + } + }); + + Thread.Sleep(500); // 100% sure task 1 starts first + + var t2 = Task.Run(() => + { + using (var scope = ScopeProvider.CreateScope()) + { + var realScope = (Scope)scope; + + Console.WriteLine("Read lock B"); + + // This will wait for the write lock to release + Assert.DoesNotThrow(() => + realScope.ReadLock(TimeSpan.FromMilliseconds(6000), Constants.Locks.ContentTree)); + + Assert.GreaterOrEqual(locksCompleted, 1); + + scope.Complete(); + Interlocked.Increment(ref locksCompleted); + Console.WriteLine("Finished Read lock B"); + } + }); + + var t3 = Task.Run(() => + { + using (var scope = ScopeProvider.CreateScope()) + { + var realScope = (Scope)scope; + + Console.WriteLine("Read lock C"); + + // This will wait for the write lock to release + Assert.DoesNotThrow(() => + realScope.ReadLock(TimeSpan.FromMilliseconds(6000), Constants.Locks.ContentTree)); + + Assert.GreaterOrEqual(locksCompleted, 1); + + scope.Complete(); + Interlocked.Increment(ref locksCompleted); + Console.WriteLine("Finished Read lock C"); + } + }); + + Task.WaitAll(t1, t2, t3); + + Assert.AreEqual(3, locksCompleted); + } + + [Test] + [NUnit.Framework.Ignore("We cannot run this test with SQLCE because it does not support a Command Timeout")] + public void Lock_Exceeds_Command_Timeout() + { + using (var scope = ScopeProvider.CreateScope()) + { + var realScope = (Scope)scope; + + var realDb = (Database)realScope.Database; + realDb.CommandTimeout = 1000; + + Console.WriteLine("Write lock A"); + // TODO: In theory this would throw + realScope.WriteLock(TimeSpan.FromMilliseconds(3000), Constants.Locks.ContentTree); + scope.Complete(); + Console.WriteLine("Finished Write lock A"); + } + } + private void NoDeadLockTestThread(int id, EventWaitHandle myEv, WaitHandle otherEv, ref Exception exception) { using (var scope = ScopeProvider.CreateScope()) diff --git a/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/DocumentRepositoryTest.cs b/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/DocumentRepositoryTest.cs index d109afd944..366e8b067d 100644 --- a/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/DocumentRepositoryTest.cs +++ b/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/DocumentRepositoryTest.cs @@ -747,7 +747,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence.Repos [Test] public void AliasRegexTest() { - System.Text.RegularExpressions.Regex regex = new SqlServerSyntaxProvider().AliasRegex; + System.Text.RegularExpressions.Regex regex = new SqlServerSyntaxProvider(Options.Create(new GlobalSettings())).AliasRegex; Assert.AreEqual(@"(\[\w+]\.\[\w+])\s+AS\s+(\[\w+])", regex.ToString()); const string sql = "SELECT [table].[column1] AS [alias1], [table].[column2] AS [alias2] FROM [table];"; System.Text.RegularExpressions.MatchCollection matches = regex.Matches(sql); diff --git a/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/SyntaxProvider/SqlServerSyntaxProviderTests.cs b/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/SyntaxProvider/SqlServerSyntaxProviderTests.cs index 848792a2d1..800b702888 100644 --- a/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/SyntaxProvider/SqlServerSyntaxProviderTests.cs +++ b/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/SyntaxProvider/SqlServerSyntaxProviderTests.cs @@ -1,9 +1,11 @@ using System; using System.Diagnostics; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; using Moq; using NPoco; using NUnit.Framework; +using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Infrastructure.Migrations; using Umbraco.Cms.Infrastructure.Migrations.Expressions.Common.Expressions; using Umbraco.Cms.Infrastructure.Migrations.Expressions.Create.Index; @@ -78,7 +80,7 @@ WHERE (([umbracoNode].[nodeObjectType] = @0))) x)".Replace(Environment.NewLine, [Test] public void Format_SqlServer_NonClusteredIndexDefinition_AddsNonClusteredDirective() { - var sqlSyntax = new SqlServerSyntaxProvider(); + var sqlSyntax = new SqlServerSyntaxProvider(Options.Create(new GlobalSettings())); var indexDefinition = CreateIndexDefinition(); indexDefinition.IndexType = IndexTypes.NonClustered; @@ -90,7 +92,7 @@ WHERE (([umbracoNode].[nodeObjectType] = @0))) x)".Replace(Environment.NewLine, [Test] public void Format_SqlServer_NonClusteredIndexDefinition_UsingIsClusteredFalse_AddsClusteredDirective() { - var sqlSyntax = new SqlServerSyntaxProvider(); + var sqlSyntax = new SqlServerSyntaxProvider(Options.Create(new GlobalSettings())); var indexDefinition = CreateIndexDefinition(); indexDefinition.IndexType = IndexTypes.Clustered; @@ -103,7 +105,7 @@ WHERE (([umbracoNode].[nodeObjectType] = @0))) x)".Replace(Environment.NewLine, public void CreateIndexBuilder_SqlServer_NonClustered_CreatesNonClusteredIndex() { var logger = Mock.Of>(); - var sqlSyntax = new SqlServerSyntaxProvider(); + var sqlSyntax = new SqlServerSyntaxProvider(Options.Create(new GlobalSettings())); var db = new TestDatabase(DatabaseType.SqlServer2005, sqlSyntax); var context = new MigrationContext(db, logger); @@ -124,7 +126,7 @@ WHERE (([umbracoNode].[nodeObjectType] = @0))) x)".Replace(Environment.NewLine, public void CreateIndexBuilder_SqlServer_Unique_CreatesUniqueNonClusteredIndex() { var logger = Mock.Of>(); - var sqlSyntax = new SqlServerSyntaxProvider(); + var sqlSyntax = new SqlServerSyntaxProvider(Options.Create(new GlobalSettings())); var db = new TestDatabase(DatabaseType.SqlServer2005, sqlSyntax); var context = new MigrationContext(db, logger); @@ -145,7 +147,7 @@ WHERE (([umbracoNode].[nodeObjectType] = @0))) x)".Replace(Environment.NewLine, public void CreateIndexBuilder_SqlServer_Unique_CreatesUniqueNonClusteredIndex_Multi_Columnn() { var logger = Mock.Of>(); - var sqlSyntax = new SqlServerSyntaxProvider(); + var sqlSyntax = new SqlServerSyntaxProvider(Options.Create(new GlobalSettings())); var db = new TestDatabase(DatabaseType.SqlServer2005, sqlSyntax); var context = new MigrationContext(db, logger); @@ -166,7 +168,7 @@ WHERE (([umbracoNode].[nodeObjectType] = @0))) x)".Replace(Environment.NewLine, public void CreateIndexBuilder_SqlServer_Clustered_CreatesClusteredIndex() { var logger = Mock.Of>(); - var sqlSyntax = new SqlServerSyntaxProvider(); + var sqlSyntax = new SqlServerSyntaxProvider(Options.Create(new GlobalSettings())); var db = new TestDatabase(DatabaseType.SqlServer2005, sqlSyntax); var context = new MigrationContext(db, logger); diff --git a/src/Umbraco.Tests.UnitTests/TestHelpers/BaseUsingSqlSyntax.cs b/src/Umbraco.Tests.UnitTests/TestHelpers/BaseUsingSqlSyntax.cs index e1eb437282..dce144803f 100644 --- a/src/Umbraco.Tests.UnitTests/TestHelpers/BaseUsingSqlSyntax.cs +++ b/src/Umbraco.Tests.UnitTests/TestHelpers/BaseUsingSqlSyntax.cs @@ -4,10 +4,12 @@ using System; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; using Moq; using NPoco; using NUnit.Framework; using Umbraco.Cms.Core.Composing; +using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Infrastructure.Persistence; using Umbraco.Cms.Infrastructure.Persistence.Mappers; @@ -41,7 +43,7 @@ namespace Umbraco.Cms.Tests.UnitTests.TestHelpers IServiceProvider factory = composition.CreateServiceProvider(); var pocoMappers = new NPoco.MapperCollection { new PocoMapper() }; var pocoDataFactory = new FluentPocoDataFactory((type, iPocoDataFactory) => new PocoDataBuilder(type, pocoMappers).Init()); - var sqlSyntax = new SqlServerSyntaxProvider(); + var sqlSyntax = new SqlServerSyntaxProvider(Options.Create(new GlobalSettings())); SqlContext = new SqlContext(sqlSyntax, DatabaseType.SqlServer2012, pocoDataFactory, new Lazy(() => factory.GetRequiredService())); Mappers = factory.GetRequiredService(); } diff --git a/src/Umbraco.Tests.UnitTests/TestHelpers/TestHelper.cs b/src/Umbraco.Tests.UnitTests/TestHelpers/TestHelper.cs index 81276ba562..7b0f190566 100644 --- a/src/Umbraco.Tests.UnitTests/TestHelpers/TestHelper.cs +++ b/src/Umbraco.Tests.UnitTests/TestHelpers/TestHelper.cs @@ -94,7 +94,7 @@ namespace Umbraco.Cms.Tests.UnitTests.TestHelpers public static Lazy GetMockSqlContext() { ISqlContext sqlContext = Mock.Of(); - var syntax = new SqlServerSyntaxProvider(); + var syntax = new SqlServerSyntaxProvider(Options.Create(new GlobalSettings())); Mock.Get(sqlContext).Setup(x => x.SqlSyntax).Returns(syntax); return new Lazy(() => sqlContext); } diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Core/Models/UserExtensionsTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Core/Models/UserExtensionsTests.cs index 556fb42ff0..ae51d77a93 100644 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Core/Models/UserExtensionsTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Core/Models/UserExtensionsTests.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.Linq; using Moq; using NUnit.Framework; +using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.Entities; using Umbraco.Cms.Core.Services; @@ -44,7 +45,7 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.Models .Setup(x => x.GetAllPaths(It.IsAny(), It.IsAny())) .Returns((type, ids) => new[] { new TreeEntityPath { Id = startNodeId, Path = startNodePath } }); - Assert.AreEqual(outcome, user.HasPathAccess(content, esmock.Object)); + Assert.AreEqual(outcome, user.HasPathAccess(content, esmock.Object, AppCaches.Disabled)); } [TestCase("", "1", "1")] // single user start, top level diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Core/Security/ContentPermissionsTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Core/Security/ContentPermissionsTests.cs index fc52aaf275..9bbd353516 100644 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Core/Security/ContentPermissionsTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Core/Security/ContentPermissionsTests.cs @@ -3,6 +3,7 @@ using Moq; using NUnit.Framework; +using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.Entities; using Umbraco.Cms.Core.Models.Membership; @@ -31,7 +32,7 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.Security IEntityService entityService = entityServiceMock.Object; var userServiceMock = new Mock(); IUserService userService = userServiceMock.Object; - var contentPermissions = new ContentPermissions(userService, contentService, entityService); + var contentPermissions = new ContentPermissions(userService, contentService, entityService, AppCaches.Disabled); // Act ContentPermissions.ContentAccess result = contentPermissions.CheckPermissions(1234, user, out IContent foundContent); @@ -58,7 +59,7 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.Security IUserService userService = userServiceMock.Object; var entityServiceMock = new Mock(); IEntityService entityService = entityServiceMock.Object; - var contentPermissions = new ContentPermissions(userService, contentService, entityService); + var contentPermissions = new ContentPermissions(userService, contentService, entityService, AppCaches.Disabled); // Act ContentPermissions.ContentAccess result = contentPermissions.CheckPermissions(1234, user, out IContent foundContent, new[] { 'F' }); @@ -87,7 +88,7 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.Security entityServiceMock.Setup(x => x.GetAllPaths(It.IsAny(), It.IsAny())) .Returns(new[] { Mock.Of(entity => entity.Id == 9876 && entity.Path == "-1,9876") }); IEntityService entityService = entityServiceMock.Object; - var contentPermissions = new ContentPermissions(userService, contentService, entityService); + var contentPermissions = new ContentPermissions(userService, contentService, entityService, AppCaches.Disabled); // Act ContentPermissions.ContentAccess result = contentPermissions.CheckPermissions(1234, user, out IContent foundContent, new[] { 'F' }); @@ -117,7 +118,7 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.Security IUserService userService = userServiceMock.Object; var entityServiceMock = new Mock(); IEntityService entityService = entityServiceMock.Object; - var contentPermissions = new ContentPermissions(userService, contentService, entityService); + var contentPermissions = new ContentPermissions(userService, contentService, entityService, AppCaches.Disabled); // Act ContentPermissions.ContentAccess result = contentPermissions.CheckPermissions(1234, user, out IContent foundContent, new[] { 'F' }); @@ -147,7 +148,7 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.Security IUserService userService = userServiceMock.Object; var entityServiceMock = new Mock(); IEntityService entityService = entityServiceMock.Object; - var contentPermissions = new ContentPermissions(userService, contentService, entityService); + var contentPermissions = new ContentPermissions(userService, contentService, entityService, AppCaches.Disabled); // Act ContentPermissions.ContentAccess result = contentPermissions.CheckPermissions(1234, user, out IContent foundContent, new[] { 'F' }); @@ -167,7 +168,7 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.Security IUserService userService = userServiceMock.Object; var entityServiceMock = new Mock(); IEntityService entityService = entityServiceMock.Object; - var contentPermissions = new ContentPermissions(userService, contentService, entityService); + var contentPermissions = new ContentPermissions(userService, contentService, entityService, AppCaches.Disabled); // Act ContentPermissions.ContentAccess result = contentPermissions.CheckPermissions(-1, user, out IContent _); @@ -187,7 +188,7 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.Security IUserService userService = userServiceMock.Object; var entityServiceMock = new Mock(); IEntityService entityService = entityServiceMock.Object; - var contentPermissions = new ContentPermissions(userService, contentService, entityService); + var contentPermissions = new ContentPermissions(userService, contentService, entityService, AppCaches.Disabled); // Act ContentPermissions.ContentAccess result = contentPermissions.CheckPermissions(-20, user, out IContent _); @@ -209,7 +210,7 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.Security entityServiceMock.Setup(x => x.GetAllPaths(It.IsAny(), It.IsAny())) .Returns(new[] { Mock.Of(entity => entity.Id == 1234 && entity.Path == "-1,1234") }); IEntityService entityService = entityServiceMock.Object; - var contentPermissions = new ContentPermissions(userService, contentService, entityService); + var contentPermissions = new ContentPermissions(userService, contentService, entityService, AppCaches.Disabled); // Act ContentPermissions.ContentAccess result = contentPermissions.CheckPermissions(-20, user, out IContent foundContent); @@ -232,7 +233,7 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.Security entityServiceMock.Setup(x => x.GetAllPaths(It.IsAny(), It.IsAny())) .Returns(new[] { Mock.Of(entity => entity.Id == 1234 && entity.Path == "-1,1234") }); IEntityService entityService = entityServiceMock.Object; - var contentPermissions = new ContentPermissions(userService, contentService, entityService); + var contentPermissions = new ContentPermissions(userService, contentService, entityService, AppCaches.Disabled); // Act ContentPermissions.ContentAccess result = contentPermissions.CheckPermissions(-1, user, out IContent foundContent); @@ -259,7 +260,7 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.Security IUserService userService = userServiceMock.Object; var entityServiceMock = new Mock(); IEntityService entityService = entityServiceMock.Object; - var contentPermissions = new ContentPermissions(userService, contentService, entityService); + var contentPermissions = new ContentPermissions(userService, contentService, entityService, AppCaches.Disabled); // Act ContentPermissions.ContentAccess result = contentPermissions.CheckPermissions(-1, user, out IContent foundContent, new[] { 'A' }); @@ -286,7 +287,7 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.Security IEntityService entityService = entityServiceMock.Object; var contentServiceMock = new Mock(); IContentService contentService = contentServiceMock.Object; - var contentPermissions = new ContentPermissions(userService, contentService, entityService); + var contentPermissions = new ContentPermissions(userService, contentService, entityService, AppCaches.Disabled); // Act ContentPermissions.ContentAccess result = contentPermissions.CheckPermissions(-1, user, out IContent foundContent, new[] { 'B' }); @@ -314,7 +315,7 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.Security IEntityService entityService = entityServiceMock.Object; var contentServiceMock = new Mock(); IContentService contentService = contentServiceMock.Object; - var contentPermissions = new ContentPermissions(userService, contentService, entityService); + var contentPermissions = new ContentPermissions(userService, contentService, entityService, AppCaches.Disabled); // Act ContentPermissions.ContentAccess result = contentPermissions.CheckPermissions(-20, user, out IContent foundContent, new[] { 'A' }); @@ -341,7 +342,7 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.Security IEntityService entityService = entityServiceMock.Object; var contentServiceMock = new Mock(); IContentService contentService = contentServiceMock.Object; - var contentPermissions = new ContentPermissions(userService, contentService, entityService); + var contentPermissions = new ContentPermissions(userService, contentService, entityService, AppCaches.Disabled); // Act ContentPermissions.ContentAccess result = contentPermissions.CheckPermissions(-20, user, out IContent foundContent, new[] { 'B' }); diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Core/Security/MediaPermissionsTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Core/Security/MediaPermissionsTests.cs index 370e968a76..b719b57c37 100644 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Core/Security/MediaPermissionsTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Core/Security/MediaPermissionsTests.cs @@ -3,6 +3,7 @@ using Moq; using NUnit.Framework; +using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.Entities; using Umbraco.Cms.Core.Models.Membership; @@ -29,7 +30,7 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.Security IMediaService mediaService = mediaServiceMock.Object; var entityServiceMock = new Mock(); IEntityService entityService = entityServiceMock.Object; - var mediaPermissions = new MediaPermissions(mediaService, entityService); + var mediaPermissions = new MediaPermissions(mediaService, entityService, AppCaches.Disabled); // Act MediaPermissions.MediaAccess result = mediaPermissions.CheckPermissions(user, 1234, out _); @@ -51,7 +52,7 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.Security IMediaService mediaService = mediaServiceMock.Object; var entityServiceMock = new Mock(); IEntityService entityService = entityServiceMock.Object; - var mediaPermissions = new MediaPermissions(mediaService, entityService); + var mediaPermissions = new MediaPermissions(mediaService, entityService, AppCaches.Disabled); // Act/assert MediaPermissions.MediaAccess result = mediaPermissions.CheckPermissions(user, 1234, out _); @@ -73,7 +74,7 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.Security entityServiceMock.Setup(x => x.GetAllPaths(It.IsAny(), It.IsAny())) .Returns(new[] { Mock.Of(entity => entity.Id == 9876 && entity.Path == "-1,9876") }); IEntityService entityService = entityServiceMock.Object; - var mediaPermissions = new MediaPermissions(mediaService, entityService); + var mediaPermissions = new MediaPermissions(mediaService, entityService, AppCaches.Disabled); // Act MediaPermissions.MediaAccess result = mediaPermissions.CheckPermissions(user, 1234, out _); @@ -91,7 +92,7 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.Security IMediaService mediaService = mediaServiceMock.Object; var entityServiceMock = new Mock(); IEntityService entityService = entityServiceMock.Object; - var mediaPermissions = new MediaPermissions(mediaService, entityService); + var mediaPermissions = new MediaPermissions(mediaService, entityService, AppCaches.Disabled); // Act MediaPermissions.MediaAccess result = mediaPermissions.CheckPermissions(user, -1, out _); @@ -111,7 +112,7 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.Security entityServiceMock.Setup(x => x.GetAllPaths(It.IsAny(), It.IsAny())) .Returns(new[] { Mock.Of(entity => entity.Id == 1234 && entity.Path == "-1,1234") }); IEntityService entityService = entityServiceMock.Object; - var mediaPermissions = new MediaPermissions(mediaService, entityService); + var mediaPermissions = new MediaPermissions(mediaService, entityService, AppCaches.Disabled); // Act MediaPermissions.MediaAccess result = mediaPermissions.CheckPermissions(user, -1, out _); @@ -129,7 +130,7 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.Security IMediaService mediaService = mediaServiceMock.Object; var entityServiceMock = new Mock(); IEntityService entityService = entityServiceMock.Object; - var mediaPermissions = new MediaPermissions(mediaService, entityService); + var mediaPermissions = new MediaPermissions(mediaService, entityService, AppCaches.Disabled); // Act MediaPermissions.MediaAccess result = mediaPermissions.CheckPermissions(user, -21, out _); @@ -149,7 +150,7 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.Security entityServiceMock.Setup(x => x.GetAllPaths(It.IsAny(), It.IsAny())) .Returns(new[] { Mock.Of(entity => entity.Id == 1234 && entity.Path == "-1,1234") }); IEntityService entityService = entityServiceMock.Object; - var mediaPermissions = new MediaPermissions(mediaService, entityService); + var mediaPermissions = new MediaPermissions(mediaService, entityService, AppCaches.Disabled); // Act MediaPermissions.MediaAccess result = mediaPermissions.CheckPermissions(user, -21, out _); diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Editors/UserEditorAuthorizationHelperTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Editors/UserEditorAuthorizationHelperTests.cs index 0cc7346d0e..ebdbaf08c0 100644 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Editors/UserEditorAuthorizationHelperTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Editors/UserEditorAuthorizationHelperTests.cs @@ -6,6 +6,7 @@ using System.Linq; using Moq; using NUnit.Framework; using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Editors; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.Entities; @@ -34,7 +35,8 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Editors var authHelper = new UserEditorAuthorizationHelper( contentService.Object, mediaService.Object, - entityService.Object); + entityService.Object, + AppCaches.Disabled); Attempt result = authHelper.IsAuthorized(currentUser, savingUser, new int[0], new int[0], new string[0]); @@ -55,7 +57,8 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Editors var authHelper = new UserEditorAuthorizationHelper( contentService.Object, mediaService.Object, - entityService.Object); + entityService.Object, + AppCaches.Disabled); Attempt result = authHelper.IsAuthorized(currentUser, savingUser, new int[0], new int[0], new string[0]); @@ -76,7 +79,8 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Editors var authHelper = new UserEditorAuthorizationHelper( contentService.Object, mediaService.Object, - entityService.Object); + entityService.Object, + AppCaches.Disabled); Attempt result = authHelper.IsAuthorized(currentUser, savingUser, new int[0], new int[0], new[] { "FunGroup" }); @@ -97,7 +101,8 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Editors var authHelper = new UserEditorAuthorizationHelper( contentService.Object, mediaService.Object, - entityService.Object); + entityService.Object, + AppCaches.Disabled); Attempt result = authHelper.IsAuthorized(currentUser, savingUser, new int[0], new int[0], new[] { "test" }); @@ -130,7 +135,8 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Editors var authHelper = new UserEditorAuthorizationHelper( contentService.Object, mediaService.Object, - entityService.Object); + entityService.Object, + AppCaches.Disabled); // adding 5555 which currentUser has access to since it's a child of 9876 ... adding is still ok even though currentUser doesn't have access to 1234 Attempt result = authHelper.IsAuthorized(currentUser, savingUser, new[] { 1234, 5555 }, new int[0], new string[0]); @@ -164,7 +170,8 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Editors var authHelper = new UserEditorAuthorizationHelper( contentService.Object, mediaService.Object, - entityService.Object); + entityService.Object, + AppCaches.Disabled); // removing 4567 start node even though currentUser doesn't have acces to it ... removing is ok Attempt result = authHelper.IsAuthorized(currentUser, savingUser, new[] { 1234 }, new int[0], new string[0]); @@ -198,7 +205,8 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Editors var authHelper = new UserEditorAuthorizationHelper( contentService.Object, mediaService.Object, - entityService.Object); + entityService.Object, + AppCaches.Disabled); // adding 1234 but currentUser doesn't have access to it ... nope Attempt result = authHelper.IsAuthorized(currentUser, savingUser, new[] { 1234 }, new int[0], new string[0]); @@ -232,7 +240,8 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Editors var authHelper = new UserEditorAuthorizationHelper( contentService.Object, mediaService.Object, - entityService.Object); + entityService.Object, + AppCaches.Disabled); // adding 5555 which currentUser has access to since it's a child of 9876 ... ok Attempt result = authHelper.IsAuthorized(currentUser, savingUser, new[] { 5555 }, new int[0], new string[0]); @@ -266,7 +275,8 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Editors var authHelper = new UserEditorAuthorizationHelper( contentService.Object, mediaService.Object, - entityService.Object); + entityService.Object, + AppCaches.Disabled); // adding 1234 but currentUser doesn't have access to it ... nope Attempt result = authHelper.IsAuthorized(currentUser, savingUser, new int[0], new[] { 1234 }, new string[0]); @@ -300,7 +310,8 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Editors var authHelper = new UserEditorAuthorizationHelper( contentService.Object, mediaService.Object, - entityService.Object); + entityService.Object, + AppCaches.Disabled); // adding 5555 which currentUser has access to since it's a child of 9876 ... ok Attempt result = authHelper.IsAuthorized(currentUser, savingUser, new int[0], new[] { 5555 }, new string[0]); @@ -334,7 +345,8 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Editors var authHelper = new UserEditorAuthorizationHelper( contentService.Object, mediaService.Object, - entityService.Object); + entityService.Object, + AppCaches.Disabled); // adding 5555 which currentUser has access to since it's a child of 9876 ... adding is still ok even though currentUser doesn't have access to 1234 Attempt result = authHelper.IsAuthorized(currentUser, savingUser, new int[0], new[] { 1234, 5555 }, new string[0]); @@ -368,7 +380,8 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Editors var authHelper = new UserEditorAuthorizationHelper( contentService.Object, mediaService.Object, - entityService.Object); + entityService.Object, + AppCaches.Disabled); // removing 4567 start node even though currentUser doesn't have acces to it ... removing is ok Attempt result = authHelper.IsAuthorized(currentUser, savingUser, new int[0], new[] { 1234 }, new string[0]); diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Examine/UmbracoContentValueSetValidatorTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Examine/UmbracoContentValueSetValidatorTests.cs index 87002824f7..a68b6f9200 100644 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Examine/UmbracoContentValueSetValidatorTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Examine/UmbracoContentValueSetValidatorTests.cs @@ -9,6 +9,7 @@ using Moq; using NUnit.Framework; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Infrastructure.Examine; @@ -20,7 +21,11 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Examine [Test] public void Invalid_Category() { - var validator = new ContentValueSetValidator(false, true, Mock.Of()); + var validator = new ContentValueSetValidator( + false, + true, + Mock.Of(), + Mock.Of()); ValueSetValidationResult result = validator.Validate(ValueSet.FromObject("555", IndexTypes.Content, new { hello = "world", path = "-1,555" })); Assert.AreEqual(ValueSetValidationResult.Valid, result); @@ -35,7 +40,11 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Examine [Test] public void Must_Have_Path() { - var validator = new ContentValueSetValidator(false, true, Mock.Of()); + var validator = new ContentValueSetValidator( + false, + true, + Mock.Of(), + Mock.Of()); ValueSetValidationResult result = validator.Validate(ValueSet.FromObject("555", IndexTypes.Content, new { hello = "world" })); Assert.AreEqual(ValueSetValidationResult.Failed, result); @@ -47,7 +56,12 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Examine [Test] public void Parent_Id() { - var validator = new ContentValueSetValidator(false, true, Mock.Of(), 555); + var validator = new ContentValueSetValidator( + false, + true, + Mock.Of(), + Mock.Of(), + 555); ValueSetValidationResult result = validator.Validate(ValueSet.FromObject("555", IndexTypes.Content, new { hello = "world", path = "-1,555" })); Assert.AreEqual(ValueSetValidationResult.Filtered, result); @@ -123,6 +137,7 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Examine false, true, Mock.Of(), + Mock.Of(), includeItemTypes: new List { "include-content" }); ValueSetValidationResult result = validator.Validate(ValueSet.FromObject("555", IndexTypes.Content, "test-content", new { hello = "world", path = "-1,555" })); @@ -142,6 +157,7 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Examine false, true, Mock.Of(), + Mock.Of(), excludeItemTypes: new List { "exclude-content" }); ValueSetValidationResult result = validator.Validate(ValueSet.FromObject("555", IndexTypes.Content, "test-content", new { hello = "world", path = "-1,555" })); @@ -161,6 +177,7 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Examine false, true, Mock.Of(), + Mock.Of(), includeItemTypes: new List { "include-content", "exclude-content" }, excludeItemTypes: new List { "exclude-content" }); @@ -180,7 +197,11 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Examine [Test] public void Recycle_Bin_Content() { - var validator = new ContentValueSetValidator(true, false, Mock.Of()); + var validator = new ContentValueSetValidator( + true, + false, + Mock.Of(), + Mock.Of()); ValueSetValidationResult result = validator.Validate(ValueSet.FromObject("555", IndexTypes.Content, new { hello = "world", path = "-1,-20,555" })); Assert.AreEqual(ValueSetValidationResult.Failed, result); @@ -206,7 +227,11 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Examine [Test] public void Recycle_Bin_Media() { - var validator = new ContentValueSetValidator(true, false, Mock.Of()); + var validator = new ContentValueSetValidator( + true, + false, + Mock.Of(), + Mock.Of()); ValueSetValidationResult result = validator.Validate(ValueSet.FromObject("555", IndexTypes.Media, new { hello = "world", path = "-1,-21,555" })); Assert.AreEqual(ValueSetValidationResult.Filtered, result); @@ -221,7 +246,11 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Examine [Test] public void Published_Only() { - var validator = new ContentValueSetValidator(true, true, Mock.Of()); + var validator = new ContentValueSetValidator( + true, + true, + Mock.Of(), + Mock.Of()); ValueSetValidationResult result = validator.Validate(ValueSet.FromObject("555", IndexTypes.Content, new { hello = "world", path = "-1,555" })); Assert.AreEqual(ValueSetValidationResult.Failed, result); @@ -252,7 +281,10 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Examine [Test] public void Published_Only_With_Variants() { - var validator = new ContentValueSetValidator(true, true, Mock.Of()); + var validator = new ContentValueSetValidator(true, + true, + Mock.Of(), + Mock.Of()); ValueSetValidationResult result = validator.Validate(new ValueSet( "555", @@ -316,7 +348,11 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Examine .Returns(Attempt.Succeed(new PublicAccessEntry(Guid.NewGuid(), 555, 444, 333, Enumerable.Empty()))); publicAccessService.Setup(x => x.IsProtected("-1,777")) .Returns(Attempt.Fail()); - var validator = new ContentValueSetValidator(false, false, publicAccessService.Object); + var validator = new ContentValueSetValidator( + false, + false, + publicAccessService.Object, + Mock.Of()); ValueSetValidationResult result = validator.Validate(ValueSet.FromObject("555", IndexTypes.Content, new { hello = "world", path = "-1,555" })); Assert.AreEqual(ValueSetValidationResult.Filtered, result); diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Migrations/MigrationPlanTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Migrations/MigrationPlanTests.cs index bc48cb4317..c6090a7bc2 100644 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Migrations/MigrationPlanTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Migrations/MigrationPlanTests.cs @@ -6,9 +6,11 @@ using System.Collections.Generic; using System.Linq; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Extensions.Options; using Moq; using NPoco; using NUnit.Framework; +using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Migrations; using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Core.Services; @@ -36,7 +38,7 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Migrations .Setup(x => x.Database) .Returns(database); - var sqlContext = new SqlContext(new SqlServerSyntaxProvider(), DatabaseType.SQLCe, Mock.Of()); + var sqlContext = new SqlContext(new SqlServerSyntaxProvider(Options.Create(new GlobalSettings())), DatabaseType.SQLCe, Mock.Of()); var scopeProvider = new MigrationTests.TestScopeProvider(scope) { SqlContext = sqlContext }; IMigrationBuilder migrationBuilder = Mock.Of(); diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Migrations/PostMigrationTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Migrations/PostMigrationTests.cs index 70c418e54c..4b1e2aa526 100644 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Migrations/PostMigrationTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Migrations/PostMigrationTests.cs @@ -4,9 +4,11 @@ using System; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Extensions.Options; using Moq; using NPoco; using NUnit.Framework; +using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Migrations; using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Core.Services; @@ -49,7 +51,7 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Migrations .Returns(database); var sqlContext = new SqlContext( - new SqlServerSyntaxProvider(), + new SqlServerSyntaxProvider(Options.Create(new GlobalSettings())), DatabaseType.SQLCe, Mock.Of()); var scopeProvider = new MigrationTests.TestScopeProvider(scope) { SqlContext = sqlContext }; @@ -99,7 +101,7 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Migrations .Returns(database); var sqlContext = new SqlContext( - new SqlServerSyntaxProvider(), + new SqlServerSyntaxProvider(Options.Create(new GlobalSettings())), DatabaseType.SQLCe, Mock.Of()); var scopeProvider = new MigrationTests.TestScopeProvider(scope) { SqlContext = sqlContext }; diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Persistence/NPocoTests/NPocoSqlTemplateTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Persistence/NPocoTests/NPocoSqlTemplateTests.cs index 4632e11413..cc152bed66 100644 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Persistence/NPocoTests/NPocoSqlTemplateTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Persistence/NPocoTests/NPocoSqlTemplateTests.cs @@ -2,9 +2,11 @@ // See LICENSE for more details. using System; +using Microsoft.Extensions.Options; using Moq; using NPoco; using NUnit.Framework; +using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Infrastructure.Persistence; using Umbraco.Cms.Infrastructure.Persistence.Mappers; using Umbraco.Cms.Infrastructure.Persistence.SqlSyntax; @@ -19,7 +21,7 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Persistence.NPocoTe [Test] public void SqlTemplates() { - var sqlContext = new SqlContext(new SqlServerSyntaxProvider(), DatabaseType.SqlServer2012, Mock.Of()); + var sqlContext = new SqlContext(new SqlServerSyntaxProvider(Options.Create(new GlobalSettings())), DatabaseType.SqlServer2012, Mock.Of()); var sqlTemplates = new SqlTemplates(sqlContext); // this can be used for queries that we know we'll use a *lot* and @@ -42,7 +44,7 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Persistence.NPocoTe var mappers = new NPoco.MapperCollection { new PocoMapper() }; var factory = new FluentPocoDataFactory((type, iPocoDataFactory) => new PocoDataBuilder(type, mappers).Init()); - var sqlContext = new SqlContext(new SqlServerSyntaxProvider(), DatabaseType.SQLCe, factory); + var sqlContext = new SqlContext(new SqlServerSyntaxProvider(Options.Create(new GlobalSettings())), DatabaseType.SQLCe, factory); var sqlTemplates = new SqlTemplates(sqlContext); const string sqlBase = "SELECT [zbThing1].[id] AS [Id], [zbThing1].[name] AS [Name] FROM [zbThing1] WHERE "; diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Persistence/Querying/ExpressionTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Persistence/Querying/ExpressionTests.cs index 2e4556daf0..a93a8cbe35 100644 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Persistence/Querying/ExpressionTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Persistence/Querying/ExpressionTests.cs @@ -6,8 +6,10 @@ using System.Diagnostics; using System.Linq; using System.Linq.Expressions; using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Extensions.Options; using Moq; using NUnit.Framework; +using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.Membership; using Umbraco.Cms.Core.Persistence; @@ -154,7 +156,7 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Persistence.Queryin [Test] public void Equals_Method_For_Value_Gets_Escaped() { - var sqlSyntax = new SqlServerSyntaxProvider(); + var sqlSyntax = new SqlServerSyntaxProvider(Options.Create(new GlobalSettings())); Expression> predicate = user => user.Username.Equals("hello@world.com"); var modelToSqlExpressionHelper = new ModelToSqlExpressionVisitor(SqlContext.SqlSyntax, Mappers); var result = modelToSqlExpressionHelper.Visit(predicate); diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Authorization/AdminUsersHandlerTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Authorization/AdminUsersHandlerTests.cs index ade6a304d3..191381d8c5 100644 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Authorization/AdminUsersHandlerTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Authorization/AdminUsersHandlerTests.cs @@ -9,6 +9,7 @@ using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Primitives; using Moq; using NUnit.Framework; +using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Editors; using Umbraco.Cms.Core.Models.Membership; @@ -197,7 +198,7 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Web.BackOffice.Authorization var mockContentService = new Mock(); var mockMediaService = new Mock(); var mockEntityService = new Mock(); - return new UserEditorAuthorizationHelper(mockContentService.Object, mockMediaService.Object, mockEntityService.Object); + return new UserEditorAuthorizationHelper(mockContentService.Object, mockMediaService.Object, mockEntityService.Object, AppCaches.Disabled); } } } diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Authorization/ContentPermissionsPublishBranchHandlerTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Authorization/ContentPermissionsPublishBranchHandlerTests.cs index 416b2f0d40..c85ae8705a 100644 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Authorization/ContentPermissionsPublishBranchHandlerTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Authorization/ContentPermissionsPublishBranchHandlerTests.cs @@ -7,6 +7,7 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Authorization; using Moq; using NUnit.Framework; +using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.Entities; using Umbraco.Cms.Core.Models.Membership; @@ -127,7 +128,7 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Web.BackOffice.Authorization .Setup(x => x.GetById(It.Is(y => y == nodeId))) .Returns(CreateContent(nodeId)); - return new ContentPermissions(userService, mockContentService.Object, entityService); + return new ContentPermissions(userService, mockContentService.Object, entityService, AppCaches.Disabled); } private static IContent CreateContent(int nodeId) diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Authorization/ContentPermissionsQueryStringHandlerTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Authorization/ContentPermissionsQueryStringHandlerTests.cs index 58ff85d427..aa227842ec 100644 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Authorization/ContentPermissionsQueryStringHandlerTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Authorization/ContentPermissionsQueryStringHandlerTests.cs @@ -11,6 +11,7 @@ using Microsoft.Extensions.Primitives; using Moq; using NUnit.Framework; using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.Membership; using Umbraco.Cms.Core.Security; @@ -229,7 +230,7 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Web.BackOffice.Authorization .Setup(x => x.GetById(It.Is(y => y == nodeId))) .Returns(CreateContent(nodeId)); - return new ContentPermissions(mockUserService.Object, mockContentService.Object, entityService); + return new ContentPermissions(mockUserService.Object, mockContentService.Object, entityService, AppCaches.Disabled); } private static IContent CreateContent(int nodeId) diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Authorization/ContentPermissionsResourceHandlerTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Authorization/ContentPermissionsResourceHandlerTests.cs index 345b9a2b0d..a3428e8fe3 100644 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Authorization/ContentPermissionsResourceHandlerTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Authorization/ContentPermissionsResourceHandlerTests.cs @@ -7,6 +7,7 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Authorization; using Moq; using NUnit.Framework; +using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.Membership; using Umbraco.Cms.Core.Security; @@ -118,7 +119,7 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Web.BackOffice.Authorization .Returns(CreateContent(nodeId)); var mockEntityService = new Mock(); - return new ContentPermissions(mockUserService.Object, mockContentService.Object, mockEntityService.Object); + return new ContentPermissions(mockUserService.Object, mockContentService.Object, mockEntityService.Object, AppCaches.Disabled); } } } diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Authorization/MediaPermissionsQueryStringHandlerTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Authorization/MediaPermissionsQueryStringHandlerTests.cs index af2dd9226a..c4215ea722 100644 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Authorization/MediaPermissionsQueryStringHandlerTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Authorization/MediaPermissionsQueryStringHandlerTests.cs @@ -11,6 +11,7 @@ using Microsoft.Extensions.Primitives; using Moq; using NUnit.Framework; using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.Membership; using Umbraco.Cms.Core.Security; @@ -195,7 +196,7 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Web.BackOffice.Authorization .Setup(x => x.GetById(It.Is(y => y == nodeId))) .Returns(CreateMedia(nodeId)); - return new MediaPermissions(mockMediaService.Object, entityService); + return new MediaPermissions(mockMediaService.Object, entityService, AppCaches.Disabled); } private static IMedia CreateMedia(int nodeId) diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Authorization/MediaPermissionsResourceHandlerTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Authorization/MediaPermissionsResourceHandlerTests.cs index 9b2c217ac6..18d7e503df 100644 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Authorization/MediaPermissionsResourceHandlerTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Authorization/MediaPermissionsResourceHandlerTests.cs @@ -7,6 +7,7 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Authorization; using Moq; using NUnit.Framework; +using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.Membership; using Umbraco.Cms.Core.Security; @@ -111,7 +112,7 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Web.BackOffice.Authorization .Returns(CreateMedia(nodeId)); var mockEntityService = new Mock(); - return new MediaPermissions(mockMediaService.Object, mockEntityService.Object); + return new MediaPermissions(mockMediaService.Object, mockEntityService.Object, AppCaches.Disabled); } } } diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Authorization/UserGroupHandlerTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Authorization/UserGroupHandlerTests.cs index f6fc89cc48..cf08011bc5 100644 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Authorization/UserGroupHandlerTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Authorization/UserGroupHandlerTests.cs @@ -9,6 +9,7 @@ using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Primitives; using Moq; using NUnit.Framework; +using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Models.Membership; using Umbraco.Cms.Core.Security; using Umbraco.Cms.Core.Services; @@ -116,7 +117,7 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Web.BackOffice.Authorization Mock mockBackOfficeSecurityAccessor = CreateMockBackOfficeSecurityAccessor(userIsAdmin); - return new UserGroupHandler(mockHttpContextAccessor.Object, mockUserService.Object, mockContentService.Object, mockMediaService.Object, mockEntityService.Object, mockBackOfficeSecurityAccessor.Object); + return new UserGroupHandler(mockHttpContextAccessor.Object, mockUserService.Object, mockContentService.Object, mockMediaService.Object, mockEntityService.Object, mockBackOfficeSecurityAccessor.Object, AppCaches.Disabled); } private static Mock CreateMockHttpContextAccessor(string queryStringValue) diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Filters/FilterAllowedOutgoingContentAttributeTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Filters/FilterAllowedOutgoingContentAttributeTests.cs index dbd6bd4bad..4da095c121 100644 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Filters/FilterAllowedOutgoingContentAttributeTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Filters/FilterAllowedOutgoingContentAttributeTests.cs @@ -7,6 +7,7 @@ using Microsoft.AspNetCore.Mvc; using Moq; using NUnit.Framework; using Umbraco.Cms.Core.Actions; +using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.ContentEditing; using Umbraco.Cms.Core.Models.Entities; @@ -34,6 +35,7 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Web.BackOffice.Filters ActionBrowse.ActionLetter, Mock.Of(), Mock.Of(), + AppCaches.Disabled, Mock.Of()); dynamic result = att.GetValueFromResponse(new ObjectResult(expected)); @@ -53,6 +55,7 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Web.BackOffice.Filters ActionBrowse.ActionLetter, Mock.Of(), Mock.Of(), + AppCaches.Disabled, Mock.Of()); dynamic result = att.GetValueFromResponse(new ObjectResult(container)); @@ -72,6 +75,7 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Web.BackOffice.Filters ActionBrowse.ActionLetter, Mock.Of(), Mock.Of(), + AppCaches.Disabled, Mock.Of()); dynamic actual = att.GetValueFromResponse(new ObjectResult(container)); @@ -97,6 +101,7 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Web.BackOffice.Filters ActionBrowse.ActionLetter, userService, entityService, + AppCaches.Disabled, Mock.Of()); var path = string.Empty; @@ -148,6 +153,7 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Web.BackOffice.Filters ActionBrowse.ActionLetter, userService, Mock.Of(), + AppCaches.Disabled, Mock.Of()); att.FilterBasedOnPermissions(list, user); diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Web.Common/Routing/RoutableDocumentFilterTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Web.Common/Routing/RoutableDocumentFilterTests.cs index 7ce84a854b..3613de093f 100644 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Web.Common/Routing/RoutableDocumentFilterTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Web.Common/Routing/RoutableDocumentFilterTests.cs @@ -6,6 +6,7 @@ using Microsoft.AspNetCore.Routing.Patterns; using Microsoft.Extensions.Options; using Moq; using NUnit.Framework; +using Umbraco.Cms.Core; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Hosting; using Umbraco.Cms.Web.Common.Routing; @@ -22,7 +23,7 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Web.Common.Routing private IHostingEnvironment GetHostingEnvironment() { var hostingEnv = new Mock(); - hostingEnv.Setup(x => x.ToAbsolute(It.IsAny())).Returns((string virtualPath) => virtualPath.TrimStart('~', '/')); + hostingEnv.Setup(x => x.ToAbsolute(It.IsAny())).Returns((string virtualPath) => virtualPath.TrimStart(Constants.CharArrays.TildeForwardSlash)); return hostingEnv.Object; } diff --git a/src/Umbraco.Tests/Membership/MembersMembershipProviderTests.cs b/src/Umbraco.Tests/Membership/MembersMembershipProviderTests.cs new file mode 100644 index 0000000000..8f1e36aa11 --- /dev/null +++ b/src/Umbraco.Tests/Membership/MembersMembershipProviderTests.cs @@ -0,0 +1,114 @@ +// using System.Collections.Specialized; +// using System.Web.Security; +// using Moq; +// using NUnit.Framework; +// using Umbraco.Core; +// using Umbraco.Core.Cache; +// using Umbraco.Core.Composing; +// using Umbraco.Core.Logging; +// using Umbraco.Core.Models; +// using Umbraco.Core.Services; +// using Umbraco.Core.Sync; +// using Umbraco.Tests.Integration; +// using Umbraco.Tests.TestHelpers; +// using Umbraco.Tests.TestHelpers.Entities; +// using Umbraco.Tests.Testing; +// using Umbraco.Web; +// using Umbraco.Web.Cache; +// using Umbraco.Web.Security.Providers; +// +// namespace Umbraco.Tests.Membership +// { +// [TestFixture] +// [UmbracoTest(Database = UmbracoTestOptions.Database.NewSchemaPerFixture)] +// public class MembersMembershipProviderTests : TestWithDatabaseBase +// { +// private MembersMembershipProvider MembersMembershipProvider { get; set; } +// private IDistributedCacheBinder DistributedCacheBinder { get; set; } +// +// public IMemberService MemberService => Current.Factory.GetInstance(); +// public IMemberTypeService MemberTypeService => Current.Factory.GetInstance(); +// public ILogger Logger => Current.Factory.GetInstance(); +// +// public override void SetUp() +// { +// base.SetUp(); +// +// MembersMembershipProvider = new MembersMembershipProvider(MemberService, MemberTypeService); +// +// MembersMembershipProvider.Initialize("test", new NameValueCollection { { "passwordFormat", MembershipPasswordFormat.Clear.ToString() } }); +// +// DistributedCacheBinder = new DistributedCacheBinder(new DistributedCache(), Mock.Of(), Logger); +// DistributedCacheBinder.BindEvents(true); +// } +// +// [TearDown] +// public void Teardown() +// { +// DistributedCacheBinder?.UnbindEvents(); +// DistributedCacheBinder = null; +// } +// +// protected override void Compose() +// { +// base.Compose(); +// +// // the cache refresher component needs to trigger to refresh caches +// // but then, it requires a lot of plumbing ;( +// // FIXME: and we cannot inject a DistributedCache yet +// // so doing all this mess +// Composition.RegisterUnique(); +// Composition.RegisterUnique(f => Mock.Of()); +// Composition.WithCollectionBuilder() +// .Add(() => Composition.TypeLoader.GetCacheRefreshers()); +// } +// +// protected override AppCaches GetAppCaches() +// { +// // this is what's created core web runtime +// return new AppCaches( +// new DeepCloneAppCache(new ObjectCacheAppCache()), +// NoAppCache.Instance, +// new IsolatedCaches(type => new DeepCloneAppCache(new ObjectCacheAppCache()))); +// } +// +// /// +// /// MembersMembershipProvider.ValidateUser is expected to increase the number of failed attempts and also read that same number. +// /// +// /// +// /// This test requires the caching to be enabled, as it already is correct in the database. +// /// Shows the error described here: https://github.com/umbraco/Umbraco-CMS/issues/9861 +// /// +// [Test] +// public void ValidateUser__must_lock_out_users_after_max_attempts_of_wrong_password() +// { +// // Arrange +// IMemberType memberType = MockedContentTypes.CreateSimpleMemberType(); +// ServiceContext.MemberTypeService.Save(memberType); +// var member = MockedMember.CreateSimpleMember(memberType, "test", "test@test.com", "password","test"); +// ServiceContext.MemberService.Save(member); +// +// var wrongPassword = "wrongPassword"; +// var numberOfFailedAttempts = MembersMembershipProvider.MaxInvalidPasswordAttempts+2; +// +// // Act +// var memberBefore = ServiceContext.MemberService.GetById(member.Id); +// for (int i = 0; i < numberOfFailedAttempts; i++) +// { +// MembersMembershipProvider.ValidateUser(member.Username, wrongPassword); +// } +// var memberAfter = ServiceContext.MemberService.GetById(member.Id); +// +// // Assert +// Assert.Multiple(() => +// { +// Assert.AreEqual(0 , memberBefore.FailedPasswordAttempts, "Expected 0 failed password attempts before"); +// Assert.IsFalse(memberBefore.IsLockedOut, "Expected the member NOT to be locked out before"); +// +// Assert.AreEqual(MembersMembershipProvider.MaxInvalidPasswordAttempts, memberAfter.FailedPasswordAttempts, "Expected exactly the max possible failed password attempts after"); +// Assert.IsTrue(memberAfter.IsLockedOut, "Expected the member to be locked out after"); +// }); +// +// } +// } +// } diff --git a/src/Umbraco.Tests/Persistence/Mappers/MapperTestBase.cs b/src/Umbraco.Tests/Persistence/Mappers/MapperTestBase.cs index 5e3650969a..446d407077 100644 --- a/src/Umbraco.Tests/Persistence/Mappers/MapperTestBase.cs +++ b/src/Umbraco.Tests/Persistence/Mappers/MapperTestBase.cs @@ -1,5 +1,7 @@ using System; +using Microsoft.Extensions.Options; using Moq; +using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Infrastructure.Persistence; using Umbraco.Cms.Infrastructure.Persistence.Mappers; using Umbraco.Cms.Persistence.SqlCe; @@ -11,7 +13,7 @@ namespace Umbraco.Tests.Persistence.Mappers protected Lazy MockSqlContext() { var sqlContext = Mock.Of(); - var syntax = new SqlCeSyntaxProvider(); + var syntax = new SqlCeSyntaxProvider(Options.Create(new GlobalSettings())); Mock.Get(sqlContext).Setup(x => x.SqlSyntax).Returns(syntax); return new Lazy(() => sqlContext); } diff --git a/src/Umbraco.Tests/TestHelpers/BaseUsingSqlCeSyntax.cs b/src/Umbraco.Tests/TestHelpers/BaseUsingSqlCeSyntax.cs index f634312788..d0717c1f78 100644 --- a/src/Umbraco.Tests/TestHelpers/BaseUsingSqlCeSyntax.cs +++ b/src/Umbraco.Tests/TestHelpers/BaseUsingSqlCeSyntax.cs @@ -4,11 +4,13 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Extensions.Options; using Moq; using NPoco; using NUnit.Framework; using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Composing; +using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.Logging; using Umbraco.Cms.Infrastructure.Persistence; @@ -66,7 +68,7 @@ namespace Umbraco.Tests.TestHelpers var pocoMappers = new NPoco.MapperCollection { new PocoMapper() }; var pocoDataFactory = new FluentPocoDataFactory((type, iPocoDataFactory) => new PocoDataBuilder(type, pocoMappers).Init()); - var sqlSyntax = new SqlCeSyntaxProvider(); + var sqlSyntax = new SqlCeSyntaxProvider(Options.Create(new GlobalSettings())); SqlContext = new SqlContext(sqlSyntax, DatabaseType.SQLCe, pocoDataFactory, new Lazy(() => factory.GetRequiredService())); Mappers = factory.GetRequiredService(); diff --git a/src/Umbraco.Tests/TestHelpers/TestObjects-Mocks.cs b/src/Umbraco.Tests/TestHelpers/TestObjects-Mocks.cs index 6e9ce379b3..4c8c38420b 100644 --- a/src/Umbraco.Tests/TestHelpers/TestObjects-Mocks.cs +++ b/src/Umbraco.Tests/TestHelpers/TestObjects-Mocks.cs @@ -6,6 +6,7 @@ using System.Linq; using System.Linq.Expressions; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; using Moq; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Configuration.Models; @@ -36,7 +37,7 @@ namespace Umbraco.Tests.TestHelpers /// This is just a void factory that has no actual database. public IUmbracoDatabaseFactory GetDatabaseFactoryMock(bool configured = true, bool canConnect = true) { - var sqlSyntax = new SqlCeSyntaxProvider(); + var sqlSyntax = new SqlCeSyntaxProvider(Options.Create(new GlobalSettings())); var sqlContext = Mock.Of(); Mock.Get(sqlContext).Setup(x => x.SqlSyntax).Returns(sqlSyntax); diff --git a/src/Umbraco.Tests/TestHelpers/TestObjects.cs b/src/Umbraco.Tests/TestHelpers/TestObjects.cs index 3d4f58fd18..6f3ac08219 100644 --- a/src/Umbraco.Tests/TestHelpers/TestObjects.cs +++ b/src/Umbraco.Tests/TestHelpers/TestObjects.cs @@ -39,7 +39,7 @@ namespace Umbraco.Tests.TestHelpers /// that can begin a transaction. public UmbracoDatabase GetUmbracoSqlCeDatabase(ILogger logger) { - var syntax = new SqlCeSyntaxProvider(); + var syntax = new SqlCeSyntaxProvider(Options.Create(new GlobalSettings())); var connection = GetDbConnection(); var sqlContext = new SqlContext(syntax, DatabaseType.SQLCe, Mock.Of()); return new UmbracoDatabase(connection, sqlContext, logger, TestHelper.BulkSqlInsertProvider); @@ -54,7 +54,7 @@ namespace Umbraco.Tests.TestHelpers /// that can begin a transaction. public UmbracoDatabase GetUmbracoSqlServerDatabase(ILogger logger) { - var syntax = new SqlServerSyntaxProvider(); // do NOT try to get the server's version! + var syntax = new SqlServerSyntaxProvider(Options.Create(new GlobalSettings())); // do NOT try to get the server's version! var connection = GetDbConnection(); var sqlContext = new SqlContext(syntax, DatabaseType.SqlServer2008, Mock.Of()); return new UmbracoDatabase(connection, sqlContext, logger, TestHelper.BulkSqlInsertProvider); @@ -62,9 +62,9 @@ namespace Umbraco.Tests.TestHelpers public IScopeProvider GetScopeProvider(ILoggerFactory loggerFactory, FileSystems fileSystems = null, IUmbracoDatabaseFactory databaseFactory = null) { - var globalSettings = new GlobalSettings(); + var globalSettings = Options.Create(new GlobalSettings()); var connectionString = ConfigurationManager.ConnectionStrings[Constants.System.UmbracoConnectionName].ConnectionString; - var connectionStrings = new ConnectionStrings { UmbracoConnectionString = new ConfigConnectionString(Constants.System.UmbracoConnectionName, connectionString) }; + var connectionStrings = Options.Create(new ConnectionStrings { UmbracoConnectionString = new ConfigConnectionString(Constants.System.UmbracoConnectionName, connectionString) }); var coreDebugSettings = new CoreDebugSettings(); if (databaseFactory == null) @@ -83,7 +83,7 @@ namespace Umbraco.Tests.TestHelpers new DatabaseSchemaCreatorFactory(Mock.Of>(),loggerFactory, new UmbracoVersion())); } - fileSystems ??= new FileSystems(Current.Factory, loggerFactory.CreateLogger(), loggerFactory, TestHelper.IOHelper, Options.Create(globalSettings), TestHelper.GetHostingEnvironment()); + fileSystems ??= new FileSystems(Current.Factory, loggerFactory.CreateLogger(), loggerFactory, TestHelper.IOHelper, globalSettings, TestHelper.GetHostingEnvironment()); var coreDebug = TestHelper.CoreDebugSettings; var mediaFileSystem = Mock.Of(); return new ScopeProvider(databaseFactory, fileSystems, Options.Create(coreDebugSettings), mediaFileSystem, loggerFactory.CreateLogger(), loggerFactory, NoAppCache.Instance); diff --git a/src/Umbraco.Tests/TestHelpers/TestWithDatabaseBase.cs b/src/Umbraco.Tests/TestHelpers/TestWithDatabaseBase.cs index 7840dc6a21..65695a7420 100644 --- a/src/Umbraco.Tests/TestHelpers/TestWithDatabaseBase.cs +++ b/src/Umbraco.Tests/TestHelpers/TestWithDatabaseBase.cs @@ -144,7 +144,7 @@ namespace Umbraco.Tests.TestHelpers protected virtual ISqlSyntaxProvider GetSyntaxProvider() { - return new SqlCeSyntaxProvider(); + return new SqlCeSyntaxProvider(Microsoft.Extensions.Options.Options.Create(new GlobalSettings())); } protected virtual string GetDbProviderName() diff --git a/src/Umbraco.Tests/Testing/UmbracoTestBase.cs b/src/Umbraco.Tests/Testing/UmbracoTestBase.cs index 47ba5f78ce..3a4033af6e 100644 --- a/src/Umbraco.Tests/Testing/UmbracoTestBase.cs +++ b/src/Umbraco.Tests/Testing/UmbracoTestBase.cs @@ -480,8 +480,8 @@ namespace Umbraco.Tests.Testing Builder.Services.AddUnique(_ => new TransientEventMessagesFactory()); - var globalSettings = new GlobalSettings(); - var connectionStrings = new ConnectionStrings(); + var globalSettings = Microsoft.Extensions.Options.Options.Create(new GlobalSettings()); + var connectionStrings = Microsoft.Extensions.Options.Options.Create(new ConnectionStrings()); Builder.Services.AddUnique(f => new UmbracoDatabaseFactory(_loggerFactory.CreateLogger(), LoggerFactory, diff --git a/src/Umbraco.Tests/Umbraco.Tests.csproj b/src/Umbraco.Tests/Umbraco.Tests.csproj index 930db50828..9e2fb6799a 100644 --- a/src/Umbraco.Tests/Umbraco.Tests.csproj +++ b/src/Umbraco.Tests/Umbraco.Tests.csproj @@ -85,6 +85,7 @@ 2.0.0-alpha.20200128.15 + 1.11.30 @@ -119,7 +120,7 @@ - + @@ -159,6 +160,7 @@ + diff --git a/src/Umbraco.Web.BackOffice/Authorization/UserGroupHandler.cs b/src/Umbraco.Web.BackOffice/Authorization/UserGroupHandler.cs index 8020faa4ff..6fe807e6fe 100644 --- a/src/Umbraco.Web.BackOffice/Authorization/UserGroupHandler.cs +++ b/src/Umbraco.Web.BackOffice/Authorization/UserGroupHandler.cs @@ -8,6 +8,7 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Primitives; using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Models.Membership; using Umbraco.Cms.Core.Security; using Umbraco.Cms.Core.Services; @@ -27,6 +28,7 @@ namespace Umbraco.Cms.Web.BackOffice.Authorization private readonly IMediaService _mediaService; private readonly IEntityService _entityService; private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor; + private readonly AppCaches _appCaches; /// /// Initializes a new instance of the class. @@ -43,7 +45,8 @@ namespace Umbraco.Cms.Web.BackOffice.Authorization IContentService contentService, IMediaService mediaService, IEntityService entityService, - IBackOfficeSecurityAccessor backOfficeSecurityAccessor) + IBackOfficeSecurityAccessor backOfficeSecurityAccessor, + AppCaches appCaches) { _httpContextAccessor = httpContextAccessor; _userService = userService; @@ -51,6 +54,7 @@ namespace Umbraco.Cms.Web.BackOffice.Authorization _mediaService = mediaService; _entityService = entityService; _backOfficeSecurityAccessor = backOfficeSecurityAccessor; + _appCaches = appCaches; } /// @@ -80,7 +84,8 @@ namespace Umbraco.Cms.Web.BackOffice.Authorization _userService, _contentService, _mediaService, - _entityService); + _entityService, + _appCaches); Attempt isAuth = authHelper.AuthorizeGroupAccess(currentUser, intIds); diff --git a/src/Umbraco.Web.BackOffice/Controllers/AuthenticationController.cs b/src/Umbraco.Web.BackOffice/Controllers/AuthenticationController.cs index 9bf77abba2..1ffc4ab996 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/AuthenticationController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/AuthenticationController.cs @@ -603,7 +603,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers var userDetail = _umbracoMapper.Map(user); // update the userDetail and set their remaining seconds - userDetail.SecondsUntilTimeout = TimeSpan.FromMinutes(_globalSettings.TimeOutInMinutes).TotalSeconds; + userDetail.SecondsUntilTimeout = _globalSettings.TimeOut.TotalSeconds; return userDetail; } diff --git a/src/Umbraco.Web.BackOffice/Controllers/BackOfficeServerVariables.cs b/src/Umbraco.Web.BackOffice/Controllers/BackOfficeServerVariables.cs index 789dbfd752..5fac88ddb8 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/BackOfficeServerVariables.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/BackOfficeServerVariables.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Runtime.Serialization; @@ -378,8 +378,8 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers "umbracoSettings", new Dictionary { {"umbracoPath", _globalSettings.GetBackOfficePath(_hostingEnvironment)}, - {"mediaPath", _hostingEnvironment.ToAbsolute(globalSettings.UmbracoMediaPath).TrimEnd('/')}, - {"appPluginsPath", _hostingEnvironment.ToAbsolute(Constants.SystemDirectories.AppPlugins).TrimEnd('/')}, + {"mediaPath", _hostingEnvironment.ToAbsolute(globalSettings.UmbracoMediaPath).TrimEnd(Constants.CharArrays.ForwardSlash)}, + {"appPluginsPath", _hostingEnvironment.ToAbsolute(Constants.SystemDirectories.AppPlugins).TrimEnd(Constants.CharArrays.ForwardSlash)}, { "imageFileTypes", string.Join(",", _imageUrlGenerator.SupportedImageFileTypes) @@ -398,7 +398,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers }, {"keepUserLoggedIn", _securitySettings.KeepUserLoggedIn}, {"usernameIsEmail", _securitySettings.UsernameIsEmail}, - {"cssPath", _hostingEnvironment.ToAbsolute(globalSettings.UmbracoCssPath).TrimEnd('/')}, + {"cssPath", _hostingEnvironment.ToAbsolute(globalSettings.UmbracoCssPath).TrimEnd(Constants.CharArrays.ForwardSlash)}, {"allowPasswordReset", _securitySettings.AllowPasswordReset}, {"loginBackgroundImage", _contentSettings.LoginBackgroundImage}, {"loginLogoImage", _contentSettings.LoginLogoImage }, diff --git a/src/Umbraco.Web.BackOffice/Controllers/CodeFileController.cs b/src/Umbraco.Web.BackOffice/Controllers/CodeFileController.cs index ffdc45d5ac..7803f9cafb 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/CodeFileController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/CodeFileController.cs @@ -310,7 +310,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers if (id != Constants.System.RootString) { - codeFileDisplay.VirtualPath += id.TrimStart("/").EnsureEndsWith("/"); + codeFileDisplay.VirtualPath += id.TrimStart(Constants.CharArrays.ForwardSlash).EnsureEndsWith("/"); //if it's not new then it will have a path, otherwise it won't codeFileDisplay.Path = Url.GetTreePathFromFilePath(id); } @@ -503,7 +503,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers data.Content = StylesheetHelper.ReplaceRule(data.Content, rule.Name, null); } - data.Content = data.Content.TrimEnd('\n', '\r'); + data.Content = data.Content.TrimEnd(Constants.CharArrays.LineFeedCarriageReturn); // now add all the posted rules if (data.Rules != null && data.Rules.Any()) diff --git a/src/Umbraco.Web.BackOffice/Controllers/ContentTypeController.cs b/src/Umbraco.Web.BackOffice/Controllers/ContentTypeController.cs index 4dcd53f744..4f90f2759d 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/ContentTypeController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/ContentTypeController.cs @@ -311,63 +311,6 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers return ValidationErrorResult.CreateNotificationValidationErrorResult(result.Exception.Message); } - [Authorize(Policy = AuthorizationPolicies.TreeAccessDocumentTypes)] - public CreatedContentTypeCollectionResult PostCreateCollection(int parentId, string collectionName, bool collectionCreateTemplate, string collectionItemName, bool collectionItemCreateTemplate, string collectionIcon, string collectionItemIcon) - { - // create item doctype - var itemDocType = new ContentType(_shortStringHelper, parentId); - itemDocType.Name = collectionItemName; - itemDocType.Alias = collectionItemName.ToSafeAlias(_shortStringHelper, true); - itemDocType.Icon = collectionItemIcon; - - // create item doctype template - if (collectionItemCreateTemplate) - { - var template = CreateTemplateForContentType(itemDocType.Alias, itemDocType.Name); - itemDocType.SetDefaultTemplate(template); - } - - // save item doctype - _contentTypeService.Save(itemDocType); - - // create collection doctype - var collectionDocType = new ContentType(_shortStringHelper, parentId); - collectionDocType.Name = collectionName; - collectionDocType.Alias = collectionName.ToSafeAlias(_shortStringHelper, 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 - _contentTypeService.Save(collectionDocType); - - // test if the parent exist and then allow the collection underneath - var parentCt = _contentTypeService.Get(parentId); - if (parentCt != null) - { - var allowedCts = parentCt.AllowedContentTypes.ToList(); - allowedCts.Add(new ContentTypeSort(collectionDocType.Id, allowedCts.Count())); - parentCt.AllowedContentTypes = allowedCts; - _contentTypeService.Save(parentCt); - } - - return new CreatedContentTypeCollectionResult - { - CollectionId = collectionDocType.Id, - ContainerId = itemDocType.Id - }; - } - [Authorize(Policy = AuthorizationPolicies.TreeAccessDocumentTypes)] public ActionResult PostSave(DocumentTypeSave contentTypeSave) { @@ -642,7 +585,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers foreach (var formFile in file) { - var fileName = formFile.FileName.Trim('\"'); + var fileName = formFile.FileName.Trim(Constants.CharArrays.DoubleQuote); var ext = fileName.Substring(fileName.LastIndexOf('.') + 1).ToLower(); var root = _hostingEnvironment.MapPathContentRoot(Constants.SystemDirectories.TempFileUploads); diff --git a/src/Umbraco.Web.BackOffice/Controllers/EntityController.cs b/src/Umbraco.Web.BackOffice/Controllers/EntityController.cs index a6bf0a189a..0867a613a5 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/EntityController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/EntityController.cs @@ -6,6 +6,7 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Infrastructure; using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Mapping; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.ContentEditing; @@ -67,6 +68,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers private readonly IMacroService _macroService; private readonly IUserService _userService; private readonly ILocalizationService _localizationService; + private readonly AppCaches _appCaches; public EntityController( ITreeService treeService, @@ -87,7 +89,8 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers IMediaTypeService mediaTypeService, IMacroService macroService, IUserService userService, - ILocalizationService localizationService) + ILocalizationService localizationService, + AppCaches appCaches) { _treeService = treeService ?? throw new ArgumentNullException(nameof(treeService)); _treeSearcher = treeSearcher ?? throw new ArgumentNullException(nameof(treeSearcher)); @@ -112,6 +115,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers _macroService = macroService ?? throw new ArgumentNullException(nameof(macroService)); _userService = userService ?? throw new ArgumentNullException(nameof(userService)); _localizationService = localizationService ?? throw new ArgumentNullException(nameof(localizationService)); + _appCaches = appCaches ?? throw new ArgumentNullException(nameof(appCaches)); } /// @@ -213,7 +217,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers return foundContentResult; } - return new ActionResult>(foundContent.Path.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries).Select(int.Parse)); + return new ActionResult>(foundContent.Path.Split(Constants.CharArrays.Comma, StringSplitOptions.RemoveEmptyEntries).Select(int.Parse)); } /// @@ -231,7 +235,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers return foundContentResult; } - return new ActionResult>(foundContent.Path.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries).Select(int.Parse)); + return new ActionResult>(foundContent.Path.Split(Constants.CharArrays.Comma, StringSplitOptions.RemoveEmptyEntries).Select(int.Parse)); } /// @@ -355,7 +359,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers getPath: nodeid => { var ent = _entityService.Get(nodeid); - return ent.Path.Split(',').Reverse(); + return ent.Path.Split(Constants.CharArrays.Comma).Reverse(); }, publishedContentExists: i => _publishedContentQuery.Content(i) != null); } @@ -715,9 +719,9 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers switch (type) { case UmbracoEntityTypes.Document: - return _backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser.CalculateContentStartNodeIds(_entityService); + return _backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser.CalculateContentStartNodeIds(_entityService, _appCaches); case UmbracoEntityTypes.Media: - return _backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser.CalculateMediaStartNodeIds(_entityService); + return _backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser.CalculateMediaStartNodeIds(_entityService, _appCaches); default: return Array.Empty(); } @@ -847,7 +851,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers { // TODO: Need to check for Object types that support hierarchic here, some might not. - var ids = _entityService.Get(id).Path.Split(',').Select(int.Parse).Distinct().ToArray(); + var ids = _entityService.Get(id).Path.Split(Constants.CharArrays.Comma).Select(int.Parse).Distinct().ToArray(); var ignoreUserStartNodes = IsDataTypeIgnoringUserStartNodes(queryStrings?.GetValue("dataTypeId")); if (ignoreUserStartNodes == false) @@ -856,10 +860,10 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers switch (entityType) { case UmbracoEntityTypes.Document: - aids = _backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser.CalculateContentStartNodeIds(_entityService); + aids = _backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser.CalculateContentStartNodeIds(_entityService, _appCaches); break; case UmbracoEntityTypes.Media: - aids = _backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser.CalculateMediaStartNodeIds(_entityService); + aids = _backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser.CalculateMediaStartNodeIds(_entityService, _appCaches); break; } @@ -1159,7 +1163,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers { if (postFilter.IsNullOrWhiteSpace()) return entities; - var postFilterConditions = postFilter.Split('&'); + var postFilterConditions = postFilter.Split(Constants.CharArrays.Ampersand); foreach (var postFilterCondition in postFilterConditions) { @@ -1176,9 +1180,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers return entities; } - private static QueryCondition BuildQueryCondition(string postFilter) - { - var postFilterParts = postFilter.Split(new[] + private static readonly string[] _postFilterSplitStrings = new[] { "=", "==", @@ -1188,7 +1190,10 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers "<", ">=", "<=" - }, 2, StringSplitOptions.RemoveEmptyEntries); + }; + private static QueryCondition BuildQueryCondition(string postFilter) + { + var postFilterParts = postFilter.Split(_postFilterSplitStrings, 2, StringSplitOptions.RemoveEmptyEntries); if (postFilterParts.Length != 2) { diff --git a/src/Umbraco.Web.BackOffice/Controllers/MacrosController.cs b/src/Umbraco.Web.BackOffice/Controllers/MacrosController.cs index 8a1df92c00..001b19fb1c 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/MacrosController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/MacrosController.cs @@ -387,7 +387,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers files.AddRange( fileInfo.Select(file => - prefixVirtualPath.TrimEnd('/') + "/" + (path.Replace(orgPath, string.Empty).Trim('/') + "/" + file.Name).Trim('/'))); + prefixVirtualPath.TrimEnd(Constants.CharArrays.ForwardSlash) + "/" + (path.Replace(orgPath, string.Empty).Trim(Constants.CharArrays.ForwardSlash) + "/" + file.Name).Trim(Constants.CharArrays.ForwardSlash))); } diff --git a/src/Umbraco.Web.BackOffice/Controllers/MediaController.cs b/src/Umbraco.Web.BackOffice/Controllers/MediaController.cs index 8b1e4c8577..3eb8d740bc 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/MediaController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/MediaController.cs @@ -12,6 +12,7 @@ using Microsoft.AspNetCore.Mvc.Infrastructure; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.ContentApps; using Umbraco.Cms.Core.Dictionary; @@ -23,6 +24,7 @@ using Umbraco.Cms.Core.Media; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.ContentEditing; using Umbraco.Cms.Core.Models.Entities; +using Umbraco.Cms.Core.Models.Membership; using Umbraco.Cms.Core.Models.Validation; using Umbraco.Cms.Core.Persistence.Querying; using Umbraco.Cms.Core.PropertyEditors; @@ -69,6 +71,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers private readonly IImageUrlGenerator _imageUrlGenerator; private readonly IJsonSerializer _serializer; private readonly IAuthorizationService _authorizationService; + private readonly AppCaches _appCaches; private readonly ILogger _logger; public MediaController( @@ -92,7 +95,8 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers IHostingEnvironment hostingEnvironment, IImageUrlGenerator imageUrlGenerator, IJsonSerializer serializer, - IAuthorizationService authorizationService) + IAuthorizationService authorizationService, + AppCaches appCaches) : base(cultureDictionary, loggerFactory, shortStringHelper, eventMessages, localizedTextService, serializer) { _shortStringHelper = shortStringHelper; @@ -114,6 +118,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers _imageUrlGenerator = imageUrlGenerator; _serializer = serializer; _authorizationService = authorizationService; + _appCaches = appCaches; } /// @@ -291,7 +296,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers protected int[] UserStartNodes { - get { return _userStartNodes ?? (_userStartNodes = _backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser.CalculateMediaStartNodeIds(_entityService)); } + get { return _userStartNodes ?? (_userStartNodes = _backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser.CalculateMediaStartNodeIds(_entityService, _appCaches)); } } /// @@ -726,7 +731,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers if (!string.IsNullOrEmpty(path)) { - var folders = path.Split('/'); + var folders = path.Split(Constants.CharArrays.ForwardSlash); for (int i = 0; i < folders.Length - 1; i++) { @@ -775,7 +780,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers //get the files foreach (var formFile in file) { - var fileName = formFile.FileName.Trim(new[] { '\"' }).TrimEnd(); + var fileName = formFile.FileName.Trim(Constants.CharArrays.DoubleQuote).TrimEnd(); var safeFileName = fileName.ToSafeFileName(ShortStringHelper); var ext = safeFileName.Substring(safeFileName.LastIndexOf('.') + 1).ToLower(); @@ -975,6 +980,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers return new ActionResult(toMove); } + public PagedResult GetPagedReferences(int id, string entityType, int pageNumber = 1, int pageSize = 100) { if (pageNumber <= 0 || pageSize <= 0) diff --git a/src/Umbraco.Web.BackOffice/Controllers/PackageInstallController.cs b/src/Umbraco.Web.BackOffice/Controllers/PackageInstallController.cs index 578e58ae20..9404e9a292 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/PackageInstallController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/PackageInstallController.cs @@ -21,8 +21,7 @@ using Umbraco.Cms.Web.Common.ActionsResults; using Umbraco.Cms.Web.Common.Attributes; using Umbraco.Cms.Web.Common.Authorization; using Umbraco.Extensions; -using Constants = Umbraco.Cms.Core.Constants; - +using Umbraco.Cms.Core; namespace Umbraco.Cms.Web.BackOffice.Controllers { /// @@ -162,7 +161,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers //get the files foreach (var formFile in file) { - var fileName = formFile.FileName.Trim('\"'); + var fileName = formFile.FileName.Trim(Constants.CharArrays.DoubleQuote); var ext = fileName.Substring(fileName.LastIndexOf('.') + 1).ToLower(); if (ext.InvariantEquals("zip") || ext.InvariantEquals("umb")) diff --git a/src/Umbraco.Web.BackOffice/Controllers/TourController.cs b/src/Umbraco.Web.BackOffice/Controllers/TourController.cs index b77ff2a2cc..607df48701 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/TourController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/TourController.cs @@ -4,6 +4,7 @@ using System.IO; using System.Linq; using Microsoft.Extensions.Options; using Newtonsoft.Json; +using Umbraco.Cms.Core; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Hosting; using Umbraco.Cms.Core.Models; @@ -11,7 +12,6 @@ using Umbraco.Cms.Core.Security; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Tour; using Umbraco.Cms.Web.Common.Attributes; -using Constants = Umbraco.Cms.Core.Constants; namespace Umbraco.Cms.Web.BackOffice.Controllers { @@ -72,7 +72,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers { foreach (var plugin in Directory.EnumerateDirectories(appPlugins)) { - var pluginName = Path.GetFileName(plugin.TrimEnd('\\')); + var pluginName = Path.GetFileName(plugin.TrimEnd(Constants.CharArrays.Backslash)); var pluginFilters = _filters.Where(x => x.PluginName != null && x.PluginName.IsMatch(pluginName)) .ToList(); @@ -154,7 +154,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers { return false; } - var contentTypes = x.ContentType.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries).Select(ct => ct.Trim()); + var contentTypes = x.ContentType.Split(Constants.CharArrays.Comma, StringSplitOptions.RemoveEmptyEntries).Select(ct => ct.Trim()); return contentTypes.Intersect(doctypeAliasWithCompositions).Any(); }); } diff --git a/src/Umbraco.Web.BackOffice/Controllers/UserGroupEditorAuthorizationHelper.cs b/src/Umbraco.Web.BackOffice/Controllers/UserGroupEditorAuthorizationHelper.cs index 4b759cfaa2..0d90dba120 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/UserGroupEditorAuthorizationHelper.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/UserGroupEditorAuthorizationHelper.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using System.Linq; using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.Membership; using Umbraco.Cms.Core.Services; @@ -15,13 +16,15 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers 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; } /// @@ -113,7 +116,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers 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); } } @@ -123,7 +126,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers 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.BackOffice/Controllers/UserGroupsController.cs b/src/Umbraco.Web.BackOffice/Controllers/UserGroupsController.cs index e7f90bf521..ea6c3d9c60 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/UserGroupsController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/UserGroupsController.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Mapping; using Umbraco.Cms.Core.Models.ContentEditing; using Umbraco.Cms.Core.Models.Membership; @@ -31,11 +32,13 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers private readonly UmbracoMapper _umbracoMapper; private readonly ILocalizedTextService _localizedTextService; private readonly IShortStringHelper _shortStringHelper; + private readonly AppCaches _appCaches; public UserGroupsController(IUserService userService, IContentService contentService, IEntityService entityService, IMediaService mediaService, IBackOfficeSecurityAccessor backofficeSecurityAccessor, UmbracoMapper umbracoMapper, ILocalizedTextService localizedTextService, - IShortStringHelper shortStringHelper) + IShortStringHelper shortStringHelper, + AppCaches appCaches) { _userService = userService ?? throw new ArgumentNullException(nameof(userService)); _contentService = contentService ?? throw new ArgumentNullException(nameof(contentService)); @@ -46,6 +49,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers _localizedTextService = localizedTextService ?? throw new ArgumentNullException(nameof(localizedTextService)); _shortStringHelper = shortStringHelper ?? throw new ArgumentNullException(nameof(shortStringHelper)); + _appCaches = appCaches ?? throw new ArgumentNullException(nameof(appCaches)); } [UserGroupValidate] @@ -55,7 +59,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers //authorize that the user has access to save this user group var authHelper = new UserGroupEditorAuthorizationHelper( - _userService, _contentService, _mediaService, _entityService); + _userService, _contentService, _mediaService, _entityService, _appCaches); var isAuthorized = authHelper.AuthorizeGroupAccess(_backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser, userGroupSave.Alias); if (isAuthorized == false) diff --git a/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilderExtensions.cs b/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilderExtensions.cs index e28c8e4196..8dd99cf192 100644 --- a/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilderExtensions.cs +++ b/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilderExtensions.cs @@ -1,5 +1,6 @@ using System; using System.Linq; +using Ganss.XSS; using Microsoft.AspNetCore.Authorization; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; @@ -179,6 +180,14 @@ namespace Umbraco.Extensions }); builder.Services.AddUnique(); + builder.Services.AddUnique(_ => + { + var sanitizer = new HtmlSanitizer(); + sanitizer.AllowedAttributes.UnionWith(Constants.SvgSanitizer.Attributes); + sanitizer.AllowedCssProperties.UnionWith(Constants.SvgSanitizer.Attributes); + sanitizer.AllowedTags.UnionWith(Constants.SvgSanitizer.Tags); + return sanitizer; + }); builder.Services.AddUnique(); return builder; diff --git a/src/Umbraco.Web.BackOffice/Filters/CheckIfUserTicketDataIsStaleAttribute.cs b/src/Umbraco.Web.BackOffice/Filters/CheckIfUserTicketDataIsStaleAttribute.cs index 6836931e00..cac031358d 100644 --- a/src/Umbraco.Web.BackOffice/Filters/CheckIfUserTicketDataIsStaleAttribute.cs +++ b/src/Umbraco.Web.BackOffice/Filters/CheckIfUserTicketDataIsStaleAttribute.cs @@ -42,6 +42,7 @@ namespace Umbraco.Cms.Web.BackOffice.Filters private readonly IBackOfficeSignInManager _backOfficeSignInManager; private readonly IBackOfficeAntiforgery _backOfficeAntiforgery; private readonly IScopeProvider _scopeProvider; + private readonly AppCaches _appCaches; public CheckIfUserTicketDataIsStaleFilter( IRequestCache requestCache, @@ -52,7 +53,8 @@ namespace Umbraco.Cms.Web.BackOffice.Filters IOptions globalSettings, IBackOfficeSignInManager backOfficeSignInManager, IBackOfficeAntiforgery backOfficeAntiforgery, - IScopeProvider scopeProvider) + IScopeProvider scopeProvider, + AppCaches appCaches) { _requestCache = requestCache; _umbracoMapper = umbracoMapper; @@ -63,6 +65,7 @@ namespace Umbraco.Cms.Web.BackOffice.Filters _backOfficeSignInManager = backOfficeSignInManager; _backOfficeAntiforgery = backOfficeAntiforgery; _scopeProvider = scopeProvider; + _appCaches = appCaches; } @@ -142,12 +145,12 @@ namespace Umbraco.Cms.Web.BackOffice.Filters () => user.Groups.Select(x => x.Alias).UnsortedSequenceEqual(identity.GetRoles()) == false, () => { - var startContentIds = user.CalculateContentStartNodeIds(_entityService); + var startContentIds = user.CalculateContentStartNodeIds(_entityService, _appCaches); return startContentIds.UnsortedSequenceEqual(identity.GetStartContentNodes()) == false; }, () => { - var startMediaIds = user.CalculateMediaStartNodeIds(_entityService); + var startMediaIds = user.CalculateMediaStartNodeIds(_entityService, _appCaches); return startMediaIds.UnsortedSequenceEqual(identity.GetStartMediaNodes()) == false; } }; diff --git a/src/Umbraco.Web.BackOffice/Filters/FilterAllowedOutgoingContentAttribute.cs b/src/Umbraco.Web.BackOffice/Filters/FilterAllowedOutgoingContentAttribute.cs index 44ccfcb115..5056357781 100644 --- a/src/Umbraco.Web.BackOffice/Filters/FilterAllowedOutgoingContentAttribute.cs +++ b/src/Umbraco.Web.BackOffice/Filters/FilterAllowedOutgoingContentAttribute.cs @@ -5,6 +5,7 @@ using System.Globalization; using System.Linq; using Microsoft.AspNetCore.Mvc; using Umbraco.Cms.Core.Actions; +using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.Membership; using Umbraco.Cms.Core.Security; @@ -53,20 +54,18 @@ namespace Umbraco.Cms.Web.BackOffice.Filters } internal sealed class FilterAllowedOutgoingContentFilter : FilterAllowedOutgoingMediaFilter { + private readonly char _permissionToCheck; private readonly IUserService _userService; private readonly IEntityService _entityService; - private readonly char _permissionToCheck; + private readonly AppCaches _appCaches; - - - public FilterAllowedOutgoingContentFilter(Type outgoingType, string propertyName, char permissionToCheck, IUserService userService, IEntityService entityService, IBackOfficeSecurityAccessor backofficeSecurityAccessor) - : base(entityService, backofficeSecurityAccessor, outgoingType, propertyName) + public FilterAllowedOutgoingContentFilter(Type outgoingType, string propertyName, char permissionToCheck, IUserService userService, IEntityService entityService, AppCaches appCaches, IBackOfficeSecurityAccessor backofficeSecurityAccessor) + : base(entityService, backofficeSecurityAccessor, appCaches, outgoingType, propertyName) { - _userService = userService ?? throw new ArgumentNullException(nameof(userService)); - _entityService = entityService ?? throw new ArgumentNullException(nameof(entityService)); + _permissionToCheck = permissionToCheck; _userService = userService; _entityService = entityService; - _permissionToCheck = permissionToCheck; + _appCaches = appCaches; } protected override void FilterItems(IUser user, IList items) @@ -78,7 +77,7 @@ namespace Umbraco.Cms.Web.BackOffice.Filters protected override int[] GetUserStartNodes(IUser user) { - return user.CalculateContentStartNodeIds(_entityService); + return user.CalculateContentStartNodeIds(_entityService, _appCaches); } protected override int RecycleBinId diff --git a/src/Umbraco.Web.BackOffice/Filters/FilterAllowedOutgoingMediaAttribute.cs b/src/Umbraco.Web.BackOffice/Filters/FilterAllowedOutgoingMediaAttribute.cs index 23ce54df4e..d9f44e733c 100644 --- a/src/Umbraco.Web.BackOffice/Filters/FilterAllowedOutgoingMediaAttribute.cs +++ b/src/Umbraco.Web.BackOffice/Filters/FilterAllowedOutgoingMediaAttribute.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.Linq; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Filters; +using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Composing; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.Membership; @@ -35,12 +36,19 @@ namespace Umbraco.Cms.Web.BackOffice.Filters private readonly Type _outgoingType; private readonly IEntityService _entityService; private readonly IBackOfficeSecurityAccessor _backofficeSecurityAccessor; + private readonly AppCaches _appCaches; private readonly string _propertyName; - public FilterAllowedOutgoingMediaFilter(IEntityService entityService, IBackOfficeSecurityAccessor backofficeSecurityAccessor, Type outgoingType, string propertyName) + public FilterAllowedOutgoingMediaFilter( + IEntityService entityService, + IBackOfficeSecurityAccessor backofficeSecurityAccessor, + AppCaches appCaches, + Type outgoingType, + string propertyName) { _entityService = entityService ?? throw new ArgumentNullException(nameof(entityService)); _backofficeSecurityAccessor = backofficeSecurityAccessor ?? throw new ArgumentNullException(nameof(backofficeSecurityAccessor)); + _appCaches = appCaches; _propertyName = propertyName; _outgoingType = outgoingType; @@ -48,7 +56,7 @@ namespace Umbraco.Cms.Web.BackOffice.Filters protected virtual int[] GetUserStartNodes(IUser user) { - return user.CalculateMediaStartNodeIds(_entityService); + return user.CalculateMediaStartNodeIds(_entityService, _appCaches); } protected virtual int RecycleBinId => Constants.System.RecycleBinMedia; diff --git a/src/Umbraco.Web.BackOffice/Mapping/ContentMapDefinition.cs b/src/Umbraco.Web.BackOffice/Mapping/ContentMapDefinition.cs index 797a8ed368..c56e277509 100644 --- a/src/Umbraco.Web.BackOffice/Mapping/ContentMapDefinition.cs +++ b/src/Umbraco.Web.BackOffice/Mapping/ContentMapDefinition.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; using Microsoft.Extensions.Logging; using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Dictionary; using Umbraco.Cms.Core.Mapping; using Umbraco.Cms.Core.Models; @@ -42,6 +43,7 @@ namespace Umbraco.Cms.Web.BackOffice.Mapping private readonly IVariationContextAccessor _variationContextAccessor; private readonly IPublishedUrlProvider _publishedUrlProvider; private readonly UriUtility _uriUtility; + private readonly AppCaches _appCaches; private readonly TabsAndPropertiesMapper _tabsAndPropertiesMapper; private readonly ContentSavedStateMapper _stateMapper; private readonly ContentBasicSavedStateMapper _basicStateMapper; @@ -66,7 +68,8 @@ namespace Umbraco.Cms.Web.BackOffice.Mapping UriUtility uriUtility, IPublishedUrlProvider publishedUrlProvider, IEntityService entityService, - IBackOfficeSecurityAccessor backOfficeSecurityAccessor) + IBackOfficeSecurityAccessor backOfficeSecurityAccessor, + AppCaches appCaches) { _commonMapper = commonMapper; _commonTreeNodeMapper = commonTreeNodeMapper; @@ -85,6 +88,7 @@ namespace Umbraco.Cms.Web.BackOffice.Mapping _variationContextAccessor = variationContextAccessor; _uriUtility = uriUtility; _publishedUrlProvider = publishedUrlProvider; + _appCaches = appCaches; _tabsAndPropertiesMapper = new TabsAndPropertiesMapper(cultureDictionary, localizedTextService, contentTypeBaseServiceProvider); _stateMapper = new ContentSavedStateMapper(); @@ -282,7 +286,7 @@ namespace Umbraco.Cms.Web.BackOffice.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 @@ -297,7 +301,7 @@ namespace Umbraco.Cms.Web.BackOffice.Mapping if (parent == null) return false; - var pathParts = parent.Path.Split(',').Select(x => int.TryParse(x, out var i) ? i : 0).ToList(); + var pathParts = parent.Path.Split(Constants.CharArrays.Comma).Select(x => int.TryParse(x, out var i) ? i : 0).ToList(); // reduce the path parts so we exclude top level content items that // are higher up than a user's start nodes diff --git a/src/Umbraco.Web.BackOffice/ModelBinders/ContentModelBinderHelper.cs b/src/Umbraco.Web.BackOffice/ModelBinders/ContentModelBinderHelper.cs index 55d2be84a7..1e6e6eb1ba 100644 --- a/src/Umbraco.Web.BackOffice/ModelBinders/ContentModelBinderHelper.cs +++ b/src/Umbraco.Web.BackOffice/ModelBinders/ContentModelBinderHelper.cs @@ -54,7 +54,7 @@ namespace Umbraco.Cms.Web.BackOffice.ModelBinders { //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 = formFile.Name.Trim('\"').Split('_'); + var parts = formFile.Name.Trim(Constants.CharArrays.DoubleQuote).Split(Constants.CharArrays.Underscore); if (parts.Length < 2) { bindingContext.HttpContext.SetReasonPhrase( "The request was not formatted correctly the file name's must be underscore delimited"); @@ -88,7 +88,7 @@ namespace Umbraco.Cms.Web.BackOffice.ModelBinders // TODO: anything after 4 parts we can put in metadata - var fileName = formFile.FileName.Trim('\"'); + var fileName = formFile.FileName.Trim(Constants.CharArrays.DoubleQuote); var tempFileUploadFolder = hostingEnvironment.MapPathContentRoot(Constants.SystemDirectories.TempFileUploads); Directory.CreateDirectory(tempFileUploadFolder); diff --git a/src/Umbraco.Web.BackOffice/Security/BackOfficeSecureDataFormat.cs b/src/Umbraco.Web.BackOffice/Security/BackOfficeSecureDataFormat.cs index c365273cbe..d2064a8ed9 100644 --- a/src/Umbraco.Web.BackOffice/Security/BackOfficeSecureDataFormat.cs +++ b/src/Umbraco.Web.BackOffice/Security/BackOfficeSecureDataFormat.cs @@ -11,12 +11,12 @@ namespace Umbraco.Cms.Web.BackOffice.Security /// internal class BackOfficeSecureDataFormat : ISecureDataFormat { - private readonly int _loginTimeoutMinutes; + private readonly TimeSpan _loginTimeout; private readonly ISecureDataFormat _ticketDataFormat; - public BackOfficeSecureDataFormat(int loginTimeoutMinutes, ISecureDataFormat ticketDataFormat) + public BackOfficeSecureDataFormat(TimeSpan loginTimeout, ISecureDataFormat ticketDataFormat) { - _loginTimeoutMinutes = loginTimeoutMinutes; + _loginTimeout = loginTimeout; _ticketDataFormat = ticketDataFormat ?? throw new ArgumentNullException(nameof(ticketDataFormat)); } @@ -27,7 +27,7 @@ namespace Umbraco.Cms.Web.BackOffice.Security new AuthenticationProperties(data.Properties.Items) { IssuedUtc = data.Properties.IssuedUtc, - ExpiresUtc = data.Properties.ExpiresUtc ?? DateTimeOffset.UtcNow.AddMinutes(_loginTimeoutMinutes), + ExpiresUtc = data.Properties.ExpiresUtc ?? DateTimeOffset.UtcNow.Add(_loginTimeout), AllowRefresh = data.Properties.AllowRefresh, IsPersistent = data.Properties.IsPersistent, RedirectUri = data.Properties.RedirectUri diff --git a/src/Umbraco.Web.BackOffice/Security/ConfigureBackOfficeCookieOptions.cs b/src/Umbraco.Web.BackOffice/Security/ConfigureBackOfficeCookieOptions.cs index 9e9549134e..024ee50aaf 100644 --- a/src/Umbraco.Web.BackOffice/Security/ConfigureBackOfficeCookieOptions.cs +++ b/src/Umbraco.Web.BackOffice/Security/ConfigureBackOfficeCookieOptions.cs @@ -89,7 +89,7 @@ namespace Umbraco.Cms.Web.BackOffice.Security public void Configure(CookieAuthenticationOptions options) { options.SlidingExpiration = true; - options.ExpireTimeSpan = TimeSpan.FromMinutes(_globalSettings.TimeOutInMinutes); + options.ExpireTimeSpan = _globalSettings.TimeOut; options.Cookie.Domain = _securitySettings.AuthCookieDomain; options.Cookie.Name = _securitySettings.AuthCookieName; options.Cookie.HttpOnly = true; @@ -109,7 +109,7 @@ namespace Umbraco.Cms.Web.BackOffice.Security IDataProtector dataProtector = options.DataProtectionProvider.CreateProtector("Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationMiddleware", Constants.Security.BackOfficeAuthenticationType, "v2"); var ticketDataFormat = new TicketDataFormat(dataProtector); - options.TicketDataFormat = new BackOfficeSecureDataFormat(_globalSettings.TimeOutInMinutes, ticketDataFormat); + options.TicketDataFormat = new BackOfficeSecureDataFormat(_globalSettings.TimeOut, ticketDataFormat); // Custom cookie manager so we can filter requests options.CookieManager = new BackOfficeCookieManager( diff --git a/src/Umbraco.Web.BackOffice/Security/ConfigureBackOfficeSecurityStampValidatorOptions.cs b/src/Umbraco.Web.BackOffice/Security/ConfigureBackOfficeSecurityStampValidatorOptions.cs index 88099b4c6e..1d3dcca0c1 100644 --- a/src/Umbraco.Web.BackOffice/Security/ConfigureBackOfficeSecurityStampValidatorOptions.cs +++ b/src/Umbraco.Web.BackOffice/Security/ConfigureBackOfficeSecurityStampValidatorOptions.cs @@ -10,7 +10,7 @@ namespace Umbraco.Cms.Web.BackOffice.Security { public void Configure(BackOfficeSecurityStampValidatorOptions options) { - options.ValidationInterval = TimeSpan.FromMinutes(30); + options.ValidationInterval = TimeSpan.FromMinutes(3); } } diff --git a/src/Umbraco.Web.BackOffice/Services/IconService.cs b/src/Umbraco.Web.BackOffice/Services/IconService.cs index e80fe24894..b3423e55c6 100644 --- a/src/Umbraco.Web.BackOffice/Services/IconService.cs +++ b/src/Umbraco.Web.BackOffice/Services/IconService.cs @@ -8,7 +8,6 @@ using Umbraco.Cms.Core.Hosting; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Services; using Umbraco.Extensions; -using Constants = Umbraco.Cms.Core.Constants; namespace Umbraco.Cms.Web.BackOffice.Services { @@ -16,32 +15,28 @@ namespace Umbraco.Cms.Web.BackOffice.Services { private readonly IOptions _globalSettings; private readonly IHostingEnvironment _hostingEnvironment; + private readonly IHtmlSanitizer _htmlSanitizer; - public IconService(IOptions globalSettings, IHostingEnvironment hostingEnvironment) + public IconService( + IOptions globalSettings, + IHostingEnvironment hostingEnvironment, + IHtmlSanitizer htmlSanitizer) { _globalSettings = globalSettings; _hostingEnvironment = hostingEnvironment; + _htmlSanitizer = htmlSanitizer; } /// public IList GetAllIcons() { - var icons = new List(); var directory = new DirectoryInfo(_hostingEnvironment.MapPathWebRoot($"{_globalSettings.Value.IconsPath}/")); var iconNames = directory.GetFiles("*.svg"); - iconNames.OrderBy(f => f.Name).ToList().ForEach(iconInfo => - { - var icon = GetIcon(iconInfo); + return iconNames.OrderBy(f => f.Name) + .Select(iconInfo => GetIcon(iconInfo)).WhereNotNull().ToList(); - if (icon != null) - { - icons.Add(icon); - } - }); - - return icons; } /// @@ -72,15 +67,10 @@ namespace Umbraco.Cms.Web.BackOffice.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 sanitizedString = _htmlSanitizer.Sanitize(svgContent); var svg = new IconModel { diff --git a/src/Umbraco.Web.BackOffice/Trees/ContentTreeController.cs b/src/Umbraco.Web.BackOffice/Trees/ContentTreeController.cs index 726c9612a6..6f50182092 100644 --- a/src/Umbraco.Web.BackOffice/Trees/ContentTreeController.cs +++ b/src/Umbraco.Web.BackOffice/Trees/ContentTreeController.cs @@ -7,6 +7,7 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Actions; +using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Mail; using Umbraco.Cms.Core.Models; @@ -41,6 +42,7 @@ namespace Umbraco.Cms.Web.BackOffice.Trees private readonly IUserService _userService; private readonly ILocalizationService _localizationService; private readonly IEmailSender _emailSender; + private readonly AppCaches _appCaches; public ContentTreeController( ILocalizedTextService localizedTextService, @@ -58,8 +60,9 @@ namespace Umbraco.Cms.Web.BackOffice.Trees IPublicAccessService publicAccessService, ILocalizationService localizationService, IEventAggregator eventAggregator, - IEmailSender emailSender) - : base(localizedTextService, umbracoApiControllerTypeCollection, menuItemCollectionFactory, entityService, backofficeSecurityAccessor, logger, actionCollection, userService, dataTypeService, eventAggregator) + IEmailSender emailSender, + AppCaches appCaches) + : base(localizedTextService, umbracoApiControllerTypeCollection, menuItemCollectionFactory, entityService, backofficeSecurityAccessor, logger, actionCollection, userService, dataTypeService, eventAggregator, appCaches) { _treeSearcher = treeSearcher; _actions = actions; @@ -71,6 +74,7 @@ namespace Umbraco.Cms.Web.BackOffice.Trees _userService = userService; _localizationService = localizationService; _emailSender = emailSender; + _appCaches = appCaches; } protected override int RecycleBinId => Constants.System.RecycleBinContent; @@ -80,7 +84,7 @@ namespace Umbraco.Cms.Web.BackOffice.Trees private int[] _userStartNodes; protected override int[] UserStartNodes - => _userStartNodes ?? (_userStartNodes = _backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser.CalculateContentStartNodeIds(_entityService)); + => _userStartNodes ?? (_userStartNodes = _backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser.CalculateContentStartNodeIds(_entityService, _appCaches)); @@ -164,7 +168,7 @@ namespace Umbraco.Cms.Web.BackOffice.Trees //these two are the standard items menu.Items.Add(LocalizedTextService, opensDialog: true); - menu.Items.Add(LocalizedTextService, true); + menu.Items.Add(LocalizedTextService, true, opensDialog: true); //filter the standard items FilterUserAllowedMenuItems(menu, nodeActions); @@ -194,7 +198,7 @@ namespace Umbraco.Cms.Web.BackOffice.Trees } //if the user has no path access for this node, all they can do is refresh - if (!_backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser.HasContentPathAccess(item, _entityService)) + if (!_backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser.HasContentPathAccess(item, _entityService, _appCaches)) { var menu = _menuItemCollectionFactory.Create(); menu.Items.Add(new RefreshNode(LocalizedTextService, true)); @@ -204,7 +208,7 @@ namespace Umbraco.Cms.Web.BackOffice.Trees var nodeMenu = GetAllNodeMenuItems(item); //if the content node is in the recycle bin, don't have a default menu, just show the regular menu - if (item.Path.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries).Contains(RecycleBinId.ToInvariantString())) + if (item.Path.Split(Constants.CharArrays.Comma, StringSplitOptions.RemoveEmptyEntries).Contains(RecycleBinId.ToInvariantString())) { nodeMenu.DefaultMenuAlias = null; nodeMenu = GetNodeMenuItemsForDeletedContent(item); @@ -270,7 +274,7 @@ namespace Umbraco.Cms.Web.BackOffice.Trees AddActionNode(item, menu, opensDialog: true); AddActionNode(item, menu, true, opensDialog: true); AddActionNode(item, menu, opensDialog: true); - AddActionNode(item, menu, true); + AddActionNode(item, menu, true, opensDialog: true); AddActionNode(item, menu, opensDialog: true); AddActionNode(item, menu, opensDialog: true); AddActionNode(item, menu, true, opensDialog: true); diff --git a/src/Umbraco.Web.BackOffice/Trees/ContentTreeControllerBase.cs b/src/Umbraco.Web.BackOffice/Trees/ContentTreeControllerBase.cs index 447c896b8a..17b1722e8c 100644 --- a/src/Umbraco.Web.BackOffice/Trees/ContentTreeControllerBase.cs +++ b/src/Umbraco.Web.BackOffice/Trees/ContentTreeControllerBase.cs @@ -7,6 +7,7 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Actions; +using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.Entities; @@ -28,6 +29,7 @@ namespace Umbraco.Cms.Web.BackOffice.Trees private readonly ActionCollection _actionCollection; private readonly IUserService _userService; private readonly IDataTypeService _dataTypeService; + private readonly AppCaches _appCaches; public IMenuItemCollectionFactory MenuItemCollectionFactory { get; } @@ -41,7 +43,8 @@ namespace Umbraco.Cms.Web.BackOffice.Trees ActionCollection actionCollection, IUserService userService, IDataTypeService dataTypeService, - IEventAggregator eventAggregator + IEventAggregator eventAggregator, + AppCaches appCaches ) : base(localizedTextService, umbracoApiControllerTypeCollection, eventAggregator) { @@ -51,6 +54,7 @@ namespace Umbraco.Cms.Web.BackOffice.Trees _actionCollection = actionCollection; _userService = userService; _dataTypeService = dataTypeService; + _appCaches = appCaches; MenuItemCollectionFactory = menuItemCollectionFactory; } @@ -151,12 +155,12 @@ namespace Umbraco.Cms.Web.BackOffice.Trees switch (RecycleBinId) { case Constants.System.RecycleBinMedia: - startNodeIds = _backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser.CalculateMediaStartNodeIds(_entityService); - startNodePaths = _backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser.GetMediaStartNodePaths(_entityService); + startNodeIds = _backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser.CalculateMediaStartNodeIds(_entityService, _appCaches); + startNodePaths = _backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser.GetMediaStartNodePaths(_entityService, _appCaches); break; case Constants.System.RecycleBinContent: - startNodeIds = _backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser.CalculateContentStartNodeIds(_entityService); - startNodePaths = _backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser.GetContentStartNodePaths(_entityService); + startNodeIds = _backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser.CalculateContentStartNodeIds(_entityService, _appCaches); + startNodePaths = _backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser.GetContentStartNodePaths(_entityService, _appCaches); break; default: throw new NotSupportedException("Path access is only determined on content or media"); @@ -322,8 +326,8 @@ namespace Umbraco.Cms.Web.BackOffice.Trees { if (entity == null) return false; return RecycleBinId == Constants.System.RecycleBinContent - ? _backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser.HasContentPathAccess(entity, _entityService) - : _backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser.HasMediaPathAccess(entity, _entityService); + ? _backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser.HasContentPathAccess(entity, _entityService, _appCaches) + : _backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser.HasMediaPathAccess(entity, _entityService, _appCaches); } /// @@ -406,7 +410,7 @@ namespace Umbraco.Cms.Web.BackOffice.Trees if (startNodes.Any(x => { - var pathParts = x.Path.Split(','); + var pathParts = x.Path.Split(Constants.CharArrays.Comma); return pathParts.Contains(e.Id.ToInvariantString()); })) { diff --git a/src/Umbraco.Web.BackOffice/Trees/DataTypeTreeController.cs b/src/Umbraco.Web.BackOffice/Trees/DataTypeTreeController.cs index 758feece07..98d65562e2 100644 --- a/src/Umbraco.Web.BackOffice/Trees/DataTypeTreeController.cs +++ b/src/Umbraco.Web.BackOffice/Trees/DataTypeTreeController.cs @@ -53,7 +53,7 @@ namespace Umbraco.Cms.Web.BackOffice.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.BackOffice/Trees/DictionaryTreeController.cs b/src/Umbraco.Web.BackOffice/Trees/DictionaryTreeController.cs index 7dc1f386fe..0722a095b3 100644 --- a/src/Umbraco.Web.BackOffice/Trees/DictionaryTreeController.cs +++ b/src/Umbraco.Web.BackOffice/Trees/DictionaryTreeController.cs @@ -86,7 +86,7 @@ namespace Umbraco.Cms.Web.BackOffice.Trees id, queryStrings, x.ItemKey, - "icon-book-alt", + Constants.Icons.Dictionary, _localizationService.GetDictionaryItemChildren(x.Key).Any()))); } else @@ -102,7 +102,7 @@ namespace Umbraco.Cms.Web.BackOffice.Trees id, queryStrings, x.ItemKey, - "icon-book-alt", + Constants.Icons.Dictionary, _localizationService.GetDictionaryItemChildren(x.Key).Any()))); } diff --git a/src/Umbraco.Web.BackOffice/Trees/FileSystemTreeController.cs b/src/Umbraco.Web.BackOffice/Trees/FileSystemTreeController.cs index 9ee334013a..d20c400e7f 100644 --- a/src/Umbraco.Web.BackOffice/Trees/FileSystemTreeController.cs +++ b/src/Umbraco.Web.BackOffice/Trees/FileSystemTreeController.cs @@ -78,7 +78,7 @@ namespace Umbraco.Cms.Web.BackOffice.Trees if (Extensions.Contains("*")) return true; - return extension != null && Extensions.Contains(extension.Trim('.'), StringComparer.InvariantCultureIgnoreCase); + return extension != null && Extensions.Contains(extension.Trim(Constants.CharArrays.Period), StringComparer.InvariantCultureIgnoreCase); }); foreach (var file in files) diff --git a/src/Umbraco.Web.BackOffice/Trees/LanguageTreeController.cs b/src/Umbraco.Web.BackOffice/Trees/LanguageTreeController.cs index a0a80a8206..93ddd37bfb 100644 --- a/src/Umbraco.Web.BackOffice/Trees/LanguageTreeController.cs +++ b/src/Umbraco.Web.BackOffice/Trees/LanguageTreeController.cs @@ -52,7 +52,7 @@ namespace Umbraco.Cms.Web.BackOffice.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.BackOffice/Trees/LogViewerTreeController.cs b/src/Umbraco.Web.BackOffice/Trees/LogViewerTreeController.cs index e24fcfb09e..29424c5e12 100644 --- a/src/Umbraco.Web.BackOffice/Trees/LogViewerTreeController.cs +++ b/src/Umbraco.Web.BackOffice/Trees/LogViewerTreeController.cs @@ -51,8 +51,8 @@ namespace Umbraco.Cms.Web.BackOffice.Trees var root = rootResult.Value; //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.BackOffice/Trees/MediaTreeController.cs b/src/Umbraco.Web.BackOffice/Trees/MediaTreeController.cs index b260281f50..2ff354410c 100644 --- a/src/Umbraco.Web.BackOffice/Trees/MediaTreeController.cs +++ b/src/Umbraco.Web.BackOffice/Trees/MediaTreeController.cs @@ -7,6 +7,7 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Actions; +using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.ContentEditing; @@ -32,6 +33,7 @@ namespace Umbraco.Cms.Web.BackOffice.Trees { private readonly UmbracoTreeSearcher _treeSearcher; private readonly IMediaService _mediaService; + private readonly AppCaches _appCaches; private readonly IEntityService _entityService; private readonly IBackOfficeSecurityAccessor _backofficeSecurityAccessor; @@ -47,11 +49,13 @@ namespace Umbraco.Cms.Web.BackOffice.Trees IDataTypeService dataTypeService, UmbracoTreeSearcher treeSearcher, IMediaService mediaService, - IEventAggregator eventAggregator) - : base(localizedTextService, umbracoApiControllerTypeCollection, menuItemCollectionFactory, entityService, backofficeSecurityAccessor, logger, actionCollection, userService, dataTypeService, eventAggregator) + IEventAggregator eventAggregator, + AppCaches appCaches) + : base(localizedTextService, umbracoApiControllerTypeCollection, menuItemCollectionFactory, entityService, backofficeSecurityAccessor, logger, actionCollection, userService, dataTypeService, eventAggregator, appCaches) { _treeSearcher = treeSearcher; _mediaService = mediaService; + _appCaches = appCaches; _entityService = entityService; _backofficeSecurityAccessor = backofficeSecurityAccessor; } @@ -62,7 +66,7 @@ namespace Umbraco.Cms.Web.BackOffice.Trees private int[] _userStartNodes; protected override int[] UserStartNodes - => _userStartNodes ?? (_userStartNodes = _backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser.CalculateMediaStartNodeIds(_entityService)); + => _userStartNodes ?? (_userStartNodes = _backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser.CalculateMediaStartNodeIds(_entityService, _appCaches)); /// /// Creates a tree node for a content item based on an UmbracoEntity @@ -113,7 +117,7 @@ namespace Umbraco.Cms.Web.BackOffice.Trees // root actions menu.Items.Add(LocalizedTextService, opensDialog: true); - menu.Items.Add(LocalizedTextService, true); + menu.Items.Add(LocalizedTextService, true, opensDialog: true); menu.Items.Add(new RefreshNode(LocalizedTextService, true)); return menu; } @@ -129,7 +133,7 @@ namespace Umbraco.Cms.Web.BackOffice.Trees } //if the user has no path access for this node, all they can do is refresh - if (!_backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser.HasMediaPathAccess(item, _entityService)) + if (!_backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser.HasMediaPathAccess(item, _entityService, _appCaches)) { menu.Items.Add(new RefreshNode(LocalizedTextService, true)); return menu; @@ -137,7 +141,7 @@ namespace Umbraco.Cms.Web.BackOffice.Trees //if the media item is in the recycle bin, we don't have a default menu and we need to show a limited menu - if (item.Path.Split(new[] {','}, StringSplitOptions.RemoveEmptyEntries).Contains(RecycleBinId.ToInvariantString())) + if (item.Path.Split(Constants.CharArrays.Comma, StringSplitOptions.RemoveEmptyEntries).Contains(RecycleBinId.ToInvariantString())) { menu.Items.Add(LocalizedTextService, opensDialog: true); menu.Items.Add(LocalizedTextService, opensDialog: true); diff --git a/src/Umbraco.Web.BackOffice/Trees/PackagesTreeController.cs b/src/Umbraco.Web.BackOffice/Trees/PackagesTreeController.cs index e54e33bcb6..cb915f545b 100644 --- a/src/Umbraco.Web.BackOffice/Trees/PackagesTreeController.cs +++ b/src/Umbraco.Web.BackOffice/Trees/PackagesTreeController.cs @@ -46,7 +46,7 @@ namespace Umbraco.Cms.Web.BackOffice.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.BackOffice/Trees/UrlHelperExtensions.cs b/src/Umbraco.Web.BackOffice/Trees/UrlHelperExtensions.cs index 9d996d7dcb..eb63dcf354 100644 --- a/src/Umbraco.Web.BackOffice/Trees/UrlHelperExtensions.cs +++ b/src/Umbraco.Web.BackOffice/Trees/UrlHelperExtensions.cs @@ -27,7 +27,7 @@ namespace Umbraco.Cms.Web.BackOffice.Trees var sb = new StringBuilder("-1"); //split the virtual path and iterate through it - var pathPaths = virtualPath.Split('/'); + var pathPaths = virtualPath.Split(Constants.CharArrays.ForwardSlash); for (var p = 0; p < pathPaths.Length; p++) { diff --git a/src/Umbraco.Web.Common/AspNetCore/AspNetCoreHostingEnvironment.cs b/src/Umbraco.Web.Common/AspNetCore/AspNetCoreHostingEnvironment.cs index df7a75a791..e3ee619ec4 100644 --- a/src/Umbraco.Web.Common/AspNetCore/AspNetCoreHostingEnvironment.cs +++ b/src/Umbraco.Web.Common/AspNetCore/AspNetCoreHostingEnvironment.cs @@ -128,7 +128,7 @@ namespace Umbraco.Cms.Web.Common.AspNetCore return virtualPath; } - string fullPath = ApplicationVirtualPath.EnsureEndsWith('/') + virtualPath.TrimStart('~', '/'); + string fullPath = ApplicationVirtualPath.EnsureEndsWith('/') + virtualPath.TrimStart(Core.Constants.CharArrays.TildeForwardSlash); return fullPath; } diff --git a/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilderExtensions.cs b/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilderExtensions.cs index 6c11b91a95..b658ec25b6 100644 --- a/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilderExtensions.cs +++ b/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilderExtensions.cs @@ -181,9 +181,14 @@ namespace Umbraco.Extensions builder.Services.AddUnique(); builder.Services.AddMiniProfiler(options => - + { // WebProfiler determine and start profiling. We should not use the MiniProfilerMiddleware to also profile - options.ShouldProfile = request => false); + options.ShouldProfile = request => false; + + // 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/"); + }); builder.AddNotificationHandler(); return builder; diff --git a/src/Umbraco.Web.Common/Extensions/FormCollectionExtensions.cs b/src/Umbraco.Web.Common/Extensions/FormCollectionExtensions.cs index 03ec2ce8af..d555446a67 100644 --- a/src/Umbraco.Web.Common/Extensions/FormCollectionExtensions.cs +++ b/src/Umbraco.Web.Common/Extensions/FormCollectionExtensions.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; using System.Text; using Microsoft.AspNetCore.Http; +using Umbraco.Cms.Core; namespace Umbraco.Extensions { @@ -23,7 +24,7 @@ namespace Umbraco.Extensions var builder = new StringBuilder(); foreach (var i in items.Where(i => keysToIgnore.InvariantContains(i.Key) == false)) builder.Append(string.Format("{0}={1}&", i.Key, i.Value)); - return builder.ToString().TrimEnd('&'); + return builder.ToString().TrimEnd(Constants.CharArrays.Ampersand); } /// diff --git a/src/Umbraco.Web.Common/Extensions/ImageCropperTemplateExtensions.cs b/src/Umbraco.Web.Common/Extensions/ImageCropperTemplateExtensions.cs index 0768e965b8..82bb8d2c01 100644 --- a/src/Umbraco.Web.Common/Extensions/ImageCropperTemplateExtensions.cs +++ b/src/Umbraco.Web.Common/Extensions/ImageCropperTemplateExtensions.cs @@ -12,18 +12,22 @@ namespace Umbraco.Extensions /// public static class ImageCropperTemplateExtensions { + + private static readonly JsonSerializerSettings s_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, s_imageCropperValueJsonSerializerSettings); } catch (Exception ex) { @@ -31,7 +35,9 @@ namespace Umbraco.Extensions } } + imageCrops ??= new ImageCropperValue(); return imageCrops; + } } } diff --git a/src/Umbraco.Web.Common/Filters/UmbracoMemberAuthorizeFilter.cs b/src/Umbraco.Web.Common/Filters/UmbracoMemberAuthorizeFilter.cs index 24c82ee23b..d49db02bb9 100644 --- a/src/Umbraco.Web.Common/Filters/UmbracoMemberAuthorizeFilter.cs +++ b/src/Umbraco.Web.Common/Filters/UmbracoMemberAuthorizeFilter.cs @@ -69,7 +69,7 @@ namespace Umbraco.Cms.Web.Common.Filters } var members = new List(); - foreach (var s in AllowMembers.Split(',')) + foreach (var s in AllowMembers.Split(Core.Constants.CharArrays.Comma)) { if (int.TryParse(s, out var id)) { @@ -77,7 +77,7 @@ namespace Umbraco.Cms.Web.Common.Filters } } - return _websiteSecurity.IsMemberAuthorized(AllowType.Split(','), AllowGroup.Split(','), members); + return _websiteSecurity.IsMemberAuthorized(AllowType.Split(Core.Constants.CharArrays.Comma), AllowGroup.Split(Core.Constants.CharArrays.Comma), members); } } } diff --git a/src/Umbraco.Web.Common/Macros/MacroRenderer.cs b/src/Umbraco.Web.Common/Macros/MacroRenderer.cs index 129936071c..0b2b2e6625 100644 --- a/src/Umbraco.Web.Common/Macros/MacroRenderer.cs +++ b/src/Umbraco.Web.Common/Macros/MacroRenderer.cs @@ -375,7 +375,7 @@ namespace Umbraco.Cms.Web.Common.Macros if (attributeValue.StartsWith("[") == false) return attributeValue; - var tokens = attributeValue.Split(',').Select(x => x.Trim()).ToArray(); + var tokens = attributeValue.Split(Core.Constants.CharArrays.Comma).Select(x => x.Trim()).ToArray(); // ensure we only process valid input ie each token must be [?x] and not eg a json array // like [1,2,3] which we don't want to parse - however the last one can be a literal, so diff --git a/src/Umbraco.Web.UI.Client/package-lock.json b/src/Umbraco.Web.UI.Client/package-lock.json index 452b5c2071..fc6376bfcf 100644 --- a/src/Umbraco.Web.UI.Client/package-lock.json +++ b/src/Umbraco.Web.UI.Client/package-lock.json @@ -1842,7 +1842,8 @@ "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "dev": true + "dev": true, + "optional": true }, "base64id": { "version": "1.0.0", @@ -2051,7 +2052,8 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", "integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ=", - "dev": true + "dev": true, + "optional": true }, "got": { "version": "8.3.2", @@ -2129,6 +2131,7 @@ "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-2.0.1.tgz", "integrity": "sha1-2N0ZeVldLcATnh/ka4tkbLPN8Dg=", "dev": true, + "optional": true, "requires": { "p-finally": "^1.0.0" } @@ -2170,6 +2173,7 @@ "resolved": "https://registry.npmjs.org/bl/-/bl-1.2.3.tgz", "integrity": "sha512-pvcNpa0UU69UT341rO6AYy4FVAIkUHuZXRIWbq+zHnsVcRzDDjIAhGuuYoi0d//cwIwtt4pkpKycWEfjdV+vww==", "dev": true, + "optional": true, "requires": { "readable-stream": "^2.3.5", "safe-buffer": "^5.1.1" @@ -2179,13 +2183,15 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", - "dev": true + "dev": true, + "optional": true }, "readable-stream": { "version": "2.3.7", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", "dev": true, + "optional": true, "requires": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", @@ -2201,6 +2207,7 @@ "resolved": "http://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", "dev": true, + "optional": true, "requires": { "safe-buffer": "~5.1.0" } @@ -2341,6 +2348,7 @@ "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", "dev": true, + "optional": true, "requires": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" @@ -2366,7 +2374,8 @@ "version": "0.2.13", "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", "integrity": "sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI=", - "dev": true + "dev": true, + "optional": true }, "buffer-equal": { "version": "1.0.0", @@ -2563,6 +2572,7 @@ "resolved": "https://registry.npmjs.org/caw/-/caw-2.0.1.tgz", "integrity": "sha1-bDygcfwZRyCIPC3F2psHS/x+npU=", "dev": true, + "optional": true, "requires": { "get-proxy": "^2.0.0", "isurl": "^1.0.0-alpha5", @@ -2994,7 +3004,8 @@ "version": "2.20.3", "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", - "dev": true + "dev": true, + "optional": true }, "component-bind": { "version": "1.0.0", @@ -3086,6 +3097,7 @@ "resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.12.tgz", "integrity": "sha1-D96NCRIA616AjK8l/mGMAvSOTvo=", "dev": true, + "optional": true, "requires": { "ini": "^1.3.4", "proto-list": "~1.2.1" @@ -3141,6 +3153,7 @@ "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.3.tgz", "integrity": "sha1-4TDK9+cnkIfFYWwgB9BIVpiYT70=", "dev": true, + "optional": true, "requires": { "safe-buffer": "5.1.2" } @@ -3582,6 +3595,7 @@ "resolved": "https://registry.npmjs.org/decompress/-/decompress-4.2.1.tgz", "integrity": "sha512-e48kc2IjU+2Zw8cTb6VZcJQ3lgVbS4uuB1TfCHbiZIP/haNXm+SVyhu+87jts5/3ROpd82GSVCoNs/z8l4ZOaQ==", "dev": true, + "optional": true, "requires": { "decompress-tar": "^4.0.0", "decompress-tarbz2": "^4.0.0", @@ -3598,6 +3612,7 @@ "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-1.3.0.tgz", "integrity": "sha1-ecEDO4BRW9bSTsmTPoYMp17ifww=", "dev": true, + "optional": true, "requires": { "pify": "^3.0.0" }, @@ -3606,7 +3621,8 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", - "dev": true + "dev": true, + "optional": true } } } @@ -3617,6 +3633,7 @@ "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-3.3.0.tgz", "integrity": "sha1-gKTdMjdIOEv6JICDYirt7Jgq3/M=", "dev": true, + "optional": true, "requires": { "mimic-response": "^1.0.0" } @@ -3626,6 +3643,7 @@ "resolved": "https://registry.npmjs.org/decompress-tar/-/decompress-tar-4.1.1.tgz", "integrity": "sha1-cYy9P8sWIJcW5womuE57pFkuWvE=", "dev": true, + "optional": true, "requires": { "file-type": "^5.2.0", "is-stream": "^1.1.0", @@ -3636,7 +3654,8 @@ "version": "5.2.0", "resolved": "https://registry.npmjs.org/file-type/-/file-type-5.2.0.tgz", "integrity": "sha1-LdvqfHP/42No365J3DOMBYwritY=", - "dev": true + "dev": true, + "optional": true } } }, @@ -3645,6 +3664,7 @@ "resolved": "https://registry.npmjs.org/decompress-tarbz2/-/decompress-tarbz2-4.1.1.tgz", "integrity": "sha1-MIKluIDqQEOBY0nzeLVsUWvho5s=", "dev": true, + "optional": true, "requires": { "decompress-tar": "^4.1.0", "file-type": "^6.1.0", @@ -3657,7 +3677,8 @@ "version": "6.2.0", "resolved": "https://registry.npmjs.org/file-type/-/file-type-6.2.0.tgz", "integrity": "sha1-5QzXXTVv/tTjBtxPW89Sp5kDqRk=", - "dev": true + "dev": true, + "optional": true } } }, @@ -3666,6 +3687,7 @@ "resolved": "https://registry.npmjs.org/decompress-targz/-/decompress-targz-4.1.1.tgz", "integrity": "sha1-wJvDXE0R894J8tLaU+neI+fOHu4=", "dev": true, + "optional": true, "requires": { "decompress-tar": "^4.1.1", "file-type": "^5.2.0", @@ -3676,7 +3698,8 @@ "version": "5.2.0", "resolved": "https://registry.npmjs.org/file-type/-/file-type-5.2.0.tgz", "integrity": "sha1-LdvqfHP/42No365J3DOMBYwritY=", - "dev": true + "dev": true, + "optional": true } } }, @@ -3685,6 +3708,7 @@ "resolved": "https://registry.npmjs.org/decompress-unzip/-/decompress-unzip-4.0.1.tgz", "integrity": "sha1-3qrM39FK6vhVePczroIQ+bSEj2k=", "dev": true, + "optional": true, "requires": { "file-type": "^3.8.0", "get-stream": "^2.2.0", @@ -3696,13 +3720,15 @@ "version": "3.9.0", "resolved": "https://registry.npmjs.org/file-type/-/file-type-3.9.0.tgz", "integrity": "sha1-JXoHg4TR24CHvESdEH1SpSZyuek=", - "dev": true + "dev": true, + "optional": true }, "get-stream": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-2.3.1.tgz", "integrity": "sha1-Xzj5PzRgCWZu4BUKBUFn+Rvdld4=", "dev": true, + "optional": true, "requires": { "object-assign": "^4.0.1", "pinkie-promise": "^2.0.0" @@ -3712,7 +3738,8 @@ "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", - "dev": true + "dev": true, + "optional": true } } }, @@ -4000,7 +4027,8 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", - "dev": true + "dev": true, + "optional": true } } }, @@ -4017,7 +4045,8 @@ "version": "0.1.4", "resolved": "https://registry.npmjs.org/duplexer3/-/duplexer3-0.1.4.tgz", "integrity": "sha1-7gHdHKwO08vH/b6jfcCo8c4ALOI=", - "dev": true + "dev": true, + "optional": true }, "duplexify": { "version": "3.7.1", @@ -4662,6 +4691,7 @@ "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz", "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==", "dev": true, + "optional": true, "requires": { "cross-spawn": "^6.0.0", "get-stream": "^4.0.0", @@ -4803,6 +4833,7 @@ "resolved": "https://registry.npmjs.org/ext-list/-/ext-list-2.2.2.tgz", "integrity": "sha1-C5jmTtgvWs8PKTG6v2khLvUt3Tc=", "dev": true, + "optional": true, "requires": { "mime-db": "^1.28.0" } @@ -4812,6 +4843,7 @@ "resolved": "https://registry.npmjs.org/ext-name/-/ext-name-5.0.0.tgz", "integrity": "sha1-cHgZgdGD7hXROZPIgiBFxQbI8KY=", "dev": true, + "optional": true, "requires": { "ext-list": "^2.0.0", "sort-keys-length": "^1.0.0" @@ -5049,6 +5081,7 @@ "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", "integrity": "sha1-JcfInLH5B3+IkbvmHY85Dq4lbx4=", "dev": true, + "optional": true, "requires": { "pend": "~1.2.0" } @@ -5087,13 +5120,15 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/filename-reserved-regex/-/filename-reserved-regex-2.0.0.tgz", "integrity": "sha1-q/c9+rc10EVECr/qLZHzieu/oik=", - "dev": true + "dev": true, + "optional": true }, "filenamify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/filenamify/-/filenamify-2.1.0.tgz", "integrity": "sha1-iPr0lfsbR6v9YSMAACoWIoxnfuk=", "dev": true, + "optional": true, "requires": { "filename-reserved-regex": "^2.0.0", "strip-outer": "^1.0.0", @@ -5442,7 +5477,8 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", "integrity": "sha1-a+Dem+mYzhavivwkSXue6bfM2a0=", - "dev": true + "dev": true, + "optional": true }, "fs-mkdirp-stream": { "version": "1.0.0", @@ -5489,7 +5525,8 @@ "ansi-regex": { "version": "2.1.1", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "aproba": { "version": "1.2.0", @@ -5510,12 +5547,14 @@ "balanced-match": { "version": "1.0.0", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "brace-expansion": { "version": "1.1.11", "bundled": true, "dev": true, + "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -5530,17 +5569,20 @@ "code-point-at": { "version": "1.1.0", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "concat-map": { "version": "0.0.1", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "console-control-strings": { "version": "1.1.0", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "core-util-is": { "version": "1.0.2", @@ -5657,7 +5699,8 @@ "inherits": { "version": "2.0.3", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "ini": { "version": "1.3.5", @@ -5669,6 +5712,7 @@ "version": "1.0.0", "bundled": true, "dev": true, + "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -5683,6 +5727,7 @@ "version": "3.0.4", "bundled": true, "dev": true, + "optional": true, "requires": { "brace-expansion": "^1.1.7" } @@ -5690,12 +5735,14 @@ "minimist": { "version": "0.0.8", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "minipass": { "version": "2.3.5", "bundled": true, "dev": true, + "optional": true, "requires": { "safe-buffer": "^5.1.2", "yallist": "^3.0.0" @@ -5714,6 +5761,7 @@ "version": "0.5.1", "bundled": true, "dev": true, + "optional": true, "requires": { "minimist": "0.0.8" } @@ -5794,7 +5842,8 @@ "number-is-nan": { "version": "1.0.1", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "object-assign": { "version": "4.1.1", @@ -5806,6 +5855,7 @@ "version": "1.4.0", "bundled": true, "dev": true, + "optional": true, "requires": { "wrappy": "1" } @@ -5891,7 +5941,8 @@ "safe-buffer": { "version": "5.1.2", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "safer-buffer": { "version": "2.1.2", @@ -5927,6 +5978,7 @@ "version": "1.0.2", "bundled": true, "dev": true, + "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -5946,6 +5998,7 @@ "version": "3.0.1", "bundled": true, "dev": true, + "optional": true, "requires": { "ansi-regex": "^2.0.0" } @@ -5989,12 +6042,14 @@ "wrappy": { "version": "1.0.2", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "yallist": { "version": "3.0.3", "bundled": true, - "dev": true + "dev": true, + "optional": true } } }, @@ -6021,6 +6076,7 @@ "resolved": "https://registry.npmjs.org/get-proxy/-/get-proxy-2.1.0.tgz", "integrity": "sha1-NJ8rTZHUTE1NTpy6KtkBQ/rF75M=", "dev": true, + "optional": true, "requires": { "npm-conf": "^1.1.0" } @@ -6029,13 +6085,15 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-4.0.1.tgz", "integrity": "sha1-uWjGsKBDhDJJAui/Gl3zJXmkUP4=", - "dev": true + "dev": true, + "optional": true }, "get-stream": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", "dev": true, + "optional": true, "requires": { "pump": "^3.0.0" }, @@ -6045,6 +6103,7 @@ "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", "dev": true, + "optional": true, "requires": { "end-of-stream": "^1.1.0", "once": "^1.3.1" @@ -6157,7 +6216,8 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true + "dev": true, + "optional": true }, "pump": { "version": "3.0.0", @@ -7262,7 +7322,8 @@ "version": "1.4.2", "resolved": "https://registry.npmjs.org/has-symbol-support-x/-/has-symbol-support-x-1.4.2.tgz", "integrity": "sha1-FAn5i8ACR9pF2mfO4KNvKC/yZFU=", - "dev": true + "dev": true, + "optional": true }, "has-symbols": { "version": "1.0.0", @@ -7275,6 +7336,7 @@ "resolved": "https://registry.npmjs.org/has-to-string-tag-x/-/has-to-string-tag-x-1.4.1.tgz", "integrity": "sha1-oEWrOD17SyASoAFIqwql8pAETU0=", "dev": true, + "optional": true, "requires": { "has-symbol-support-x": "^1.4.1" } @@ -7480,7 +7542,8 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", - "dev": true + "dev": true, + "optional": true }, "ignore": { "version": "4.0.6", @@ -7620,7 +7683,8 @@ "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true + "dev": true, + "optional": true }, "svgo": { "version": "1.3.2", @@ -7692,6 +7756,7 @@ "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-2.1.0.tgz", "integrity": "sha1-ji1INIdCEhtKghi3oTfppSBJ3IA=", "dev": true, + "optional": true, "requires": { "repeating": "^2.0.0" } @@ -8018,7 +8083,8 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/is-finite/-/is-finite-1.1.0.tgz", "integrity": "sha512-cdyMtqX/BOqqNBBiKlIVkytNHm49MtMlYyn1zxzvJKWmFMlGzm+ry5BBfYyeY9YmNKbRSo/o7OX9w9ale0wg3w==", - "dev": true + "dev": true, + "optional": true }, "is-fullwidth-code-point": { "version": "1.0.0", @@ -8068,7 +8134,8 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/is-natural-number/-/is-natural-number-4.0.1.tgz", "integrity": "sha1-q5124dtM7VHjXeDHLr7PCfc0zeg=", - "dev": true + "dev": true, + "optional": true }, "is-negated-glob": { "version": "1.0.0", @@ -8106,13 +8173,15 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-object/-/is-object-1.0.2.tgz", "integrity": "sha512-2rRIahhZr2UWb45fIOuvZGpFtz0TyOZLf32KxBbSoUCeZR495zCKlWUKKUByk3geS2eAs7ZAABt0Y/Rx0GiQGA==", - "dev": true + "dev": true, + "optional": true }, "is-plain-obj": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz", "integrity": "sha1-caUMhCnfync8kqOQpKA7OfzVHT4=", - "dev": true + "dev": true, + "optional": true }, "is-plain-object": { "version": "2.0.4", @@ -8182,13 +8251,15 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/is-retry-allowed/-/is-retry-allowed-1.2.0.tgz", "integrity": "sha1-13hIi9CkZmo76KFIK58rqv7eqLQ=", - "dev": true + "dev": true, + "optional": true }, "is-stream": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=", - "dev": true + "dev": true, + "optional": true }, "is-svg": { "version": "3.0.0", @@ -8283,6 +8354,7 @@ "resolved": "https://registry.npmjs.org/isurl/-/isurl-1.0.0.tgz", "integrity": "sha1-sn9PSfPNqj6kSgpbfzRi5u3DnWc=", "dev": true, + "optional": true, "requires": { "has-to-string-tag-x": "^1.2.0", "is-object": "^1.0.1" @@ -9178,7 +9250,8 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-1.0.1.tgz", "integrity": "sha1-b54wtHCE2XGnyCD/FabFFnt0wm8=", - "dev": true + "dev": true, + "optional": true }, "lpad-align": { "version": "1.1.2", @@ -9248,7 +9321,8 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz", "integrity": "sha1-2TPOuSBdgr3PSIb2dCvcK03qFG0=", - "dev": true + "dev": true, + "optional": true }, "map-visit": { "version": "1.0.0", @@ -9416,7 +9490,8 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", "integrity": "sha1-SSNTiHju9CBjy4o+OweYeBSHqxs=", - "dev": true + "dev": true, + "optional": true }, "minimatch": { "version": "3.0.4", @@ -9651,9 +9726,9 @@ "dev": true }, "nouislider": { - "version": "14.6.2", - "resolved": "https://registry.npmjs.org/nouislider/-/nouislider-14.6.2.tgz", - "integrity": "sha512-/lJeqJBghNAZS3P2VYrHzm1RM6YJPvvC/1wNpGaHBRX+05wpzUDafrW/ohAYp4kjKhRH8+BJ0vkorCHiMmgTMQ==" + "version": "14.6.3", + "resolved": "https://registry.npmjs.org/nouislider/-/nouislider-14.6.3.tgz", + "integrity": "sha512-/3tAqsWY2JYW9vd7bC14bFRA1P9A+pRHOtKmoMsyfnB0fQcd1UFx2pdY1Ey5wAUzTnXTesmYaEo/ecLVETijIQ==" }, "now-and-later": { "version": "2.0.1", @@ -12763,6 +12838,7 @@ "resolved": "https://registry.npmjs.org/npm-conf/-/npm-conf-1.1.3.tgz", "integrity": "sha1-JWzEe9DiGMJZxOlVC/QTvCGSr/k=", "dev": true, + "optional": true, "requires": { "config-chain": "^1.1.11", "pify": "^3.0.0" @@ -12772,7 +12848,8 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", - "dev": true + "dev": true, + "optional": true } } }, @@ -12781,6 +12858,7 @@ "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", "integrity": "sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=", "dev": true, + "optional": true, "requires": { "path-key": "^2.0.0" } @@ -13149,7 +13227,8 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=", - "dev": true + "dev": true, + "optional": true }, "p-is-promise": { "version": "1.1.0", @@ -13186,6 +13265,7 @@ "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-1.2.1.tgz", "integrity": "sha1-XrOzU7f86Z8QGhA4iAuwVOu+o4Y=", "dev": true, + "optional": true, "requires": { "p-finally": "^1.0.0" } @@ -13376,7 +13456,8 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", "integrity": "sha1-elfrVQpng/kRUzH89GY9XI4AelA=", - "dev": true + "dev": true, + "optional": true }, "performance-now": { "version": "2.1.0", @@ -13883,7 +13964,8 @@ "version": "1.2.4", "resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz", "integrity": "sha1-IS1b/hMYMGpCD2QCuOJv85ZHqEk=", - "dev": true + "dev": true, + "optional": true }, "prr": { "version": "1.0.1", @@ -14241,6 +14323,7 @@ "resolved": "https://registry.npmjs.org/repeating/-/repeating-2.0.1.tgz", "integrity": "sha1-UhTFOpJtNVJwdSf7q0FdvAjQbdo=", "dev": true, + "optional": true, "requires": { "is-finite": "^1.0.0" } @@ -14595,6 +14678,7 @@ "resolved": "https://registry.npmjs.org/seek-bzip/-/seek-bzip-1.0.6.tgz", "integrity": "sha512-e1QtP3YL5tWww8uKaOCQ18UxIT2laNBXHjV/S2WYCiK4udiv8lkG89KRIoCjUagnAmCBurjF4zEVX2ByBbnCjQ==", "dev": true, + "optional": true, "requires": { "commander": "^2.8.1" } @@ -14989,6 +15073,7 @@ "resolved": "https://registry.npmjs.org/sort-keys/-/sort-keys-1.1.2.tgz", "integrity": "sha1-RBttTTRnmPG05J6JIK37oOVD+a0=", "dev": true, + "optional": true, "requires": { "is-plain-obj": "^1.0.0" } @@ -14998,6 +15083,7 @@ "resolved": "https://registry.npmjs.org/sort-keys-length/-/sort-keys-length-1.0.1.tgz", "integrity": "sha1-nLb09OnkgVWmqgZx7dM2/xR5oYg=", "dev": true, + "optional": true, "requires": { "sort-keys": "^1.0.0" } @@ -15327,6 +15413,7 @@ "resolved": "https://registry.npmjs.org/strip-dirs/-/strip-dirs-2.1.0.tgz", "integrity": "sha1-SYdzYmT8NEzyD2w0rKnRPR1O1sU=", "dev": true, + "optional": true, "requires": { "is-natural-number": "^4.0.1" } @@ -15335,7 +15422,8 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=", - "dev": true + "dev": true, + "optional": true }, "strip-final-newline": { "version": "2.0.0", @@ -15365,6 +15453,7 @@ "resolved": "https://registry.npmjs.org/strip-outer/-/strip-outer-1.0.1.tgz", "integrity": "sha1-sv0qv2YEudHmATBXGV34Nrip1jE=", "dev": true, + "optional": true, "requires": { "escape-string-regexp": "^1.0.2" } @@ -15490,6 +15579,7 @@ "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-1.6.2.tgz", "integrity": "sha1-jqVdqzeXIlPZqa+Q/c1VmuQ1xVU=", "dev": true, + "optional": true, "requires": { "bl": "^1.0.0", "buffer-alloc": "^1.2.0", @@ -15504,13 +15594,15 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", - "dev": true + "dev": true, + "optional": true }, "readable-stream": { "version": "2.3.7", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", "dev": true, + "optional": true, "requires": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", @@ -15526,6 +15618,7 @@ "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", "dev": true, + "optional": true, "requires": { "safe-buffer": "~5.1.0" } @@ -15536,13 +15629,15 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/temp-dir/-/temp-dir-1.0.0.tgz", "integrity": "sha1-CnwOom06Oa+n4OvqnB/AvE2qAR0=", - "dev": true + "dev": true, + "optional": true }, "tempfile": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/tempfile/-/tempfile-2.0.0.tgz", "integrity": "sha1-awRGhWqbERTRhW/8vlCczLCXcmU=", "dev": true, + "optional": true, "requires": { "temp-dir": "^1.0.0", "uuid": "^3.0.1" @@ -15637,7 +15732,8 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/timed-out/-/timed-out-4.0.1.tgz", "integrity": "sha1-8y6srFoXW+ol1/q1Zas+2HQe9W8=", - "dev": true + "dev": true, + "optional": true }, "timers-ext": { "version": "0.1.7", @@ -15694,7 +15790,8 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.1.1.tgz", "integrity": "sha1-STvUj2LXxD/N7TE6A9ytsuEhOoA=", - "dev": true + "dev": true, + "optional": true }, "to-fast-properties": { "version": "2.0.0", @@ -15796,6 +15893,7 @@ "resolved": "https://registry.npmjs.org/trim-repeated/-/trim-repeated-1.0.0.tgz", "integrity": "sha1-42RqLqTokTEr9+rObPsFOAvAHCE=", "dev": true, + "optional": true, "requires": { "escape-string-regexp": "^1.0.2" } @@ -15931,6 +16029,7 @@ "resolved": "https://registry.npmjs.org/unbzip2-stream/-/unbzip2-stream-1.4.3.tgz", "integrity": "sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg==", "dev": true, + "optional": true, "requires": { "buffer": "^5.2.1", "through": "^2.3.8" @@ -16139,7 +16238,8 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/url-to-options/-/url-to-options-1.0.1.tgz", "integrity": "sha1-FQWgOiiaSMvXpDTvuu7FBV9WM6k=", - "dev": true + "dev": true, + "optional": true }, "use": { "version": "3.1.1", @@ -16633,6 +16733,7 @@ "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", "integrity": "sha1-x+sXyT4RLLEIb6bY5R+wZnt5pfk=", "dev": true, + "optional": true, "requires": { "buffer-crc32": "~0.2.3", "fd-slicer": "~1.1.0" diff --git a/src/Umbraco.Web.UI.Client/package.json b/src/Umbraco.Web.UI.Client/package.json index b7a4779e07..b229aa8ca7 100644 --- a/src/Umbraco.Web.UI.Client/package.json +++ b/src/Umbraco.Web.UI.Client/package.json @@ -42,7 +42,7 @@ "lazyload-js": "1.0.0", "moment": "2.22.2", "ng-file-upload": "12.2.13", - "nouislider": "14.6.2", + "nouislider": "14.6.3", "npm": "^6.14.7", "spectrum-colorpicker2": "2.0.3", "tinymce": "4.9.11", diff --git a/src/Umbraco.Web.UI.Client/src/app.js b/src/Umbraco.Web.UI.Client/src/app.js index 74a7008901..645296f0e0 100644 --- a/src/Umbraco.Web.UI.Client/src/app.js +++ b/src/Umbraco.Web.UI.Client/src/app.js @@ -91,6 +91,6 @@ angular.module("umbraco.viewcache", []) // be able to configure angular values in the Default.cshtml // view which is much easier to do that configuring values by injecting them in the back office controller // to follow through to the js initialization stuff -if (angular.isFunction(document.angularReady)) { +if (_.isFunction(document.angularReady)) { document.angularReady.apply(this, [app]); } diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/content/edit.controller.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/content/edit.controller.js index 5a9f30fe24..da93450522 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/content/edit.controller.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/content/edit.controller.js @@ -223,7 +223,6 @@ //we are editing so get the content item from the server return $scope.getMethod()($scope.contentId) .then(function (data) { - $scope.content = data; appendRuntimeData(); @@ -271,7 +270,7 @@ * @param {any} app the active content app */ function createButtons(content) { - + var isBlueprint = content.isBlueprint; if ($scope.page.isNew && $location.path().search(/contentBlueprints/i) !== -1) { @@ -478,7 +477,7 @@ syncTreeNode($scope.content, $scope.content.path); - if (err.status === 400 && err.data) { + if (err && err.status === 400 && err.data) { // content was saved but is invalid. eventsService.emit("content.saved", { content: $scope.content, action: args.action, valid: false }); } diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/content/umbcontentnodeinfo.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/content/umbcontentnodeinfo.directive.js index 60f877d0b6..c20c2a368d 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/content/umbcontentnodeinfo.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/content/umbcontentnodeinfo.directive.js @@ -1,4 +1,4 @@ -(function () { +(function () { 'use strict'; function ContentNodeInfoDirective($timeout, logResource, eventsService, userService, localizationService, dateHelper, editorService, redirectUrlsResource, overlayService, entityResource) { @@ -54,15 +54,15 @@ localizationService.localizeMany(keys) .then(function (data) { - labels.deleted = data[0]; - labels.unpublished = data[1]; //aka draft - labels.published = data[2]; - labels.publishedPendingChanges = data[3]; - labels.notCreated = data[4]; - labels.unsavedChanges = data[5]; - labels.doctypeChangeWarning = data[6]; - labels.notPublished = data[7]; - scope.chooseLabel = data[8]; + [labels.deleted, + labels.unpublished, + labels.published, + labels.publishedPendingChanges, + labels.notCreated, + labels.unsavedChanges, + labels.doctypeChangeWarning, + labels.notPublished, + scope.chooseLabel] = data; setNodePublishStatus(); @@ -159,7 +159,7 @@ } scope.openTemplate = function () { - var template = _.findWhere(scope.allTemplates, {alias: scope.node.template}) + var template = _.findWhere(scope.allTemplates, { alias: scope.node.template }) if (!template) { return; } @@ -200,7 +200,7 @@ //don't load this if it's already done if (auditTrailLoaded && !forceReload) { - return; + return; } scope.loadingAuditTrail = true; @@ -251,7 +251,7 @@ function setAuditTrailLogTypeColor(auditTrail) { angular.forEach(auditTrail, function (item) { - + switch (item.logType) { case "Save": item.logTypeColor = "primary"; @@ -263,7 +263,7 @@ case "Unpublish": case "UnpublishVariant": item.logTypeColor = "warning"; - break; + break; case "Delete": item.logTypeColor = "danger"; break; @@ -313,19 +313,14 @@ function updateCurrentUrls() { // never show URLs for element types (if they happen to have been created in the content tree) - if (scope.node.isElement) { + if (scope.node.isElement || scope.node.urls === null) { scope.currentUrls = null; return; } - // find the URLs for the currently selected language - if (scope.node.variants.length > 1) { - // nodes with variants - scope.currentUrls = _.filter(scope.node.urls, (url) => (scope.currentVariant.language && scope.currentVariant.language.culture === url.culture)); - } else { - // invariant nodes - scope.currentUrls = scope.node.urls; - } + // find the urls for the currently selected language + // when there is no selected language (allow vary by culture == false), show all urls of the node. + scope.currentUrls = _.filter(scope.node.urls, (url) => (scope.currentVariant.language == null || scope.currentVariant.language.culture === url.culture)); // figure out if multiple cultures apply across the content URLs scope.currentUrlsHaveMultipleCultures = _.keys(_.groupBy(scope.currentUrls, url => url.culture)).length > 1; diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/content/umbvariantcontenteditors.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/content/umbvariantcontenteditors.directive.js index c3fd0dc9c4..3e227bfcb3 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/content/umbvariantcontenteditors.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/content/umbvariantcontenteditors.directive.js @@ -187,8 +187,7 @@ } } - eventsService.on("editors.content.splitViewRequest", (_, args) => requestSplitView(args)); - + var unbindSplitViewRequest = eventsService.on("editors.content.splitViewRequest", (_, args) => requestSplitView(args)); /** Closes the split view */ function closeSplitView(editorIndex) { // TODO: hacking animation states - these should hopefully be easier to do when we upgrade angular @@ -201,6 +200,7 @@ $location.search({"cculture": culture, "csegment": vm.editors[0].content.segment}); splitViewChanged(); + unbindSplitViewRequest(); } /** diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/umbeditorcontentheader.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/umbeditorcontentheader.directive.js index 846d5c85fe..31e51fe115 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/umbeditorcontentheader.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/umbeditorcontentheader.directive.js @@ -1,7 +1,7 @@ (function () { 'use strict'; - function EditorContentHeader(serverValidationManager, localizationService, editorState) { + function EditorContentHeader(serverValidationManager, localizationService, editorState, contentEditingHelper) { function link(scope) { var unsubscribe = []; @@ -92,7 +92,6 @@ } function onInit() { - // find default + check if we have variants. scope.content.variants.forEach(function (variant) { if (variant.language !== null && variant.language.isDefault) { @@ -115,11 +114,13 @@ if (scope.vm.hasCulture) { scope.content.variants.forEach((v) => { if (v.language !== null && v.segment === null) { + const subVariants = scope.content.variants.filter((subVariant) => subVariant.language.culture === v.language.culture && subVariant.segment !== null).sort(contentEditingHelper.sortVariants); + var variantMenuEntry = { key: String.CreateGuid(), open: v.language && v.language.culture === scope.editor.culture, variant: v, - subVariants: scope.content.variants.filter((subVariant) => subVariant.language.culture === v.language.culture && subVariant.segment !== null) + subVariants }; scope.vm.variantMenu.push(variantMenuEntry); } @@ -147,7 +148,12 @@ } unsubscribe.push(serverValidationManager.subscribe(null, variant.language !== null ? variant.language.culture : null, null, onVariantValidation, variant.segment)); }); + + scope.vm.variantMenu.sort(sortVariantsMenu); + } + function sortVariantsMenu (a, b) { + return contentEditingHelper.sortVariants(a.variant, b.variant); } scope.goBack = function () { @@ -200,6 +206,14 @@ return false; } + scope.toggleDropdown = function () { + scope.vm.dropdownOpen = !scope.vm.dropdownOpen; + + if (scope.vm.dropdownOpen) { + scope.vm.variantMenu.sort(sortVariantsMenu); + } + }; + onInit(); scope.$on('$destroy', function () { diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/umbeditorheader.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/umbeditorheader.directive.js index 2dfb0d5158..6f272f1ea2 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/umbeditorheader.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/umbeditorheader.directive.js @@ -357,9 +357,12 @@ Use this directive to construct a header inside the main editor window. scope.$emit("$changeTitle", title); } - $rootScope.$on('$setAccessibleHeader', function (event, isNew, editorFor, nameLocked, name, contentTypeName, setTitle) { + var unbindEventHandler = $rootScope.$on('$setAccessibleHeader', function (event, isNew, editorFor, nameLocked, name, contentTypeName, setTitle) { setAccessibilityHeaderDirective(isNew, editorFor, nameLocked, name, contentTypeName, setTitle); }); + scope.$on('$destroy', function () { + unbindEventHandler(); + }); } diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/checklistmodel.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/checklistmodel.directive.js index d944989bab..491dff3a41 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/checklistmodel.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/checklistmodel.directive.js @@ -11,7 +11,7 @@ angular.module('umbraco.directives') function contains(arr, item) { if (Utilities.isArray(arr)) { for (var i = 0; i < arr.length; i++) { - if (angular.equals(arr[i], item)) { + if (Utilities.equals(arr[i], item)) { return true; } } @@ -19,23 +19,23 @@ angular.module('umbraco.directives') return false; } - // add + // add function add(arr, item) { arr = Utilities.isArray(arr) ? arr : []; for (var i = 0; i < arr.length; i++) { - if (angular.equals(arr[i], item)) { + if (Utilities.equals(arr[i], item)) { return arr; } - } + } arr.push(item); return arr; - } + } // remove function remove(arr, item) { if (Utilities.isArray(arr)) { for (var i = 0; i < arr.length; i++) { - if (angular.equals(arr[i], item)) { + if (Utilities.equals(arr[i], item)) { arr.splice(i, 1); break; } diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/imaging/umbimagegravity.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/imaging/umbimagegravity.directive.js index dfa58f34f8..fd9a236f87 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/imaging/umbimagegravity.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/imaging/umbimagegravity.directive.js @@ -160,7 +160,7 @@ function onChanges(changes) { if (changes.center && !changes.center.isFirstChange() && changes.center.currentValue - && !angular.equals(changes.center.currentValue, changes.center.previousValue)) { + && !Utilities.equals(changes.center.currentValue, changes.center.previousValue)) { //when center changes update the dimensions setDimensions(); } 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 a412f73c5a..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); @@ -409,8 +409,8 @@ function umbTreeDirective($q, $rootScope, treeService, notificationsService, use //load the tree loadTree().then(function () { //because angular doesn't return a promise for the resolve method, we need to resort to some hackery, else - //like normal JS promises we could do resolve(...).then() - if (args && args.onLoaded && angular.isFunction(args.onLoaded)) { + //like normal JS promises we could do resolve(...).then() + if (args && args.onLoaded && Utilities.isFunction(args.onLoaded)) { args.onLoaded(); } }); diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbaceeditor.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbaceeditor.directive.js index 070ffd4ddd..5e1f2489e6 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbaceeditor.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbaceeditor.directive.js @@ -82,7 +82,7 @@ if (Utilities.isDefined(opts.firstLineNumber)) { if (Utilities.isNumber(opts.firstLineNumber)) { session.setOption('firstLineNumber', opts.firstLineNumber); - } else if (angular.isFunction(opts.firstLineNumber)) { + } else if (Utilities.isFunction(opts.firstLineNumber)) { session.setOption('firstLineNumber', opts.firstLineNumber()); } } @@ -116,7 +116,7 @@ // onLoad callbacks angular.forEach(opts.callbacks, function(cb) { - if (angular.isFunction(cb)) { + if (Utilities.isFunction(cb)) { cb(acee); } }); @@ -208,7 +208,7 @@ if (Utilities.isDefined(callback)) { scope.$evalAsync(function() { - if (angular.isFunction(callback)) { + if (Utilities.isFunction(callback)) { callback(args); } else { throw new Error('ui-ace use a function as callback.'); diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbavatar.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbavatar.directive.js index 7dd2f0d7a3..321cd8a59d 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbavatar.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbavatar.directive.js @@ -50,18 +50,20 @@ Use this directive to render an avatar. (function() { 'use strict'; - function AvatarDirective() { + function AvatarDirective(localizationService) { function link(scope, element, attrs, ctrl) { var eventBindings = []; scope.initials = ""; + scope.avatarAlt = ""; function onInit() { if (!scope.unknownChar) { scope.unknownChar = "?"; } scope.initials = getNameInitials(scope.name); + setAvatarAlt(scope.name); } function getNameInitials(name) { @@ -77,10 +79,23 @@ Use this directive to render an avatar. return null; } + function setAvatarAlt(name) { + if (name) { + localizationService + .localize('general_avatar') + .then(function(data) { + scope.avatarAlt = data + ' ' + name; + } + ); + } + scope.avatarAlt = null; + } + eventBindings.push(scope.$watch('name', function (newValue, oldValue) { if (newValue === oldValue) { return; } if (oldValue === undefined || newValue === undefined) { return; } scope.initials = getNameInitials(newValue); + setAvatarAlt(newValue); })); onInit(); diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbminilistview.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbminilistview.directive.js index 3865ffcdae..783cd7f90a 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbminilistview.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbminilistview.directive.js @@ -92,7 +92,7 @@ // advanced item filtering is handled here if (scope.entityTypeFilter && scope.entityTypeFilter.filter && scope.entityTypeFilter.filterAdvanced) { - var filtered = angular.isFunction(scope.entityTypeFilter.filter) + var filtered = Utilities.isFunction(scope.entityTypeFilter.filter) ? _.filter(miniListView.children, scope.entityTypeFilter.filter) : _.where(miniListView.children, scope.entityTypeFilter.filter); 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/validation/valpropertyvalidator.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/validation/valpropertyvalidator.directive.js index 37303d22ad..d66e4bd2af 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/validation/valpropertyvalidator.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/validation/valpropertyvalidator.directive.js @@ -28,9 +28,9 @@ function valPropertyValidator(serverValidationManager) { var modelCtrl = ctrls[0]; var propCtrl = ctrls.length > 1 ? ctrls[1] : null; - - // Check whether the scope has a valPropertyValidator method - if (!scope.valPropertyValidator || !angular.isFunction(scope.valPropertyValidator)) { + + // Check whether the scope has a valPropertyValidator method + if (!scope.valPropertyValidator || !Utilities.isFunction(scope.valPropertyValidator)) { throw new Error('val-property-validator directive must specify a function to call'); } diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/validation/valserver.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/validation/valserver.directive.js index ea6087d4e9..4180457792 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/validation/valserver.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/validation/valserver.directive.js @@ -72,7 +72,7 @@ function valServer(serverValidationManager) { return modelCtrl.$modelValue; }, function (newValue, oldValue) { - if (!newValue || angular.equals(newValue, oldValue)) { + if (!newValue || Utilities.equals(newValue, oldValue)) { return; } diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/content.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/content.resource.js index 70861c9a86..368eab2339 100644 --- a/src/Umbraco.Web.UI.Client/src/common/resources/content.resource.js +++ b/src/Umbraco.Web.UI.Client/src/common/resources/content.resource.js @@ -42,7 +42,55 @@ function contentResource($q, $http, umbDataFormatter, umbRequestHelper) { return { - savePermissions: function (saveModel) { + /** + * @ngdoc method + * @name umbraco.resources.contentResource#allowsCultureVariation + * @methodOf umbraco.resources.contentResource + * + * @description + * Check whether any content types have culture variant enabled + * + * ##usage + *
+        * contentResource.allowsCultureVariation()
+        *    .then(function() {
+        *       Do stuff...
+        *    });
+        * 
+ * + * @returns {Promise} resourcePromise object. + * + */ + allowsCultureVariation: function () { + return umbRequestHelper.resourcePromise( + $http.get( + umbRequestHelper.getApiUrl( + "contentApiBaseUrl", + "AllowsCultureVariation")), + 'Failed to retrieve variant content types'); + }, + + /** + * @ngdoc method + * @name umbraco.resources.contentResource#savePermissions + * @methodOf umbraco.resources.contentResource + * + * @description + * Save user group permissions for the content + * + * ##usage + *
+        * contentResource.savePermissions(saveModel)
+        *    .then(function() {
+        *       Do stuff...
+        *    });
+        * 
+ * + * @param {object} The object which contains the user group permissions for the content + * @returns {Promise} resourcePromise object. + * + */ + savePermissions: function (saveModel) { if (!saveModel) { throw "saveModel cannot be null"; } @@ -59,7 +107,25 @@ function contentResource($q, $http, umbDataFormatter, umbRequestHelper) { 'Failed to save permissions'); }, - + /** + * @ngdoc method + * @name umbraco.resources.contentResource#getRecycleBin + * @methodOf umbraco.resources.contentResource + * + * @description + * Get the recycle bin + * + * ##usage + *
+        * contentResource.getRecycleBin()
+        *    .then(function() {
+        *       Do stuff...
+        *    });
+        * 
+ * + * @returns {Promise} resourcePromise object. + * + */ getRecycleBin: function () { return umbRequestHelper.resourcePromise( $http.get( @@ -328,6 +394,26 @@ function contentResource($q, $http, umbDataFormatter, umbRequestHelper) { 'Failed to delete item ' + id); }, + /** + * @ngdoc method + * @name umbraco.resources.contentResource#deleteBlueprint + * @methodOf umbraco.resources.contentResource + * + * @description + * Deletes a content blueprint item with a given id + * + * ##usage + *
+        * contentResource.deleteBlueprint(1234)
+        *    .then(function() {
+        *        alert('its gone!');
+        *    });
+        * 
+ * + * @param {Int} id id of content blueprint item to delete + * @returns {Promise} resourcePromise object. + * + */ deleteBlueprint: function (id) { return umbRequestHelper.resourcePromise( $http.post( @@ -373,6 +459,26 @@ function contentResource($q, $http, umbDataFormatter, umbRequestHelper) { }); }, + /** + * @ngdoc method + * @name umbraco.resources.contentResource#getBlueprintById + * @methodOf umbraco.resources.contentResource + * + * @description + * Gets a content blueprint item with a given id + * + * ##usage + *
+        * contentResource.getBlueprintById(1234)
+        *    .then(function() {
+        *       Do stuff...
+        *    });
+        * 
+ * + * @param {Int} id id of content blueprint item to retrieve + * @returns {Promise} resourcePromise object. + * + */ getBlueprintById: function (id) { return umbRequestHelper.resourcePromise( $http.get( @@ -386,6 +492,26 @@ function contentResource($q, $http, umbDataFormatter, umbRequestHelper) { }); }, + /** + * @ngdoc method + * @name umbraco.resources.contentResource#getNotifySettingsById + * @methodOf umbraco.resources.contentResource + * + * @description + * Gets notification options for a content item with a given id for the current user + * + * ##usage + *
+        * contentResource.getNotifySettingsById(1234)
+        *    .then(function() {
+        *       Do stuff...
+        *    });
+        * 
+ * + * @param {Int} id id of content item + * @returns {Promise} resourcePromise object. + * + */ getNotifySettingsById: function (id) { return umbRequestHelper.resourcePromise( $http.get( @@ -396,6 +522,27 @@ function contentResource($q, $http, umbDataFormatter, umbRequestHelper) { 'Failed to retrieve data for content id ' + id); }, + /** + * @ngdoc method + * @name umbraco.resources.contentResource#getNotifySettingsById + * @methodOf umbraco.resources.contentResource + * + * @description + * Sets notification settings for a content item with a given id for the current user + * + * ##usage + *
+        * contentResource.setNotifySettingsById(1234,["D", "F", "H"])
+        *    .then(function() {
+        *       Do stuff...
+        *    });
+        * 
+ * + * @param {Int} id id of content item + * @param {Array} options the notification options to set for the content item + * @returns {Promise} resourcePromise object. + * + */ setNotifySettingsById: function (id, options) { if (!id) { throw "contentId cannot be null"; @@ -547,7 +694,7 @@ function contentResource($q, $http, umbDataFormatter, umbRequestHelper) { $http.get( umbRequestHelper.getApiUrl( "contentApiBaseUrl", - "GetEmptyBlueprint", + "GetEmpty", { blueprintId: blueprintId, parentId: parentId })), 'Failed to retrieve blueprint for id ' + blueprintId) .then(function (result) { @@ -639,7 +786,7 @@ function contentResource($q, $http, umbDataFormatter, umbRequestHelper) { else if (options.orderDirection === "desc") { options.orderDirection = "Descending"; } - + //converts the value to a js bool function toBool(v) { if (Utilities.isNumber(v)) { @@ -688,7 +835,7 @@ function contentResource($q, $http, umbDataFormatter, umbRequestHelper) { * @methodOf umbraco.resources.contentResource * * @description - * Saves changes made to a content item to its current version, if the content item is new, the isNew paramater must be passed to force creation + * Saves changes made to a content item to its current version, if the content item is new, the isNew parameter must be passed to force creation * if the content item needs to have files attached, they must be provided as the files param and passed separately * * @@ -718,6 +865,34 @@ function contentResource($q, $http, umbDataFormatter, umbRequestHelper) { return saveContentItem(content, "save" + (isNew ? "New" : ""), files, endpoint, showNotifications); }, + /** + * @ngdoc method + * @name umbraco.resources.contentResource#saveBlueprint + * @methodOf umbraco.resources.contentResource + * + * @description + * Saves changes made to a content blueprint item to its current version, if the content blueprint item is new, the isNew parameter must be passed to force creation + * if the content item needs to have files attached, they must be provided as the files param and passed separately + * + * ##usage + *
+        * contentResource.getById(1234)
+        *    .then(function(content) {
+        *          content.name = "I want a new name!";
+        *          contentResource.saveBlueprint(content, false)
+        *            .then(function(content){
+        *                alert("Retrieved, updated and saved again");
+        *            });
+        *    });
+        * 
+ * + * @param {Object} content The content blueprint item object with changes applied + * @param {Bool} isNew set to true to create a new item or to update an existing + * @param {Array} files collection of files for the document + * @param {Bool} showNotifications an option to disable/show notifications (default is true) + * @returns {Promise} resourcePromise object containing the saved content item. + * + */ saveBlueprint: function (content, isNew, files, showNotifications) { var endpoint = umbRequestHelper.getApiUrl( "contentApiBaseUrl", @@ -731,7 +906,7 @@ function contentResource($q, $http, umbDataFormatter, umbRequestHelper) { * @methodOf umbraco.resources.contentResource * * @description - * Saves and publishes changes made to a content item to a new version, if the content item is new, the isNew paramater must be passed to force creation + * Saves and publishes changes made to a content item to a new version, if the content item is new, the isNew parameter must be passed to force creation * if the content item needs to have files attached, they must be provided as the files param and passed separately * * @@ -761,6 +936,35 @@ function contentResource($q, $http, umbDataFormatter, umbRequestHelper) { return saveContentItem(content, "publish" + (isNew ? "New" : ""), files, endpoint, showNotifications); }, + /** + * @ngdoc method + * @name umbraco.resources.contentResource#publish + * @methodOf umbraco.resources.contentResource + * + * @description + * Saves and publishes changes made to a content item and its descendants to a new version, if the content item is new, the isNew parameter must be passed to force creation + * if the content items needs to have files attached, they must be provided as the files param and passed separately + * + * + * ##usage + *
+        * contentResource.getById(1234)
+        *    .then(function(content) {
+        *          content.name = "I want a new name, and be published!";
+        *          contentResource.publishWithDescendants(content, false)
+        *            .then(function(content){
+        *                alert("Retrieved, updated and published again");
+        *            });
+        *    });
+        * 
+ * + * @param {Object} content The content item object with changes applied + * @param {Bool} isNew set to true to create a new item or to update an existing + * @param {Array} files collection of files for the document + * @param {Bool} showNotifications an option to disable/show notifications (default is true) + * @returns {Promise} resourcePromise object containing the saved content item. + * + */ publishWithDescendants: function (content, isNew, force, files, showNotifications) { var endpoint = umbRequestHelper.getApiUrl( "contentApiBaseUrl", @@ -864,6 +1068,27 @@ function contentResource($q, $http, umbDataFormatter, umbRequestHelper) { }, + /** + * @ngdoc method + * @name umbraco.resources.contentResource#createBlueprintFromContent + * @methodOf umbraco.resources.contentResource + * + * @description + * Creates a content blueprint with a given name from a given content id + * + * ##usage + *
+        * contentResource.createBlueprintFromContent(1234,"name")
+        *    .then(function(content) {
+        *        alert("created");
+        *    });
+            * 
+ * + * @param {Int} id The ID of the content to create the content blueprint from + * @param {string} id The name of the content blueprint + * @returns {Promise} resourcePromise object + * + */ createBlueprintFromContent: function (contentId, name) { return umbRequestHelper.resourcePromise( $http.post( 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 c5a353d746..a693b7d20e 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 @@ -511,41 +511,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/services/angularhelper.service.js b/src/Umbraco.Web.UI.Client/src/common/services/angularhelper.service.js index 36ce4541f1..c4fd431a12 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/angularhelper.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/angularhelper.service.js @@ -154,12 +154,12 @@ function angularHelper($q) { */ safeApply: function (scope, fn) { if (scope.$$phase || (scope.$root && scope.$root.$$phase)) { - if (angular.isFunction(fn)) { + if (Utilities.isFunction(fn)) { fn(); } } else { - if (angular.isFunction(fn)) { + if (Utilities.isFunction(fn)) { scope.$apply(fn); } else { 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 34abba924e..1e78ca16ed 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 @@ -595,7 +595,7 @@ function contentEditingHelper(fileManager, $q, $location, $routeParams, editorSt //instead of having a property editor $watch their expression to check if it has // been updated, instead we'll check for the existence of a special method on their model // and just call it. - if (angular.isFunction(origProp.onValueChanged)) { + if (Utilities.isFunction(origProp.onValueChanged)) { //send the newVal + oldVal origProp.onValueChanged(origProp.value, origVal); } @@ -649,7 +649,7 @@ function contentEditingHelper(fileManager, $q, $location, $routeParams, editorSt // soft-redirecting which means the URL will change but the route wont (i.e. creating content). // In this case we need to detect what properties have changed and re-bind them with the server data. - if (args.rebindCallback && angular.isFunction(args.rebindCallback)) { + if (args.rebindCallback && Utilities.isFunction(args.rebindCallback)) { args.rebindCallback(); } @@ -696,7 +696,7 @@ function contentEditingHelper(fileManager, $q, $location, $routeParams, editorSt // soft-redirecting which means the URL will change but the route wont (i.e. creating content). // In this case we need to detect what properties have changed and re-bind them with the server data. - if (args.rebindCallback && angular.isFunction(args.rebindCallback)) { + if (args.rebindCallback && Utilities.isFunction(args.rebindCallback)) { args.rebindCallback(); } } @@ -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..326123f797 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 @@ -593,6 +592,23 @@ 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 Submits the editor. + * @param {function} editor.close Closes the editor. + * @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 @@ -1055,7 +1071,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/events.service.js b/src/Umbraco.Web.UI.Client/src/common/services/events.service.js index 965ac3d635..c7ef5bd28f 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/events.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/events.service.js @@ -34,7 +34,7 @@ function eventsService($q, $rootScope) { /** pass in the result of 'on' to this method, or just call the method returned from 'on' to unsubscribe */ unsubscribe: function(handle) { - if (angular.isFunction(handle)) { + if (Utilities.isFunction(handle)) { handle(); } } 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/listviewhelper.service.js b/src/Umbraco.Web.UI.Client/src/common/services/listviewhelper.service.js index 79c3880e60..14643dc9cd 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 @@ -45,8 +45,7 @@ (function () { 'use strict'; - function listViewHelper($location, $rootScope, localStorageService, urlHelper) { - + function listViewHelper($location, $rootScope, localStorageService, urlHelper, editorService) { var firstSelectedIndex = 0; var localStorageKey = "umblistViewLayout"; @@ -574,16 +573,51 @@ * * @param {Object} item The item to edit */ - function editItem(item) { + function editItem(item, scope) { if (!item.editPath) { return; } + + if (scope.options.useInfiniteEditor) + { + var editorModel = { + id: item.id, + submit: function(model) { + editorService.close(); + scope.getContent(scope.contentId); + }, + close: function() { + editorService.close(); + scope.getContent(scope.contentId); + } + }; + + if (item.editPath.indexOf("/content/") == 0) + { + editorService.contentEditor(editorModel); + return; + } + + if (item.editPath.indexOf("/media/") == 0) + { + editorService.mediaEditor(editorModel); + return; + } + + if (item.editPath.indexOf("/member/") == 0) + { + editorModel.id = item.key; + editorService.memberEditor(editorModel); + return; + } + } + var parts = item.editPath.split("?"); var path = parts[0]; var params = parts[1] - ? urlHelper.getQueryStringParams("?" + parts[1]) - : {}; - + ? urlHelper.getQueryStringParams("?" + parts[1]) + : {}; + $location.path(path); for (var p in params) { $location.search(p, params[p]); 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 c1d84aa16b..a51ed462fa 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/search.service.js b/src/Umbraco.Web.UI.Client/src/common/services/search.service.js index 8e9525af84..eda36a5fce 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/search.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/search.service.js @@ -67,10 +67,11 @@ angular.module('umbraco.services') } return entityResource.search(args.term, "Document", args.searchFrom, args.canceler, args.dataTypeKey) - _.each(data, function (item) { + .then(data => { data.forEach(item => searchResultFormatter.configureContentResult(item)); return data; }); + }, /** diff --git a/src/Umbraco.Web.UI.Client/src/common/services/tree.service.js b/src/Umbraco.Web.UI.Client/src/common/services/tree.service.js index 9ba4d2964b..9970995a28 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/tree.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/tree.service.js @@ -235,7 +235,7 @@ function treeService($q, treeResource, iconHelper, notificationsService, eventsS } }); } - else if (args.filter && angular.isFunction(args.filter)) { + else if (args.filter && Utilities.isFunction(args.filter)) { //if a filter is supplied a cacheKey must be supplied as well if (!args.cacheKey) { throw "args.cacheKey is required if args.filter is supplied"; @@ -315,7 +315,7 @@ function treeService($q, treeResource, iconHelper, notificationsService, eventsS args.node.hasChildren = true; //Since we've removed the children & reloaded them, we need to refresh the UI now because the tree node UI doesn't operate on normal angular $watch since that will be pretty slow - if (angular.isFunction(args.node.updateNodeData)) { + if (Utilities.isFunction(args.node.updateNodeData)) { args.node.updateNodeData(args.node); } } @@ -349,7 +349,7 @@ function treeService($q, treeResource, iconHelper, notificationsService, eventsS * @param {object} treeNode the node to remove */ removeNode: function (treeNode) { - if (!angular.isFunction(treeNode.parent)) { + if (!Utilities.isFunction(treeNode.parent)) { return; } @@ -509,7 +509,7 @@ function treeService($q, treeResource, iconHelper, notificationsService, eventsS if (current.metaData && current.metaData["treeAlias"]) { root = current; } - else if (angular.isFunction(current.parent)) { + else if (Utilities.isFunction(current.parent)) { //we can only continue if there is a parent() method which means this // tree node was loaded in as part of a real tree, not just as a single tree // node from the server. @@ -706,7 +706,7 @@ function treeService($q, treeResource, iconHelper, notificationsService, eventsS //to fire, instead we're just going to replace all the properties of this node. //there should always be a method assigned but we'll check anyways - if (angular.isFunction(node.parent().children[index].updateNodeData)) { + if (Utilities.isFunction(node.parent().children[index].updateNodeData)) { node.parent().children[index].updateNodeData(found); } else { @@ -741,7 +741,7 @@ function treeService($q, treeResource, iconHelper, notificationsService, eventsS if (!node) { throw "node cannot be null"; } - if (!angular.isFunction(node.parent)) { + if (!Utilities.isFunction(node.parent)) { throw "node.parent is not a function, the path cannot be resolved"; } 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 8658e6e67e..ab1535d85b 100644 --- a/src/Umbraco.Web.UI.Client/src/installer/installer.service.js +++ b/src/Umbraco.Web.UI.Client/src/installer/installer.service.js @@ -85,12 +85,12 @@ angular.module("umbraco.install").factory('installerService', function ($rootSco /** Have put this here because we are not referencing our other modules */ function safeApply (scope, fn) { if (scope.$$phase || scope.$root.$$phase) { - if (angular.isFunction(fn)) { + if (Utilities.isFunction(fn)) { fn(); } } else { - if (angular.isFunction(fn)) { + if (Utilities.isFunction(fn)) { scope.$apply(fn); } else { 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..9d2782f184 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 @@ -196,10 +196,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 ffbe2224d9..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,7 +104,7 @@ body.touch .umb-tree { .umb-button-ellipsis--hidden { opacity: 1; } - + .umb-tree-icon { color: @ui-option-type-hover; } @@ -131,7 +133,6 @@ body.touch .umb-tree { .umb-tree .umb-search-group { position: inherit; display: inherit; - list-style: none; h6 { @@ -154,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; } } } @@ -335,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..3b084c9905 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 @@ -475,9 +475,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-nested-content.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-nested-content.less index 834a1a69e9..bd787e2329 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-nested-content.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-nested-content.less @@ -292,4 +292,4 @@ .umb-textarea, .umb-textstring { width:100%; } -} +} \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-packages.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-packages.less index 197a5eb176..0045bed140 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-packages.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-packages.less @@ -278,7 +278,7 @@ flex: 1 1 auto; margin-right: 20px; width: calc(~'100%' - ~'@{sidebarwidth}' - ~'20px'); // Make sure that the main content area doesn't gets affected by inline styling - min-width: 500px; + min-width: 480px; } .umb-package-details__sidebar { 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 bda9fa7a7e..b96d3e8569 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 @@ -18,6 +18,8 @@ html .umb-search-filter { margin: 0; } + // "icon-search" class it kept for backward compatibility + .umb-icon, .icon-search { color: #d8d7d9; position: absolute; diff --git a/src/Umbraco.Web.UI.Client/src/less/main.less b/src/Umbraco.Web.UI.Client/src/less/main.less index 7c5ed4c9bb..31bb8484c4 100644 --- a/src/Umbraco.Web.UI.Client/src/less/main.less +++ b/src/Umbraco.Web.UI.Client/src/less/main.less @@ -606,6 +606,9 @@ table thead button:focus{ display: inline; } +.relative { + position:relative; +} // Input label styles // @Simon: not sure where to put this part yet @@ -666,3 +669,8 @@ input[type=checkbox]:checked + .input-label--small { background-color: @green-l3; text-decoration: none; } + +.language-icon { + color: #BBBABF; + margin-right: 5px; +} 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..6f9ce6ee34 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 @@ -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/treepicker/treepicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/treepicker/treepicker.controller.js index c519a1d4fa..0c5fe9af1b 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/treepicker/treepicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/treepicker/treepicker.controller.js @@ -173,7 +173,7 @@ angular.module("umbraco").controller("Umbraco.Editors.TreePickerController", $scope.model.filterAdvanced = false; //used advanced filtering - if (angular.isFunction($scope.model.filter)) { + if (Utilities.isFunction($scope.model.filter)) { $scope.model.filterAdvanced = true; } else if (Utilities.isObject($scope.model.filter)) { @@ -189,9 +189,9 @@ angular.module("umbraco").controller("Umbraco.Editors.TreePickerController", if ($scope.model.filter.startsWith("{")) { $scope.model.filterAdvanced = true; - if ($scope.model.filterByMetadata && !angular.isFunction($scope.model.filter)) + if ($scope.model.filterByMetadata && !Utilities.isFunction($scope.model.filter)) { - var filter = angular.fromJson($scope.model.filter); + var filter = Utilities.fromJson($scope.model.filter); $scope.model.filter = function (node){ return _.isMatch(node.metaData, filter);}; } else @@ -456,7 +456,7 @@ angular.module("umbraco").controller("Umbraco.Editors.TreePickerController", if ($scope.model.filterAdvanced) { //filter either based on a method or an object - var filtered = angular.isFunction($scope.model.filter) + var filtered = Utilities.isFunction($scope.model.filter) ? _.filter(nodes, $scope.model.filter) : _.where(nodes, $scope.model.filter); 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 5da2e64234..060217d33c 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 @@ -5,7 +5,7 @@ diff --git a/src/Umbraco.Web.UI.Client/src/views/errors/BootFailed.html b/src/Umbraco.Web.UI.Client/src/views/errors/BootFailed.html new file mode 100644 index 0000000000..c08627739a --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/errors/BootFailed.html @@ -0,0 +1,79 @@ + + + + + + Boot Failed + + + +
+ +
+

Boot Failed

+

Umbraco failed to boot, if you are the owner of the website please see the log file for more details.

+
+
+ + diff --git a/src/Umbraco.Web.UI.Client/src/views/languages/overview.html b/src/Umbraco.Web.UI.Client/src/views/languages/overview.html index c0a94e3dad..95cb7c535f 100644 --- a/src/Umbraco.Web.UI.Client/src/views/languages/overview.html +++ b/src/Umbraco.Web.UI.Client/src/views/languages/overview.html @@ -38,7 +38,7 @@ - + {{ language.name }} diff --git a/src/Umbraco.Web.UI.Client/src/views/mediaTypes/create.html b/src/Umbraco.Web.UI.Client/src/views/mediaTypes/create.html index 0fa5aa61c8..1ce8ab1465 100644 --- a/src/Umbraco.Web.UI.Client/src/views/mediaTypes/create.html +++ b/src/Umbraco.Web.UI.Client/src/views/mediaTypes/create.html @@ -47,7 +47,7 @@
diff --git a/src/Umbraco.Web.UI.Client/src/views/memberTypes/create.html b/src/Umbraco.Web.UI.Client/src/views/memberTypes/create.html index 46b51b5f34..673b90ef85 100644 --- a/src/Umbraco.Web.UI.Client/src/views/memberTypes/create.html +++ b/src/Umbraco.Web.UI.Client/src/views/memberTypes/create.html @@ -1,47 +1,53 @@ -