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/build/NuSpecs/UmbracoCms.Core.nuspec b/build/NuSpecs/UmbracoCms.Core.nuspec deleted file mode 100644 index 3f2aafb259..0000000000 --- a/build/NuSpecs/UmbracoCms.Core.nuspec +++ /dev/null @@ -1,96 +0,0 @@ - - - - Umbraco.Cms.Core - 9.0.0 - Umbraco Cms Core Binaries - Umbraco HQ - Umbraco HQ - MIT - https://umbraco.com/ - https://umbraco.com/dist/nuget/logo-small.png - false - Contains the core assemblies needed to run Umbraco Cms. This package only contains assemblies and can be used for package development. Use the UmbracoCms package to setup Umbraco in Visual Studio as an ASP.NET Core project. - Contains the core assemblies needed to run Umbraco Cms - en-US - umbraco - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/build/NuSpecs/UmbracoCms.Examine.Lucene.nuspec b/build/NuSpecs/UmbracoCms.Examine.Lucene.nuspec new file mode 100644 index 0000000000..19d60f27a9 --- /dev/null +++ b/build/NuSpecs/UmbracoCms.Examine.Lucene.nuspec @@ -0,0 +1,49 @@ + + + + Umbraco.Cms.Examine.Lucene + 9.0.0 + Umbraco CMS Examine Binaries + Umbraco HQ + Umbraco HQ + MIT + https://umbraco.com/ + https://umbraco.com/dist/nuget/logo-small.png + false + Contains the Examine assemblies needed to run Umbraco Cms. This package only contains assemblies and can be used for package development. Use the UmbracoCms package to setup Umbraco in Visual Studio as an ASP.NET Core project. + Contains dll files required to run Examine. + en-US + umbraco + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/build/NuSpecs/UmbracoCms.Web.nuspec b/build/NuSpecs/UmbracoCms.Web.nuspec deleted file mode 100644 index f12ada7e64..0000000000 --- a/build/NuSpecs/UmbracoCms.Web.nuspec +++ /dev/null @@ -1,57 +0,0 @@ - - - - Umbraco.Cms.Web - 9.0.0 - Umbraco Cms Core Binaries - Umbraco HQ - Umbraco HQ - MIT - https://umbraco.com/ - https://umbraco.com/dist/nuget/logo-small.png - false - Contains the web assemblies needed to run Umbraco Cms. This package only contains assemblies and can be used for package development. Use the UmbracoCms package to setup Umbraco in Visual Studio as an ASP.NET Core project. - Contains the core assemblies needed to run Umbraco Cms - en-US - umbraco - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/build/NuSpecs/UmbracoCms.nuspec b/build/NuSpecs/UmbracoCms.nuspec index ef48ce0de2..c031d71704 100644 --- a/build/NuSpecs/UmbracoCms.nuspec +++ b/build/NuSpecs/UmbracoCms.nuspec @@ -17,7 +17,8 @@ - + + - diff --git a/build/build.ps1 b/build/build.ps1 index 58d56fcdfe..bc39d04083 100644 --- a/build/build.ps1 +++ b/build/build.ps1 @@ -194,14 +194,12 @@ # remove extra files $webAppBin = "$($this.BuildTemp)\WebApp\bin" - $excludeDirs = @("$($webAppBin)\Config","$($webAppBin)\refs","$($webAppBin)\runtimes","$($webAppBin)\Umbraco","$($webAppBin)\wwwroot") + $excludeDirs = @("$($webAppBin)\refs","$($webAppBin)\runtimes","$($webAppBin)\Umbraco","$($webAppBin)\wwwroot") $excludeFiles = @("$($webAppBin)\appsettings.*","$($webAppBin)\*.deps.json","$($webAppBin)\*.exe","$($webAppBin)\*.config","$($webAppBin)\*.runtimeconfig.json") $this.RemoveDirectory($excludeDirs) $this.RemoveFile($excludeFiles) # copy rest of the files into WebApp - $this.CopyFiles("$($this.SolutionRoot)\src\Umbraco.Web.UI.NetCore\Config", "*", "$($this.BuildTemp)\WebApp\config") - $this.RemoveFile("$($this.BuildTemp)\WebApp\Config\*.Release.*") $this.CopyFiles("$($this.SolutionRoot)\src\Umbraco.Web.UI.NetCore\Umbraco", "*", "$($this.BuildTemp)\WebApp\umbraco") $excludeUmbracoDirs = @("$($this.BuildTemp)\WebApp\umbraco\lib") $this.RemoveDirectory($excludeUmbracoDirs) @@ -300,8 +298,6 @@ # create directories Write-Host "Create directories" - mkdir "$tmp\Configs" > $null - mkdir "$tmp\Configs\Lang" > $null mkdir "$tmp\WebApp\App_Data" > $null mkdir "$tmp\Templates" > $null #mkdir "$tmp\WebApp\Media" > $null @@ -311,14 +307,6 @@ Write-Host "Copy xml documentation" Copy-Item -force "$tmp\bin\*.xml" "$tmp\WebApp\bin" - Write-Host "Copy transformed configs and langs" - # note: exclude imageprocessor/*.config as imageprocessor pkg installs them - $this.CopyFiles("$tmp\WebApp\config", "*.config", "$tmp\Configs", ` - { -not $_.RelativeName.StartsWith("imageprocessor") }) - $this.CopyFiles("$tmp\WebApp\config", "*.js", "$tmp\Configs") - $this.CopyFiles("$tmp\WebApp\config\lang", "*.xml", "$tmp\Configs\Lang") - #$this.CopyFile("$tmp\WebApp\web.config", "$tmp\Configs\web.config.transform") - # Write-Host "Copy transformed web.config" # $this.CopyFile("$src\Umbraco.Web.UI\web.$buildConfiguration.Config.transformed", "$tmp\WebApp\web.config") @@ -424,7 +412,7 @@ Write-Host "Restore NuGet" Write-Host "Logging to $($this.BuildTemp)\nuget.restore.log" $params = "-Source", $nugetsourceUmbraco - &$this.BuildEnv.NuGet restore "$($this.SolutionRoot)\src\Umbraco.sln" > "$($this.BuildTemp)\nuget.restore.log" @params + &$this.BuildEnv.NuGet restore "$($this.SolutionRoot)\src\umbraco-netcore-only.sln" > "$($this.BuildTemp)\nuget.restore.log" @params if (-not $?) { throw "Failed to restore NuGet packages." } }) @@ -435,17 +423,11 @@ Write-Host "Create NuGet packages" - &$this.BuildEnv.NuGet Pack "$nuspecs\UmbracoCms.Core.nuspec" ` - -Properties BuildTmp="$($this.BuildTemp)" ` - -Version "$($this.Version.Semver.ToString())" ` - -Verbosity detailed -outputDirectory "$($this.BuildOutput)" > "$($this.BuildTemp)\nupack.cmscore.log" - if (-not $?) { throw "Failed to pack NuGet UmbracoCms.Core." } - - &$this.BuildEnv.NuGet Pack "$nuspecs\UmbracoCms.Web.nuspec" ` - -Properties BuildTmp="$($this.BuildTemp)" ` - -Version "$($this.Version.Semver.ToString())" ` - -Verbosity detailed -outputDirectory "$($this.BuildOutput)" > "$($this.BuildTemp)\nupack.cmsweb.log" - if (-not $?) { throw "Failed to pack NuGet UmbracoCms.Web." } + &dotnet pack "$($this.SolutionRoot)\src\umbraco-netcore-only.sln" ` + --output "$($this.BuildOutput)" ` + --verbosity detailed ` + -c Release ` + -p:PackageVersion="$($this.Version.Semver.ToString())" > "$($this.BuildTemp)\pack.umbraco.log" &$this.BuildEnv.NuGet Pack "$nuspecs\UmbracoCms.nuspec" ` -Properties BuildTmp="$($this.BuildTemp)" ` @@ -453,6 +435,13 @@ -Verbosity detailed -outputDirectory "$($this.BuildOutput)" > "$($this.BuildTemp)\nupack.cms.log" if (-not $?) { throw "Failed to pack NuGet UmbracoCms." } + &$this.BuildEnv.NuGet Pack "$nuspecs\UmbracoCms.Examine.Lucene.nuspec" ` + -Properties BuildTmp="$($this.BuildTemp)" ` + -Version "$($this.Version.Semver.ToString())" ` + -Verbosity detailed ` + -outputDirectory "$($this.BuildOutput)" > "$($this.BuildTemp)\nupack.examine.lucene.log" + if (-not $?) { throw "Failed to pack Nuget UmbracoCms.Lucene.nuspec"} + &$this.BuildEnv.NuGet Pack "$nuspecs\UmbracoCms.SqlCe.nuspec" ` -Properties BuildTmp="$($this.BuildTemp)" ` -Version "$($this.Version.Semver.ToString())" ` @@ -477,7 +466,7 @@ $ubuild.DefineMethod("VerifyNuGet", { $this.VerifyNuGetConsistency( - ("UmbracoCms", "UmbracoCms.Core", "UmbracoCms.Web"), + ("UmbracoCms"), ("Umbraco.Core", "Umbraco.Infrastructure", "Umbraco.Web.UI.NetCore", "Umbraco.Examine.Lucene", "Umbraco.PublishedCache.NuCache", "Umbraco.Web.Common", "Umbraco.Web.Website", "Umbraco.Web.BackOffice", "Umbraco.Persistence.SqlCe")) if ($this.OnError()) { return } }) diff --git a/build/templates/UmbracoSolution/.template.config/template.json b/build/templates/UmbracoSolution/.template.config/template.json index 7f69b5b0e8..a85a4f4af8 100644 --- a/build/templates/UmbracoSolution/.template.config/template.json +++ b/build/templates/UmbracoSolution/.template.config/template.json @@ -15,7 +15,7 @@ "version": { "type": "parameter", "datatype": "string", - "defaultValue": "0.5.0-alpha003", + "defaultValue": "9.0.0-alpha004", "description": "The version of Umbraco to load using NuGet", "replaces": "UMBRACO_VERSION_FROM_TEMPLATE" }, diff --git a/src/Directory.Build.props b/src/Directory.Build.props index cdce38df2f..c4346a0603 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -2,12 +2,20 @@ 9.0.0 9.0.0 - 9.0.0-beta001 + 9.0.0-alpha004 9.0.0 9.0 en-US Umbraco CMS Copyright © Umbraco 2021 + Umbraco HQ + https://umbraco.com/ + https://umbraco.com/dist/nuget/logo-small.png + https://opensource.org/licenses/MIT + false + umbraco + git + https://github.com/umbraco/umbraco-cms 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/Grid/GridEditorsConfig.cs b/src/Umbraco.Core/Configuration/Grid/GridEditorsConfig.cs index 680c47590e..ab6a7e9396 100644 --- a/src/Umbraco.Core/Configuration/Grid/GridEditorsConfig.cs +++ b/src/Umbraco.Core/Configuration/Grid/GridEditorsConfig.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.IO; +using System.Text; using Microsoft.Extensions.Logging; using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Hosting; @@ -51,6 +52,16 @@ namespace Umbraco.Cms.Core.Configuration.Grid _logger.LogError(ex, "Could not parse the contents of grid.editors.config.js into a JSON array '{Json}", sourceString); } } + else// Read default from embedded file + { + var assembly = GetType().Assembly; + var resourceStream = assembly.GetManifestResourceStream( + "Umbraco.Cms.Core.EmbeddedResources.Grid.grid.editors.config.js"); + + using var reader = new StreamReader(resourceStream, Encoding.UTF8); + var sourceString = reader.ReadToEnd(); + editors.AddRange(_jsonSerializer.Deserialize>(sourceString)); + } // add manifest editors, skip duplicates foreach (var gridEditor in _manifestParser.Manifest.GridEditors) 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 f8cc97acb8..a31edd5a03 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(20); /// /// Gets or sets a value for the default UI language. @@ -124,5 +126,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/RichTextEditorSettings.cs b/src/Umbraco.Core/Configuration/Models/RichTextEditorSettings.cs new file mode 100644 index 0000000000..cdd88ca409 --- /dev/null +++ b/src/Umbraco.Core/Configuration/Models/RichTextEditorSettings.cs @@ -0,0 +1,77 @@ +using System.Collections.Generic; +using Umbraco.Cms.Core.Models.ContentEditing; + +namespace Umbraco.Cms.Core.Configuration.Models +{ + public class RichTextEditorSettings + { + private static readonly string[] s_default_plugins = new[] + { + "paste", + "anchor", + "charmap", + "table", + "lists", + "advlist", + "hr", + "autolink", + "directionality", + "tabfocus", + "searchreplace" + }; + private static readonly RichTextEditorCommand[] s_default_commands = new [] + { + new RichTextEditorCommand(){Alias = "ace" , Name = "Source code editor" , Mode = RichTextEditorCommandMode.Insert}, + new RichTextEditorCommand(){Alias = "removeformat" , Name = "Remove format" , Mode = RichTextEditorCommandMode.Selection}, + new RichTextEditorCommand(){Alias = "undo" , Name = "Undo" , Mode = RichTextEditorCommandMode.Insert}, + new RichTextEditorCommand(){Alias = "redo" , Name = "Redo" , Mode = RichTextEditorCommandMode.Insert}, + new RichTextEditorCommand(){Alias = "cut" , Name = "Cut" , Mode = RichTextEditorCommandMode.Selection}, + new RichTextEditorCommand(){Alias = "copy" , Name = "Copy" , Mode = RichTextEditorCommandMode.Selection}, + new RichTextEditorCommand(){Alias = "paste" , Name = "Paste" , Mode = RichTextEditorCommandMode.All}, + new RichTextEditorCommand(){Alias = "styleselect" , Name = "Style select" , Mode = RichTextEditorCommandMode.All}, + new RichTextEditorCommand(){Alias = "bold" , Name = "Bold" , Mode = RichTextEditorCommandMode.Selection}, + new RichTextEditorCommand(){Alias = "italic" , Name = "Italic" , Mode = RichTextEditorCommandMode.Selection}, + new RichTextEditorCommand(){Alias = "underline" , Name = "Underline" , Mode = RichTextEditorCommandMode.Selection}, + new RichTextEditorCommand(){Alias = "strikethrough" , Name = "Strikethrough" , Mode = RichTextEditorCommandMode.Selection}, + new RichTextEditorCommand(){Alias = "alignleft" , Name = "Justify left" , Mode = RichTextEditorCommandMode.Selection}, + new RichTextEditorCommand(){Alias = "aligncenter" , Name = "Justify center" , Mode = RichTextEditorCommandMode.Selection}, + new RichTextEditorCommand(){Alias = "alignright" , Name = "Justify right" , Mode = RichTextEditorCommandMode.Selection}, + new RichTextEditorCommand(){Alias = "alignjustify" , Name = "Justify full" , Mode = RichTextEditorCommandMode.Selection}, + new RichTextEditorCommand(){Alias = "bullist" , Name = "Bullet list" , Mode = RichTextEditorCommandMode.All}, + new RichTextEditorCommand(){Alias = "numlist" , Name = "Numbered list" , Mode = RichTextEditorCommandMode.All}, + new RichTextEditorCommand(){Alias = "outdent" , Name = "Decrease indent" , Mode = RichTextEditorCommandMode.All}, + new RichTextEditorCommand(){Alias = "indent" , Name = "Increase indent" , Mode = RichTextEditorCommandMode.All}, + new RichTextEditorCommand(){Alias = "link" , Name = "Insert/edit link" , Mode = RichTextEditorCommandMode.All}, + new RichTextEditorCommand(){Alias = "unlink" , Name = "Remove link" , Mode = RichTextEditorCommandMode.Selection}, + new RichTextEditorCommand(){Alias = "anchor" , Name = "Anchor" , Mode = RichTextEditorCommandMode.Selection}, + new RichTextEditorCommand(){Alias = "umbmediapicker" , Name = "Image" , Mode = RichTextEditorCommandMode.Insert}, + new RichTextEditorCommand(){Alias = "umbmacro" , Name = "Macro" , Mode = RichTextEditorCommandMode.All}, + new RichTextEditorCommand(){Alias = "table" , Name = "Table" , Mode = RichTextEditorCommandMode.Insert}, + new RichTextEditorCommand(){Alias = "umbembeddialog" , Name = "Embed" , Mode = RichTextEditorCommandMode.Insert}, + new RichTextEditorCommand(){Alias = "hr" , Name = "Horizontal rule" , Mode = RichTextEditorCommandMode.Insert}, + new RichTextEditorCommand(){Alias = "subscript" , Name = "Subscript" , Mode = RichTextEditorCommandMode.Selection}, + new RichTextEditorCommand(){Alias = "superscript" , Name = "Superscript" , Mode = RichTextEditorCommandMode.Selection}, + new RichTextEditorCommand(){Alias = "charmap" , Name = "Character map" , Mode = RichTextEditorCommandMode.Insert}, + new RichTextEditorCommand(){Alias = "rtl" , Name = "Right to left" , Mode = RichTextEditorCommandMode.Selection}, + new RichTextEditorCommand(){Alias = "ltr" , Name = "Left to right" , Mode = RichTextEditorCommandMode.Selection}, + }; + + private static readonly IDictionary s_default_custom_config = new Dictionary() + { + ["entity_encoding"] = "raw" + }; + + public RichTextEditorCommand[] Commands { get; set; } = s_default_commands; + public string[] Plugins { get; set; } = s_default_plugins; + public IDictionary CustomConfig { get; set; } = s_default_custom_config; + public string ValidElements { get; set; } = "+a[id|style|rel|data-id|data-udi|rev|charset|hreflang|dir|lang|tabindex|accesskey|type|name|href|target|title|class|onfocus|onblur|onclick|ondblclick|onmousedown|onmouseup|onmouseover|onmousemove|onmouseout|onkeypress|onkeydown|onkeyup],-strong/-b[class|style],-em/-i[class|style],-strike[class|style],-u[class|style],#p[id|style|dir|class|align],-ol[class|reversed|start|style|type],-ul[class|style],-li[class|style],br[class],img[id|dir|lang|longdesc|usemap|style|class|src|onmouseover|onmouseout|border|alt=|title|hspace|vspace|width|height|align|umbracoorgwidth|umbracoorgheight|onresize|onresizestart|onresizeend|rel|data-id],-sub[style|class],-sup[style|class],-blockquote[dir|style|class],-table[border=0|cellspacing|cellpadding|width|height|class|align|summary|style|dir|id|lang|bgcolor|background|bordercolor],-tr[id|lang|dir|class|rowspan|width|height|align|valign|style|bgcolor|background|bordercolor],tbody[id|class],thead[id|class],tfoot[id|class],#td[id|lang|dir|class|colspan|rowspan|width|height|align|valign|style|bgcolor|background|bordercolor|scope],-th[id|lang|dir|class|colspan|rowspan|width|height|align|valign|style|scope],caption[id|lang|dir|class|style],-div[id|dir|class|align|style],-span[class|align|style],-pre[class|align|style],address[class|align|style],-h1[id|dir|class|align|style],-h2[id|dir|class|align|style],-h3[id|dir|class|align|style],-h4[id|dir|class|align|style],-h5[id|dir|class|align|style],-h6[id|style|dir|class|align|style],hr[class|style],small[class|style],dd[id|class|title|style|dir|lang],dl[id|class|title|style|dir|lang],dt[id|class|title|style|dir|lang],object[class|id|width|height|codebase|*],param[name|value|_value|class],embed[type|width|height|src|class|*],map[name|class],area[shape|coords|href|alt|target|class],bdo[class],button[class],iframe[*]"; + public string InvalidElements { get; set; } = "font"; + + public class RichTextEditorCommand + { + public string Alias { get; set; } + public string Name { get; set; } + public RichTextEditorCommandMode Mode { get; set; } + } + } +} diff --git a/src/Umbraco.Core/Configuration/Models/UnattendedSettings.cs b/src/Umbraco.Core/Configuration/Models/UnattendedSettings.cs index f8779d817c..26c3ee72c4 100644 --- a/src/Umbraco.Core/Configuration/Models/UnattendedSettings.cs +++ b/src/Umbraco.Core/Configuration/Models/UnattendedSettings.cs @@ -19,6 +19,12 @@ namespace Umbraco.Cms.Core.Configuration.Models /// public bool InstallUnattended { get; set; } = false; + /// + /// Gets or sets a value indicating whether unattended upgrades are enabled. + /// + public bool UpgradeUnattended { get; set; } = false; + + /// /// Gets or sets a value to use for creating a user with a name for Unattended Installs /// 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/Configuration/ModelsBuilderConfigExtensions.cs b/src/Umbraco.Core/Configuration/ModelsBuilderConfigExtensions.cs index 3d620ee9e5..0f397a749f 100644 --- a/src/Umbraco.Core/Configuration/ModelsBuilderConfigExtensions.cs +++ b/src/Umbraco.Core/Configuration/ModelsBuilderConfigExtensions.cs @@ -1,6 +1,6 @@ -using System.Configuration; -using System.IO; +using System.IO; using Umbraco.Cms.Core.Configuration.Models; +using Umbraco.Cms.Core.Exceptions; using Umbraco.Cms.Core.Hosting; namespace Umbraco.Extensions @@ -30,7 +30,7 @@ namespace Umbraco.Extensions // unless AcceptUnsafeModelsDirectory and then everything is OK. if (!Path.IsPathRooted(root)) - throw new ConfigurationErrorsException($"Root is not rooted \"{root}\"."); + throw new ConfigurationException($"Root is not rooted \"{root}\"."); if (config.StartsWith("~/")) { @@ -43,7 +43,7 @@ namespace Umbraco.Extensions root = Path.GetFullPath(root); if (!dir.StartsWith(root) && !acceptUnsafe) - throw new ConfigurationErrorsException($"Invalid models directory \"{config}\"."); + throw new ConfigurationException($"Invalid models directory \"{config}\"."); return dir; } @@ -51,7 +51,7 @@ namespace Umbraco.Extensions if (acceptUnsafe) return Path.GetFullPath(config); - throw new ConfigurationErrorsException($"Invalid models directory \"{config}\"."); + throw new ConfigurationException($"Invalid models directory \"{config}\"."); } } } 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-Configuration.cs b/src/Umbraco.Core/Constants-Configuration.cs index 0d62094dad..d596d3feec 100644 --- a/src/Umbraco.Core/Constants-Configuration.cs +++ b/src/Umbraco.Core/Constants-Configuration.cs @@ -50,6 +50,7 @@ public const string ConfigTypeFinder = ConfigPrefix + "TypeFinder"; public const string ConfigWebRouting = ConfigPrefix + "WebRouting"; public const string ConfigUserPassword = ConfigPrefix + "Security:UserPassword"; + public const string ConfigRichTextEditor = ConfigPrefix + "RichTextEditor"; } } } 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/DependencyInjection/UmbracoBuilder.Configuration.cs b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Configuration.cs index 47a98ea9e1..c37f8fb760 100644 --- a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Configuration.cs +++ b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Configuration.cs @@ -56,6 +56,7 @@ namespace Umbraco.Cms.Core.DependencyInjection AddOptions(builder, Constants.Configuration.ConfigWebRouting); AddOptions(builder, Constants.Configuration.ConfigPlugins); AddOptions(builder, Constants.Configuration.ConfigUnattended); + AddOptions(builder, Constants.Configuration.ConfigRichTextEditor); return builder; } diff --git a/src/Umbraco.Core/Editors/UserEditorAuthorizationHelper.cs b/src/Umbraco.Core/Editors/UserEditorAuthorizationHelper.cs index 0ecbdbd4ab..7cb97ec57c 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; } /// @@ -76,6 +79,18 @@ namespace Umbraco.Cms.Core.Editors if (userGroupAliases != null) { var savingGroupAliases = userGroupAliases.ToArray(); + var existingGroupAliases = savingUser == null + ? new string[0] + : savingUser.Groups.Select(x => x.Alias).ToArray(); + + var addedGroupAliases = savingGroupAliases.Except(existingGroupAliases); + + // As we know the current user is not admin, it is only allowed to use groups that the user do have themselves. + var savingGroupAliasesNotAllowed = addedGroupAliases.Except(currentUser.Groups.Select(x=>x.Alias)).ToArray(); + if (savingGroupAliasesNotAllowed.Any()) + { + return Attempt.Fail("Cannot assign the group(s) '" + string.Join(", ", savingGroupAliasesNotAllowed) + "', the current user is not part of them or admin"); + } //only validate any groups that have changed. //a non-admin user can remove groups and add groups that they have access to @@ -91,9 +106,7 @@ namespace Umbraco.Cms.Core.Editors if (userGroupsChanged) { // d) A user cannot assign a group to another user that they do not belong to - var currentUserGroups = currentUser.Groups.Select(x => x.Alias).ToArray(); - foreach (var group in newGroups) { if (currentUserGroups.Contains(group) == false) @@ -115,7 +128,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 +136,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 +149,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 +157,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.Web.UI.NetCore/config/grid.editors.config.js b/src/Umbraco.Core/EmbeddedResources/Grid/grid.editors.config.js similarity index 100% rename from src/Umbraco.Web.UI.NetCore/config/grid.editors.config.js rename to src/Umbraco.Core/EmbeddedResources/Grid/grid.editors.config.js diff --git a/src/Umbraco.Core/Exceptions/ConfigurationException.cs b/src/Umbraco.Core/Exceptions/ConfigurationException.cs new file mode 100644 index 0000000000..fe711a9823 --- /dev/null +++ b/src/Umbraco.Core/Exceptions/ConfigurationException.cs @@ -0,0 +1,41 @@ +using System; +using System.Runtime.Serialization; + +namespace Umbraco.Cms.Core.Exceptions +{ + /// + /// An exception that is thrown if the configuration is wrong. + /// + /// + [Serializable] + public class ConfigurationException : Exception + { + /// + /// Initializes a new instance of the class with a specified error message. + /// + /// The message that describes the error. + public ConfigurationException(string message) + : base(message) + { } + + /// + /// Initializes a new instance of the class with a specified error message + /// and a reference to the inner exception which is the cause of this exception. + /// + /// The message that describes the error. + /// The inner exception, or null. + public ConfigurationException(string message, Exception innerException) + : base(message, innerException) + { } + + /// + /// Initializes a new instance of the class. + /// + /// The that holds the serialized object data about the exception being thrown. + /// The that contains contextual information about the source or destination. + protected ConfigurationException(SerializationInfo info, StreamingContext context) + : base(info, context) + { } + + } +} 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/ILogViewerQuery.cs b/src/Umbraco.Core/Models/ILogViewerQuery.cs new file mode 100644 index 0000000000..3b36f0a9e8 --- /dev/null +++ b/src/Umbraco.Core/Models/ILogViewerQuery.cs @@ -0,0 +1,10 @@ +using Umbraco.Cms.Core.Models.Entities; + +namespace Umbraco.Cms.Core.Models +{ + public interface ILogViewerQuery : IEntity + { + string Name { get; set; } + string Query { get; set; } + } +} diff --git a/src/Umbraco.Core/Models/LogViewerQuery.cs b/src/Umbraco.Core/Models/LogViewerQuery.cs new file mode 100644 index 0000000000..5addfa705f --- /dev/null +++ b/src/Umbraco.Core/Models/LogViewerQuery.cs @@ -0,0 +1,34 @@ +using System; +using System.Runtime.Serialization; +using Umbraco.Cms.Core.Models.Entities; + +namespace Umbraco.Cms.Core.Models +{ + [Serializable] + [DataContract(IsReference = true)] + public class LogViewerQuery : EntityBase, ILogViewerQuery + { + private string _name; + private string _query; + + public LogViewerQuery(string name, string query) + { + Name = name; + _query = query; + } + + [DataMember] + public string Name + { + get => _name; + set => SetPropertyValueAndDetectChanges(value, ref _name, nameof(Name)); + } + + [DataMember] + public string Query + { + get => _query; + set => SetPropertyValueAndDetectChanges(value, ref _query, nameof(Query)); + } + } +} 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/Membership/UserGroup.cs b/src/Umbraco.Core/Models/Membership/UserGroup.cs index 0c5f4a7d66..fd173caa73 100644 --- a/src/Umbraco.Core/Models/Membership/UserGroup.cs +++ b/src/Umbraco.Core/Models/Membership/UserGroup.cs @@ -21,7 +21,7 @@ namespace Umbraco.Cms.Core.Models.Membership private string _icon; private string _name; private IEnumerable _permissions; - private readonly List _sectionCollection; + private List _sectionCollection; //Custom comparer for enumerable private static readonly DelegateEqualityComparer> StringEnumerableComparer = @@ -104,7 +104,10 @@ namespace Umbraco.Cms.Core.Models.Membership set => SetPropertyValueAndDetectChanges(value, ref _permissions, nameof(Permissions), StringEnumerableComparer); } - public IEnumerable AllowedSections => _sectionCollection; + public IEnumerable AllowedSections + { + get => _sectionCollection; + } public void RemoveAllowedSection(string sectionAlias) { @@ -124,5 +127,16 @@ namespace Umbraco.Cms.Core.Models.Membership } public int UserCount { get; } + + protected override void PerformDeepClone(object clone) + { + + base.PerformDeepClone(clone); + + var clonedEntity = (UserGroup)clone; + + //manually clone the start node props + clonedEntity._sectionCollection = new List(_sectionCollection); + } } } diff --git a/src/Umbraco.Core/Models/UserExtensions.cs b/src/Umbraco.Core/Models/UserExtensions.cs index 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/Persistence/Constants-DatabaseSchema.cs b/src/Umbraco.Core/Persistence/Constants-DatabaseSchema.cs index 73004c491d..b09991ae20 100644 --- a/src/Umbraco.Core/Persistence/Constants-DatabaseSchema.cs +++ b/src/Umbraco.Core/Persistence/Constants-DatabaseSchema.cs @@ -75,6 +75,8 @@ namespace Umbraco.Cms.Core public const string AuditEntry = TableNamePrefix + "Audit"; public const string Consent = TableNamePrefix + "Consent"; public const string UserLogin = TableNamePrefix + "UserLogin"; + + public const string LogViewerQuery = TableNamePrefix + "LogViewerQuery"; } } } diff --git a/src/Umbraco.Core/Persistence/Repositories/ILogViewerQueryRepository.cs b/src/Umbraco.Core/Persistence/Repositories/ILogViewerQueryRepository.cs new file mode 100644 index 0000000000..d21cd2aa1e --- /dev/null +++ b/src/Umbraco.Core/Persistence/Repositories/ILogViewerQueryRepository.cs @@ -0,0 +1,9 @@ +using Umbraco.Cms.Core.Models; + +namespace Umbraco.Cms.Core.Persistence.Repositories +{ + public interface ILogViewerQueryRepository : IReadWriteQueryRepository + { + ILogViewerQuery GetByName(string name); + } +} diff --git a/src/Umbraco.Core/PropertyEditors/ConfigurationEditor.cs b/src/Umbraco.Core/PropertyEditors/ConfigurationEditor.cs index d4f1d84984..dc1126a3c3 100644 --- a/src/Umbraco.Core/PropertyEditors/ConfigurationEditor.cs +++ b/src/Umbraco.Core/PropertyEditors/ConfigurationEditor.cs @@ -10,6 +10,7 @@ namespace Umbraco.Cms.Core.PropertyEditors /// /// Represents a data type configuration editor. /// + [DataContract] public class ConfigurationEditor : IConfigurationEditor { private IDictionary _defaultConfiguration; diff --git a/src/Umbraco.Core/PropertyEditors/IConfigurationEditor.cs b/src/Umbraco.Core/PropertyEditors/IConfigurationEditor.cs index 413f7ee24b..4144c7c0a8 100644 --- a/src/Umbraco.Core/PropertyEditors/IConfigurationEditor.cs +++ b/src/Umbraco.Core/PropertyEditors/IConfigurationEditor.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Runtime.Serialization; using Umbraco.Cms.Core.Serialization; namespace Umbraco.Cms.Core.PropertyEditors @@ -11,6 +12,7 @@ namespace Umbraco.Cms.Core.PropertyEditors /// /// Gets the fields. /// + [DataMember(Name = "fields")] List Fields { get; } /// @@ -22,6 +24,7 @@ namespace Umbraco.Cms.Core.PropertyEditors /// equivalent of an actual configuration object (ie an instance of TConfiguration, obtained /// via . /// + [DataMember(Name = "defaultConfig")] IDictionary DefaultConfiguration { get; } /// 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..86ac97db31 100644 --- a/src/Umbraco.Core/Routing/PublishedRouter.cs +++ b/src/Umbraco.Core/Routing/PublishedRouter.cs @@ -413,6 +413,15 @@ namespace Umbraco.Cms.Core.Routing _logger.LogDebug("Finder {ContentFinderType}", finder.GetType().FullName); return finder.TryFindContent(request); }); + + _logger.LogDebug( + "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 be36173981..ce524a09a1 100644 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -1,9 +1,13 @@ - + netstandard2.0 Umbraco.Cms.Core Umbraco CMS + Umbraco.Cms.Core + Umbraco CMS Core + Contains the core assembly needed to run Umbraco Cms. This package only contains the assembly, and can be used for package development. Use the template in the Umbraco.Templates package to setup Umbraco + Umbraco CMS @@ -12,13 +16,13 @@ + - @@ -43,4 +47,8 @@ <_Parameter1>DynamicProxyGenAssembly2 + + + + 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.Examine.Lucene/Umbraco.Examine.Lucene.csproj b/src/Umbraco.Examine.Lucene/Umbraco.Examine.Lucene.csproj index 43f06ceef3..329c11f879 100644 --- a/src/Umbraco.Examine.Lucene/Umbraco.Examine.Lucene.csproj +++ b/src/Umbraco.Examine.Lucene/Umbraco.Examine.Lucene.csproj @@ -5,6 +5,11 @@ Umbraco.Cms.Infrastructure.Examine Umbraco CMS Umbraco.Examine.Lucene + + + false + + Umbraco.Cms.Examine.Lucene 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 e97367b804..e3b327ffaa 100644 --- a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs +++ b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs @@ -125,8 +125,6 @@ namespace Umbraco.Cms.Infrastructure.DependencyInjection builder.Services.AddUnique(); - builder.Services.AddUnique(); - // register *all* checks, except those marked [HideFromTypeFinder] of course builder.Services.AddUnique(); @@ -192,8 +190,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(); @@ -201,7 +199,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/DependencyInjection/UmbracoBuilder.Repositories.cs b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Repositories.cs index 8292fd2ecb..710565500e 100644 --- a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Repositories.cs +++ b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Repositories.cs @@ -55,6 +55,7 @@ namespace Umbraco.Cms.Infrastructure.DependencyInjection builder.Services.AddUnique(); builder.Services.AddUnique(); builder.Services.AddUnique(); + builder.Services.AddUnique(); return builder; } 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..a758b08081 100644 --- a/src/Umbraco.Infrastructure/Logging/Viewer/LogViewerConfig.cs +++ b/src/Umbraco.Infrastructure/Logging/Viewer/LogViewerConfig.cs @@ -1,85 +1,45 @@ using System.Collections.Generic; -using System.IO; using System.Linq; -using Newtonsoft.Json; -using Umbraco.Cms.Core.Hosting; -using Umbraco.Cms.Core.Routing; -using Formatting = Newtonsoft.Json.Formatting; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Persistence.Repositories; +using Umbraco.Cms.Core.Scoping; namespace Umbraco.Cms.Core.Logging.Viewer { public class LogViewerConfig : ILogViewerConfig { - private readonly IHostingEnvironment _hostingEnvironment; - private static readonly string _pathToSearches = WebPath.Combine(Cms.Core.Constants.SystemDirectories.Config, "logviewer.searches.config.js"); - private readonly FileInfo _searchesConfig; + private readonly ILogViewerQueryRepository _logViewerQueryRepository; + private readonly IScopeProvider _scopeProvider; - public LogViewerConfig(IHostingEnvironment hostingEnvironment) + public LogViewerConfig(ILogViewerQueryRepository logViewerQueryRepository, IScopeProvider scopeProvider) { - _hostingEnvironment = hostingEnvironment; - var trimmedPath = _pathToSearches.TrimStart('~', '/').Replace('/', Path.DirectorySeparatorChar); - var absolutePath = Path.Combine(_hostingEnvironment.ApplicationPhysicalPath, trimmedPath); - _searchesConfig = new FileInfo(absolutePath); + _logViewerQueryRepository = logViewerQueryRepository; + _scopeProvider = scopeProvider; } public IReadOnlyList GetSavedSearches() { - //Our default implementation - - //If file does not exist - lets create it with an empty array - EnsureFileExists(); - - var rawJson = System.IO.File.ReadAllText(_searchesConfig.FullName); - return JsonConvert.DeserializeObject(rawJson); + using var scope = _scopeProvider.CreateScope(autoComplete: true); + var logViewerQueries = _logViewerQueryRepository.GetMany(); + var result = logViewerQueries.Select(x => new SavedLogSearch() { Name = x.Name, Query = x.Query }).ToArray(); + return result; } public IReadOnlyList AddSavedSearch(string name, string query) { - //Get the existing items - var searches = GetSavedSearches().ToList(); + using var scope = _scopeProvider.CreateScope(autoComplete: true); + _logViewerQueryRepository.Save(new LogViewerQuery(name, query)); - //Add the new item to the bottom of the list - searches.Add(new SavedLogSearch { Name = name, Query = query }); - - //Serialize to JSON string - var rawJson = JsonConvert.SerializeObject(searches, Formatting.Indented); - - //If file does not exist - lets create it with an empty array - EnsureFileExists(); - - //Write it back down to file - System.IO.File.WriteAllText(_searchesConfig.FullName, rawJson); - - //Return the updated object - so we can instantly reset the entire array from the API response - //As opposed to push a new item into the array - return searches; + return GetSavedSearches(); } public IReadOnlyList DeleteSavedSearch(string name, string query) { - //Get the existing items - var searches = GetSavedSearches().ToList(); - - //Removes the search - searches.RemoveAll(s => s.Name.Equals(name) && s.Query.Equals(query)); - - //Serialize to JSON string - var rawJson = JsonConvert.SerializeObject(searches, Formatting.Indented); - - //Write it back down to file - System.IO.File.WriteAllText(_searchesConfig.FullName, rawJson); - + using var scope = _scopeProvider.CreateScope(autoComplete: true); + var item = _logViewerQueryRepository.GetByName(name); + _logViewerQueryRepository.Delete(item); //Return the updated object - so we can instantly reset the entire array from the API response - return searches; - } - - private void EnsureFileExists() - { - if (_searchesConfig.Exists) return; - using (var writer = _searchesConfig.CreateText()) - { - writer.Write("[]"); - } + return GetSavedSearches(); } } } diff --git a/src/Umbraco.Infrastructure/Migrations/Install/DatabaseDataCreator.cs b/src/Umbraco.Infrastructure/Migrations/Install/DatabaseDataCreator.cs index 72e8b864bf..30759ae789 100644 --- a/src/Umbraco.Infrastructure/Migrations/Install/DatabaseDataCreator.cs +++ b/src/Umbraco.Infrastructure/Migrations/Install/DatabaseDataCreator.cs @@ -1,9 +1,11 @@ using System; +using System.Linq; using Microsoft.Extensions.Logging; using NPoco; using Umbraco.Cms.Core.Configuration; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Infrastructure.Migrations.Upgrade; +using Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_9_0_0; using Umbraco.Cms.Infrastructure.Persistence.Dtos; using Umbraco.Extensions; @@ -76,6 +78,9 @@ namespace Umbraco.Cms.Infrastructure.Migrations.Install if (tableName.Equals(Cms.Core.Constants.DatabaseSchema.Tables.KeyValue)) CreateKeyValueData(); + if (tableName.Equals(Cms.Core.Constants.DatabaseSchema.Tables.LogViewerQuery)) + CreateLogViewerQueryData(); + _logger.LogInformation("Done creating table {TableName} data.", tableName); } @@ -345,5 +350,17 @@ namespace Umbraco.Cms.Infrastructure.Migrations.Install _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.KeyValue, "key", false, new KeyValueDto { Key = stateValueKey, Value = finalState, UpdateDate = DateTime.Now }); } + + private void CreateLogViewerQueryData() + { + var defaultData = MigrateLogViewerQueriesFromFileToDb.DefaultLogQueries.ToArray(); + + for (int i = 0; i < defaultData.Length; i++) + { + var dto = defaultData[i]; + dto.Id = i+1; + _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.LogViewerQuery, "id", false, dto); + } + } } } diff --git a/src/Umbraco.Infrastructure/Migrations/Install/DatabaseSchemaCreator.cs b/src/Umbraco.Infrastructure/Migrations/Install/DatabaseSchemaCreator.cs index d7db160b56..d4ce35aebc 100644 --- a/src/Umbraco.Infrastructure/Migrations/Install/DatabaseSchemaCreator.cs +++ b/src/Umbraco.Infrastructure/Migrations/Install/DatabaseSchemaCreator.cs @@ -91,7 +91,8 @@ namespace Umbraco.Cms.Infrastructure.Migrations.Install typeof (AuditEntryDto), typeof (ContentVersionCultureVariationDto), typeof (DocumentCultureVariationDto), - typeof (ContentScheduleDto) + typeof (ContentScheduleDto), + typeof (LogViewerQueryDto) }; /// diff --git a/src/Umbraco.Infrastructure/Migrations/PostMigrations/DeleteLogViewerQueryFile.cs b/src/Umbraco.Infrastructure/Migrations/PostMigrations/DeleteLogViewerQueryFile.cs new file mode 100644 index 0000000000..cc1828dc2e --- /dev/null +++ b/src/Umbraco.Infrastructure/Migrations/PostMigrations/DeleteLogViewerQueryFile.cs @@ -0,0 +1,34 @@ +using System.IO; +using Umbraco.Cms.Core.Hosting; +using Umbraco.Cms.Core.Migrations; +using Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_9_0_0; + +namespace Umbraco.Cms.Infrastructure.Migrations.PostMigrations +{ + /// + /// Deletes the old file that saved log queries + /// + public class DeleteLogViewerQueryFile : IMigration + { + private readonly IHostingEnvironment _hostingEnvironment; + + /// + /// Initializes a new instance of the class. + /// + public DeleteLogViewerQueryFile(IMigrationContext context, IHostingEnvironment hostingEnvironment) + { + _hostingEnvironment = hostingEnvironment; + } + + /// + public void Migrate() + { + var logViewerQueryFile = MigrateLogViewerQueriesFromFileToDb.GetLogViewerQueryFile(_hostingEnvironment); + + if(File.Exists(logViewerQueryFile)) + { + File.Delete(logViewerQueryFile); + } + } + } +} diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs index 9aacab1740..64d704bb11 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs @@ -9,6 +9,7 @@ using Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_10_0; using Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_6_0; using Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_7_0; using Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_9_0; +using Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_9_0_0; using Umbraco.Extensions; namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade @@ -197,6 +198,9 @@ namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade To("{B5838FF5-1D22-4F6C-BCEB-F83ACB14B575}"); // to 8.10.0 To("{D6A8D863-38EC-44FB-91EC-ACD6A668BD18}"); + + // to 8.10.0 + To("{22D801BA-A1FF-4539-BFCC-2139B55594F8}"); //FINAL } } 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/Migrations/Upgrade/V_9_0_0/MigrateLogViewerQueriesFromFileToDb.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_0_0/MigrateLogViewerQueriesFromFileToDb.cs new file mode 100644 index 0000000000..88ffe1a66c --- /dev/null +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_0_0/MigrateLogViewerQueriesFromFileToDb.cs @@ -0,0 +1,108 @@ +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using Newtonsoft.Json; +using Umbraco.Cms.Core.Hosting; +using Umbraco.Cms.Infrastructure.Migrations.PostMigrations; +using Umbraco.Cms.Infrastructure.Persistence.Dtos; +using Umbraco.Extensions; + +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_9_0_0 +{ + + public class MigrateLogViewerQueriesFromFileToDb : MigrationBase + { + private readonly IHostingEnvironment _hostingEnvironment; + internal static readonly IEnumerable DefaultLogQueries = new LogViewerQueryDto[] + { + new (){ + Name = "Find all logs where the Level is NOT Verbose and NOT Debug", + Query = "Not(@Level='Verbose') and Not(@Level='Debug')" + }, + new (){ + Name = "Find all logs that has an exception property (Warning, Error & Fatal with Exceptions)", + Query = "Has(@Exception)" + }, + new (){ + Name = "Find all logs that have the property 'Duration'", + Query = "Has(Duration)" + }, + new (){ + Name = "Find all logs that have the property 'Duration' and the duration is greater than 1000ms", + Query = "Has(Duration) and Duration > 1000" + }, + new (){ + Name = "Find all logs that are from the namespace 'Umbraco.Core'", + Query = "StartsWith(SourceContext, 'Umbraco.Core')" + }, + new (){ + Name = "Find all logs that use a specific log message template", + Query = "@MessageTemplate = '[Timing {TimingId}] {EndMessage} ({TimingDuration}ms)'" + }, + new (){ + Name = "Find logs where one of the items in the SortedComponentTypes property array is equal to", + Query = "SortedComponentTypes[?] = 'Umbraco.Web.Search.ExamineComponent'" + }, + new (){ + Name = "Find logs where one of the items in the SortedComponentTypes property array contains", + Query = "Contains(SortedComponentTypes[?], 'DatabaseServer')" + }, + new (){ + Name = "Find all logs that the message has localhost in it with SQL like", + Query = "@Message like '%localhost%'" + }, + new (){ + Name = "Find all logs that the message that starts with 'end' in it with SQL like", + Query = "@Message like 'end%'" + } + }; + + public MigrateLogViewerQueriesFromFileToDb(IMigrationContext context, IHostingEnvironment hostingEnvironment) + : base(context) + { + _hostingEnvironment = hostingEnvironment; + } + + public override void Migrate() + { + Debugger.Launch(); + Debugger.Break(); + CreateDatabaseTable(); + MigrateFileContentToDB(); + } + private void CreateDatabaseTable() + { + var tables = SqlSyntax.GetTablesInSchema(Context.Database); + if (!tables.InvariantContains(Core.Constants.DatabaseSchema.Tables.LogViewerQuery)) + { + Create.Table().Do(); + } + } + + internal static string GetLogViewerQueryFile(IHostingEnvironment hostingEnvironment) + { + return hostingEnvironment.MapPathContentRoot( + Path.Combine(Cms.Core.Constants.SystemDirectories.Config, "logviewer.searches.config.js")); + } + private void MigrateFileContentToDB() + { + var logViewerQueryFile = GetLogViewerQueryFile(_hostingEnvironment); + + var logQueriesInFile = File.Exists(logViewerQueryFile) ? + JsonConvert.DeserializeObject(File.ReadAllText(logViewerQueryFile)) + : DefaultLogQueries; + + var logQueriesInDb = Database.Query().ToArray(); + + if (logQueriesInDb.Any()) + { + return; + } + + Database.InsertBulk(logQueriesInFile); + + Context.AddPostMigration(); + } + } +} 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/Dtos/LogViewerQueryDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/LogViewerQueryDto.cs new file mode 100644 index 0000000000..71642c8b73 --- /dev/null +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/LogViewerQueryDto.cs @@ -0,0 +1,22 @@ +using NPoco; +using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; + +namespace Umbraco.Cms.Infrastructure.Persistence.Dtos +{ + [TableName(Cms.Core.Constants.DatabaseSchema.Tables.LogViewerQuery)] + [PrimaryKey("id")] + [ExplicitColumns] + internal class LogViewerQueryDto + { + [Column("id")] + [PrimaryKeyColumn] + public int Id { get; set; } + + [Column("name")] + [Index(IndexTypes.UniqueNonClustered, Name = "IX_LogViewerQuery_name")] + public string Name { get; set; } + + [Column("query")] + public string Query { get; set; } + } +} diff --git a/src/Umbraco.Infrastructure/Persistence/Mappers/LogViewerQueryMapper.cs b/src/Umbraco.Infrastructure/Persistence/Mappers/LogViewerQueryMapper.cs new file mode 100644 index 0000000000..807e3b6c02 --- /dev/null +++ b/src/Umbraco.Infrastructure/Persistence/Mappers/LogViewerQueryMapper.cs @@ -0,0 +1,22 @@ +using System; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Infrastructure.Persistence.Dtos; + +namespace Umbraco.Cms.Infrastructure.Persistence.Mappers +{ + [MapperFor(typeof(ILogViewerQuery))] + [MapperFor(typeof(LogViewerQuery))] + public sealed class LogViewerQueryMapper : BaseMapper + { + public LogViewerQueryMapper(Lazy sqlContext, MapperConfigurationStore maps) + : base(sqlContext, maps) + { } + + protected override void DefineMaps() + { + DefineMap(nameof(ILogViewerQuery.Id), nameof(LogViewerQueryDto.Id)); + DefineMap(nameof(ILogViewerQuery.Name), nameof(LogViewerQueryDto.Name)); + DefineMap(nameof(ILogViewerQuery.Query), nameof(LogViewerQueryDto.Query)); + } + } +} diff --git a/src/Umbraco.Infrastructure/Persistence/Mappers/MapperCollectionBuilder.cs b/src/Umbraco.Infrastructure/Persistence/Mappers/MapperCollectionBuilder.cs index c5f0814469..38f1176b75 100644 --- a/src/Umbraco.Infrastructure/Persistence/Mappers/MapperCollectionBuilder.cs +++ b/src/Umbraco.Infrastructure/Persistence/Mappers/MapperCollectionBuilder.cs @@ -53,6 +53,7 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Mappers Add(); Add(); Add(); + Add(); return this; } } 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/LogViewerQueryRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/LogViewerQueryRepository.cs new file mode 100644 index 0000000000..35f0b8fdab --- /dev/null +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/LogViewerQueryRepository.cs @@ -0,0 +1,140 @@ +using System; +using System.Collections.Generic; +using System.Data; +using System.Linq; +using Microsoft.Extensions.Logging; +using NPoco; +using Umbraco.Cms.Core.Cache; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Persistence.Querying; +using Umbraco.Cms.Core.Persistence.Repositories; +using Umbraco.Cms.Core.Scoping; +using Umbraco.Cms.Infrastructure.Persistence.Dtos; +using Umbraco.Extensions; + +namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement +{ + internal class LogViewerQueryRepository : EntityRepositoryBase, ILogViewerQueryRepository + { + public LogViewerQueryRepository(IScopeAccessor scopeAccessor, AppCaches cache, ILogger logger) + : base(scopeAccessor, cache, logger) + { } + + protected override IRepositoryCachePolicy CreateCachePolicy() + { + return new FullDataSetRepositoryCachePolicy(GlobalIsolatedCache, ScopeAccessor, GetEntityId, /*expires:*/ false); + } + + protected override IEnumerable PerformGetAll(params int[] ids) + { + var sql = GetBaseQuery(false).Where($"{Cms.Core.Constants.DatabaseSchema.Tables.LogViewerQuery}.id > 0"); + if (ids.Any()) + { + sql.Where($"{Cms.Core.Constants.DatabaseSchema.Tables.LogViewerQuery}.id in (@ids)", new { ids = ids }); + } + + return Database.Fetch(sql).Select(ConvertFromDto); + } + + protected override IEnumerable PerformGetByQuery(IQuery query) + { + throw new NotSupportedException("This repository does not support this method"); + } + + protected override Sql GetBaseQuery(bool isCount) + { + var sql = Sql(); + sql = isCount ? sql.SelectCount() : sql.Select(); + sql = sql.From(); + return sql; + } + + protected override string GetBaseWhereClause() + { + return $"{Cms.Core.Constants.DatabaseSchema.Tables.LogViewerQuery}.id = @id"; + } + + protected override IEnumerable GetDeleteClauses() + { + var list = new List + { + $"DELETE FROM {Cms.Core.Constants.DatabaseSchema.Tables.LogViewerQuery} WHERE id = @id" + }; + return list; + } + + protected override Guid NodeObjectTypeId + { + get { throw new NotImplementedException(); } + } + + protected override void PersistNewItem(ILogViewerQuery entity) + { + var exists = Database.ExecuteScalar($"SELECT COUNT(*) FROM {Core.Constants.DatabaseSchema.Tables.LogViewerQuery} WHERE name = @name", + new { name = entity.Name }); + if (exists > 0) throw new DuplicateNameException($"The log query name '{entity.Name}' is already used"); + + entity.AddingEntity(); + + var factory = new LogViewerQueryModelFactory(); + var dto = factory.BuildDto(entity); + + var id = Convert.ToInt32(Database.Insert(dto)); + entity.Id = id; + } + + protected override void PersistUpdatedItem(ILogViewerQuery entity) + { + entity.UpdatingEntity(); + + var exists = Database.ExecuteScalar($"SELECT COUNT(*) FROM {Core.Constants.DatabaseSchema.Tables.LogViewerQuery} WHERE name = @name AND id <> @id", + new { name = entity.Name, id = entity.Id }); + //ensure there is no other log query with the same name on another entity + if (exists > 0) throw new DuplicateNameException($"The log query name '{entity.Name}' is already used"); + + + var factory = new LogViewerQueryModelFactory(); + var dto = factory.BuildDto(entity); + + Database.Update(dto); + } + + private ILogViewerQuery ConvertFromDto(LogViewerQueryDto dto) + { + var factory = new LogViewerQueryModelFactory(); + var entity = factory.BuildEntity(dto); + return entity; + } + + internal class LogViewerQueryModelFactory + { + + public ILogViewerQuery BuildEntity(LogViewerQueryDto dto) + { + var logViewerQuery = new LogViewerQuery(dto.Name, dto.Query) + { + Id = dto.Id, + }; + return logViewerQuery; + } + + public LogViewerQueryDto BuildDto(ILogViewerQuery entity) + { + var dto = new LogViewerQueryDto { Name = entity.Name, Query = entity.Query, Id = entity.Id }; + return dto; + } + } + + protected override ILogViewerQuery PerformGet(int id) + { + //use the underlying GetAll which will force cache all log queries + return GetMany().FirstOrDefault(x => x.Id == id); + } + + public ILogViewerQuery GetByName(string name) + { + //use the underlying GetAll which will force cache all log queries + return GetMany().FirstOrDefault(x => x.Name == name); + } + } +} 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 279ab1215f..0270f77904 100644 --- a/src/Umbraco.Infrastructure/Persistence/SqlSyntax/SqlServerSyntaxProvider.cs +++ b/src/Umbraco.Infrastructure/Persistence/SqlSyntax/SqlServerSyntaxProvider.cs @@ -1,10 +1,13 @@ -using System; +using System; using System.Collections.Generic; 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,9 +266,19 @@ where tbl.[name]=@0 and col.[name]=@1;", tableName, columnName) 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.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) { - WriteLock(db, TimeSpan.FromSeconds(5), lockIds); + WriteLock(db, _globalSettings.Value.SqlWriteLockTimeOut, lockIds); } public void WriteLock(IDatabase db, TimeSpan timeout, params int[] lockIds) @@ -280,19 +300,33 @@ where tbl.[name]=@0 and col.[name]=@1;", tableName, columnName) 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) { - 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) { @@ -313,14 +347,23 @@ where tbl.[name]=@0 and col.[name]=@1;", tableName, columnName) 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.", nameof(lockId)); } } 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/GridPropertyEditor.cs b/src/Umbraco.Infrastructure/PropertyEditors/GridPropertyEditor.cs index a40ab92ca8..6e732cdc0f 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/GridPropertyEditor.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/GridPropertyEditor.cs @@ -202,7 +202,7 @@ namespace Umbraco.Cms.Core.PropertyEditors _richTextPropertyValueEditor.GetReferences(x.Value))) yield return umbracoEntityReference; - foreach (var umbracoEntityReference in mediaValues.SelectMany(x => + foreach (var umbracoEntityReference in mediaValues.Where(x=>x.Value.HasValues).SelectMany(x => _mediaPickerPropertyValueEditor.GetReferences(x.Value["udi"]))) yield return umbracoEntityReference; } diff --git a/src/Umbraco.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..ef1d808b9a 100644 --- a/src/Umbraco.Infrastructure/Runtime/CoreRuntime.cs +++ b/src/Umbraco.Infrastructure/Runtime/CoreRuntime.cs @@ -4,13 +4,16 @@ 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; namespace Umbraco.Cms.Infrastructure.Runtime { @@ -25,6 +28,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 +43,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 +56,8 @@ namespace Umbraco.Cms.Infrastructure.Runtime _databaseFactory = databaseFactory; _eventAggregator = eventAggregator; _hostingEnvironment = hostingEnvironment; + _databaseBuilder = databaseBuilder; + _umbracoVersion = umbracoVersion; _logger = _loggerFactory.CreateLogger(); } @@ -86,7 +95,7 @@ namespace Umbraco.Cms.Infrastructure.Runtime if (State.Level <= RuntimeLevel.BootFailed) { - throw new InvalidOperationException($"Cannot start the runtime if the runtime level is less than or equal to {RuntimeLevel.BootFailed}"); + return; // The exception will be rethrown by BootFailedMiddelware } IApplicationShutdownRegistry hostingEnvironmentLifetime = _applicationShutdownRegistry; @@ -98,12 +107,35 @@ namespace Umbraco.Cms.Infrastructure.Runtime // acquire the main domain - if this fails then anything that should be registered with MainDom will not operate AcquireMainDom(); + // if level is Run and reason is UpgradeMigrations, that means we need to perform an unattended upgrade + if (State.Reason == RuntimeLevelReason.UpgradeMigrations && State.Level == RuntimeLevel.Run) + { + // do the upgrade + DoUnattendedUpgrade(); + + // upgrade is done, set reason to Run + DetermineRuntimeLevel(); + + } + await _eventAggregator.PublishAsync(new UmbracoApplicationStarting(State.Level), cancellationToken); // 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(); @@ -148,11 +180,12 @@ namespace Umbraco.Cms.Infrastructure.Runtime _databaseFactory.ConfigureForUpgrade(); } } - catch + catch (Exception ex) { State.Configure(RuntimeLevel.BootFailed, RuntimeLevelReason.BootFailedOnException); timer?.Fail(); - throw; + _logger.LogError(ex, "Boot Failed"); + // We do not throw the exception. It will be rethrown by BootFailedMiddleware } } } diff --git a/src/Umbraco.Infrastructure/Runtime/SqlMainDomLock.cs b/src/Umbraco.Infrastructure/Runtime/SqlMainDomLock.cs index 63828715fd..737f3550f6 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 fc8f5f3912..02d4375186 100644 --- a/src/Umbraco.Infrastructure/RuntimeState.cs +++ b/src/Umbraco.Infrastructure/RuntimeState.cs @@ -30,6 +30,7 @@ namespace Umbraco.Cms.Core /// /// The initial + /// The initial /// public static RuntimeState Booting() => new RuntimeState() { Level = RuntimeLevel.Boot }; @@ -115,7 +116,8 @@ namespace Umbraco.Cms.Core // else it is bad enough that we want to throw Reason = RuntimeLevelReason.BootFailedCannotConnectToDatabase; - throw new BootFailedException("A connection string is configured but Umbraco could not connect to the database."); + BootFailedException =new BootFailedException("A connection string is configured but Umbraco could not connect to the database."); + throw BootFailedException; } case UmbracoDatabaseState.NotInstalled: { @@ -132,7 +134,7 @@ namespace Umbraco.Cms.Core // although the files version matches the code version, the database version does not // which means the local files have been upgraded but not the database - need to upgrade _logger.LogDebug("Has not reached the final upgrade step, need to upgrade Umbraco."); - Level = RuntimeLevel.Upgrade; + Level = _unattendedSettings.Value.UpgradeUnattended ? RuntimeLevel.Run : RuntimeLevel.Upgrade; Reason = RuntimeLevelReason.UpgradeMigrations; } break; @@ -191,7 +193,8 @@ namespace Umbraco.Cms.Core // else it is bad enough that we want to throw Reason = RuntimeLevelReason.BootFailedCannotCheckUpgradeState; - throw new BootFailedException("Could not check the upgrade state.", e); + BootFailedException = new BootFailedException("Could not check the upgrade state.", e); + throw BootFailedException; } } @@ -250,9 +253,12 @@ namespace Umbraco.Cms.Core _logger.LogInformation(ex, "Error during unattended install."); database.AbortTransaction(); - throw new UnattendedInstallException( + var innerException = new UnattendedInstallException( "The database configuration failed with the following message: " + ex.Message + "\n Please check log file for additional information (can be found in '/App_Data/Logs/')"); + BootFailedException = new BootFailedException(innerException.Message, innerException); + + throw BootFailedException; } } } 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 603a83c197..66a4470645 100644 --- a/src/Umbraco.Infrastructure/Scoping/Scope.cs +++ b/src/Umbraco.Infrastructure/Scoping/Scope.cs @@ -621,7 +621,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 4535bebd8b..30dc01dc9a 100644 --- a/src/Umbraco.Infrastructure/Search/ExamineComponent.cs +++ b/src/Umbraco.Infrastructure/Search/ExamineComponent.cs @@ -276,10 +276,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: @@ -693,6 +703,7 @@ namespace Umbraco.Cms.Infrastructure.Search List valueSet = builders[index.PublishedValuesOnly].Value; index.IndexItems(valueSet); } + return Task.CompletedTask; }); } 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..1a37376070 --- /dev/null +++ b/src/Umbraco.Infrastructure/Security/ClaimsIdentityExtensions.cs @@ -0,0 +1,36 @@ +// 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 +{ + public 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[] s_ignoredClaims = new[] { ClaimTypes.CookiePath, Constants.Security.SessionIdClaimType }; + + public static void MergeClaimsFromBackOfficeIdentity(this ClaimsIdentity destination, ClaimsIdentity source) + { + foreach (Claim claim in source.Claims + .Where(claim => !s_ignoredClaims.Contains(claim.Type)) + .Where(claim => !destination.HasClaim(claim.Type, claim.Value))) + { + destination.AddClaim(new Claim(claim.Type, claim.Value)); + } + } + + public static void MergeClaimsFromBackOfficeIdentity(this ClaimsIdentity destination, BackOfficeIdentityUser source) + { + foreach (Microsoft.AspNetCore.Identity.IdentityUserClaim claim in source.Claims + .Where(claim => !s_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 50c4d1e505..0cf724ec20 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.Infrastructure/Umbraco.Infrastructure.csproj b/src/Umbraco.Infrastructure/Umbraco.Infrastructure.csproj index 23c1af2c5d..99642069f3 100644 --- a/src/Umbraco.Infrastructure/Umbraco.Infrastructure.csproj +++ b/src/Umbraco.Infrastructure/Umbraco.Infrastructure.csproj @@ -3,6 +3,9 @@ netstandard2.0 Umbraco.Cms.Infrastructure + Umbraco.Cms.Infrastructure + Umbraco CMS Infrastructure + Contains the infrastructure assembly needed to run Umbraco Cms. This package only contains the assembly, and can be used for package development. Use the template in the Umbraco.Templates package to setup Umbraco 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.PublishedCache.NuCache/Umbraco.PublishedCache.NuCache.csproj b/src/Umbraco.PublishedCache.NuCache/Umbraco.PublishedCache.NuCache.csproj index fe3ce47de4..9aea12912c 100644 --- a/src/Umbraco.PublishedCache.NuCache/Umbraco.PublishedCache.NuCache.csproj +++ b/src/Umbraco.PublishedCache.NuCache/Umbraco.PublishedCache.NuCache.csproj @@ -3,6 +3,10 @@ netstandard2.0 Umbraco.Cms.Infrastructure.PublishedCache + 8 + Umbraco.Cms.PublishedCache.NuCache + Umbraco CMS Published Cache + Contains the Published Cache assembly needed to run Umbraco Cms. This package only contains the assembly, and can be used for package development. Use the template in the Umbraco.Templates package to setup Umbraco 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.Common/Umbraco.Tests.Common.csproj b/src/Umbraco.Tests.Common/Umbraco.Tests.Common.csproj index 1040df225d..a13d6b763b 100644 --- a/src/Umbraco.Tests.Common/Umbraco.Tests.Common.csproj +++ b/src/Umbraco.Tests.Common/Umbraco.Tests.Common.csproj @@ -3,6 +3,9 @@ netstandard2.0 Umbraco.Cms.Tests.Common + Umbraco.Cms.Tests + Umbraco CMS Test Tools + Contains commonly used tools to write tests for Umbraco CMS, such as various builders for content etc. 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 e42e681a58..b68bb267cd 100644 --- a/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/LocksTests.cs +++ b/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/LocksTests.cs @@ -1,16 +1,16 @@ -using System; +using System; using System.Collections.Generic; 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.Infrastructure.Persistence.Dtos; using Umbraco.Cms.Tests.Common.Testing; using Umbraco.Cms.Tests.Integration.Testing; -using Constants = Umbraco.Cms.Core.Constants; namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence { @@ -322,6 +322,147 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence Assert.IsNull(e2); } + [Test] + public void Throws_When_Lock_Timeout_Is_Exceeded() + { + using (ExecutionContext.SuppressFlow()) + { + + + var t1 = Task.Run(() => + { + using (var scope = ScopeProvider.CreateScope()) + { + + Console.WriteLine("Write lock A"); + // This will acquire right away + scope.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()) + { + 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(() => + scope.ReadLock(TimeSpan.FromMilliseconds(3000), Constants.Locks.ContentTree)); + scope.Complete(); + Console.WriteLine("Finished Read lock B"); + } + }); + + var t3 = Task.Run(() => + { + using (var scope = ScopeProvider.CreateScope()) + { + 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(() => + scope.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; + + using (ExecutionContext.SuppressFlow()) + { + var t1 = Task.Run(() => + { + using (var scope = ScopeProvider.CreateScope()) + { + Console.WriteLine("Write lock A"); + // This will acquire right away + scope.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()) + { + Console.WriteLine("Read lock B"); + + // This will wait for the write lock to release + Assert.DoesNotThrow(() => + scope.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()) + { + Console.WriteLine("Read lock C"); + + // This will wait for the write lock to release + Assert.DoesNotThrow(() => + scope.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] + public void Lock_Exceeds_Command_Timeout() + { + using (var scope = ScopeProvider.CreateScope()) + { + var realDb = (Database)scope.Database; + realDb.CommandTimeout = 1000; + + Console.WriteLine("Write lock A"); + // TODO: In theory this would throw + scope.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/SqlServerTableByTableTest.cs b/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/SqlServerTableByTableTest.cs index a44d7ca252..55a348aad7 100644 --- a/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/SqlServerTableByTableTest.cs +++ b/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/SqlServerTableByTableTest.cs @@ -220,6 +220,19 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence } } + [Test] + public void Can_Create_umbracoLogViewerQuery_Table() + { + using (var scope = ScopeProvider.CreateScope()) + { + var helper = new DatabaseSchemaCreator(scope.Database, _loggerFactory.CreateLogger(), _loggerFactory, UmbracoVersion); + + helper.CreateTable(); + + scope.Complete(); + } + } + [Test] public void Can_Create_umbracoLanguage_Table() { 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/Assets/logviewer.searches.config.js b/src/Umbraco.Tests.UnitTests/TestHelpers/Assets/logviewer.searches.config.js deleted file mode 100644 index 25ee9b2242..0000000000 --- a/src/Umbraco.Tests.UnitTests/TestHelpers/Assets/logviewer.searches.config.js +++ /dev/null @@ -1,42 +0,0 @@ -[ - { - "name": "Find all logs where the Level is NOT Verbose and NOT Debug", - "query": "Not(@Level='Verbose') and Not(@Level='Debug')" - }, - { - "name": "Find all logs that has an exception property (Warning, Error & Critical with Exceptions)", - "query": "Has(@Exception)" - }, - { - "name": "Find all logs that have the property 'Duration'", - "query": "Has(Duration)" - }, - { - "name": "Find all logs that have the property 'Duration' and the duration is greater than 1000ms", - "query": "Has(Duration) and Duration > 1000" - }, - { - "name": "Find all logs that are from the namespace 'Umbraco.Core'", - "query": "StartsWith(SourceContext, 'Umbraco.Core')" - }, - { - "name": "Find all logs that use a specific log message template", - "query": "@MessageTemplate = '[Timing {TimingId}] {EndMessage} ({TimingDuration}ms)'" - }, - { - "name": "Find logs where one of the items in the SortedComponentTypes property array is equal to", - "query": "SortedComponentTypes[?] = 'Umbraco.Web.Search.ExamineComponent'" - }, - { - "name": "Find logs where one of the items in the SortedComponentTypes property array contains", - "query": "Contains(SortedComponentTypes[?], 'DatabaseServer')" - }, - { - "name": "Find all logs that the message has localhost in it with SQL like", - "query": "@Message like '%localhost%'" - }, - { - "name": "Find all logs that the message that starts with 'end' in it with SQL like", - "query": "@Message like 'end%'" - } -] 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/Models/UserGroupTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Core/Models/UserGroupTests.cs new file mode 100644 index 0000000000..eccd20c182 --- /dev/null +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Core/Models/UserGroupTests.cs @@ -0,0 +1,65 @@ +// Copyright (c) Umbraco. +// See LICENSE for more details. + +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Reflection; +using Newtonsoft.Json; +using NUnit.Framework; +using Umbraco.Cms.Core.Models.Membership; +using Umbraco.Cms.Tests.Common.Builders; +using Umbraco.Cms.Tests.Common.Builders.Extensions; + +namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.Models +{ + [TestFixture] + public class UserGroupTests + { + private UserGroupBuilder _builder; + + [SetUp] + public void SetUp() => _builder = new UserGroupBuilder(); + + [Test] + public void Can_Deep_Clone() + { + IUserGroup item = Build(); + + var clone = (IUserGroup)item.DeepClone(); + + var x = clone.Equals(item); + Assert.AreNotSame(clone, item); + Assert.AreEqual(clone, item); + + Assert.AreEqual(clone.AllowedSections.Count(), item.AllowedSections.Count()); + Assert.AreNotSame(clone.AllowedSections, item.AllowedSections); + + // Verify normal properties with reflection + PropertyInfo[] allProps = clone.GetType().GetProperties(); + foreach (PropertyInfo propertyInfo in allProps) + { + Assert.AreEqual(propertyInfo.GetValue(clone, null), propertyInfo.GetValue(item, null)); + } + } + + [Test] + public void Can_Serialize_Without_Error() + { + IUserGroup item = Build(); + + var json = JsonConvert.SerializeObject(item); + Debug.Print(json); + } + + private IUserGroup Build() => + _builder + .WithId(3) + .WithAllowedSections(new List(){"A", "B"}) + .Build(); + + + } + + +} 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..94064ddaba 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,13 +101,44 @@ 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" }); Assert.IsTrue(result.Success); } + [Test] + [TestCase(Constants.Security.AdminGroupAlias, Constants.Security.AdminGroupAlias, ExpectedResult = true)] + [TestCase(Constants.Security.AdminGroupAlias, "SomethingElse", ExpectedResult = true)] + [TestCase(Constants.Security.EditorGroupAlias, Constants.Security.AdminGroupAlias, ExpectedResult = false)] + [TestCase(Constants.Security.EditorGroupAlias, "SomethingElse", ExpectedResult = false)] + [TestCase(Constants.Security.EditorGroupAlias, Constants.Security.EditorGroupAlias, ExpectedResult = true)] + public bool Can_only_add_user_groups_you_are_part_of_yourself_unless_you_are_admin(string groupAlias, string groupToAdd) + { + var currentUser = Mock.Of(user => user.Groups == new[] + { + new ReadOnlyUserGroup(1, "CurrentUser", "icon-user", null, null, groupAlias, new string[0], new string[0]) + }); + IUser savingUser = null; // This means it is a new created user + + var contentService = new Mock(); + var mediaService = new Mock(); + var userService = new Mock(); + var entityService = new Mock(); + + var authHelper = new UserEditorAuthorizationHelper( + contentService.Object, + mediaService.Object, + entityService.Object, + AppCaches.Disabled); + + var result = authHelper.IsAuthorized(currentUser, savingUser, new int[0], new int[0], new[] { groupToAdd }); + + return result.Success; + } + [Test] public void Can_Add_Another_Content_Start_Node_On_User_With_Access() { @@ -130,7 +165,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 +200,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 +235,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 +270,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 +305,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 +340,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 +375,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 +410,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/Logging/LogviewerTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Logging/LogviewerTests.cs index a957f3611e..9ba522fbc9 100644 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Logging/LogviewerTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Logging/LogviewerTests.cs @@ -16,6 +16,11 @@ using Umbraco.Cms.Core.IO; using Umbraco.Cms.Core.Logging; using Umbraco.Cms.Core.Logging.Viewer; using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Persistence.Querying; +using Umbraco.Cms.Core.Persistence.Repositories; +using Umbraco.Cms.Core.Scoping; +using Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_9_0_0; +using Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; using Umbraco.Cms.Tests.UnitTests.TestHelpers; using File = System.IO.File; @@ -27,18 +32,16 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Logging private ILogViewer _logViewer; private const string LogfileName = "UmbracoTraceLog.UNITTEST.20181112.json"; - private const string SearchfileName = "logviewer.searches.config.js"; private string _newLogfilePath; private string _newLogfileDirPath; - private string _newSearchfilePath; - private string _newSearchfileDirPath; - private readonly LogTimePeriod _logTimePeriod = new LogTimePeriod( new DateTime(year: 2018, month: 11, day: 12, hour: 0, minute: 0, second: 0), new DateTime(year: 2018, month: 11, day: 13, hour: 0, minute: 0, second: 0)); + private ILogViewerQueryRepository LogViewerQueryRepository { get; } = new TestLogViewerQueryRepository(); + [OneTimeSetUp] public void Setup() { @@ -55,20 +58,14 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Logging _newLogfileDirPath = loggingConfiguration.LogDirectory; _newLogfilePath = Path.Combine(_newLogfileDirPath, LogfileName); - var exampleSearchfilePath = Path.Combine(testRoot, "TestHelpers", "Assets", SearchfileName); - _newSearchfileDirPath = Path.Combine(hostingEnv.ApplicationPhysicalPath, @"config"); - _newSearchfilePath = Path.Combine(_newSearchfileDirPath, SearchfileName); - // Create/ensure Directory exists ioHelper.EnsurePathExists(_newLogfileDirPath); - ioHelper.EnsurePathExists(_newSearchfileDirPath); // Copy the sample files File.Copy(exampleLogfilePath, _newLogfilePath, true); - File.Copy(exampleSearchfilePath, _newSearchfilePath, true); ILogger logger = Mock.Of>(); - var logViewerConfig = new LogViewerConfig(hostingEnv); + var logViewerConfig = new LogViewerConfig(LogViewerQueryRepository, Mock.Of()); _logViewer = new SerilogJsonLogViewer(logger, logViewerConfig, loggingConfiguration, Log.Logger); } @@ -81,11 +78,6 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Logging { File.Delete(_newLogfilePath); } - - if (File.Exists(_newSearchfilePath)) - { - File.Delete(_newSearchfilePath); - } } [Test] @@ -238,4 +230,55 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Logging Assert.IsEmpty(findItem, "The search item should no longer exist"); } } + + internal class TestLogViewerQueryRepository : ILogViewerQueryRepository + { + public TestLogViewerQueryRepository() + { + Store = new List(MigrateLogViewerQueriesFromFileToDb.DefaultLogQueries + .Select(LogViewerQueryModelFactory.BuildEntity)); + } + + private IList Store { get; } + private LogViewerQueryRepository.LogViewerQueryModelFactory LogViewerQueryModelFactory { get; } = new LogViewerQueryRepository.LogViewerQueryModelFactory(); + + + public ILogViewerQuery Get(int id) => Store.FirstOrDefault(x => x.Id == id); + + public IEnumerable GetMany(params int[] ids) => + ids.Any() ? Store.Where(x => ids.Contains(x.Id)) : Store; + + public bool Exists(int id) => Get(id) is not null; + + public void Save(ILogViewerQuery entity) + { + var item = Get(entity.Id); + + if (item is null) + { + Store.Add(entity); + } + else + { + item.Name = entity.Name; + item.Query = entity.Query; + } + } + + public void Delete(ILogViewerQuery entity) + { + var item = Get(entity.Id); + + if (item is not null) + { + Store.Remove(item); + } + } + + public IEnumerable Get(IQuery query) => throw new NotImplementedException(); + + public int Count(IQuery query) => throw new NotImplementedException(); + + public ILogViewerQuery GetByName(string name) => Store.FirstOrDefault(x => x.Name == name); + } } 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.Tests.UnitTests.csproj b/src/Umbraco.Tests.UnitTests/Umbraco.Tests.UnitTests.csproj index 0839047be4..1a7788591d 100644 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Tests.UnitTests.csproj +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Tests.UnitTests.csproj @@ -26,7 +26,6 @@ - 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 fa94cae4fb..6150bf43e1 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 74ae7604aa..1bce8edcac 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.31 @@ -113,12 +114,13 @@ - + + @@ -126,9 +128,7 @@ - - @@ -157,6 +157,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..3b6116cc6c 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/TourController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/TourController.cs @@ -2,8 +2,11 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using System.Text; +using System.Threading.Tasks; 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 +14,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 { @@ -39,7 +41,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers _contentTypeService = contentTypeService; } - public IEnumerable GetTours() + public async Task> GetTours() { var result = new List(); @@ -57,22 +59,24 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers var nonPluginFilters = _filters.Where(x => x.PluginName == null).ToList(); //add core tour files - var coreToursPath = Path.Combine(_hostingEnvironment.MapPathContentRoot(Constants.SystemDirectories.Config), "BackOfficeTours"); - if (Directory.Exists(coreToursPath)) + var embeddedTourNames = GetType() + .Assembly + .GetManifestResourceNames() + .Where(x => x.StartsWith("Umbraco.Cms.Web.BackOffice.EmbeddedResources.Tours.")); + + foreach (var embeddedTourName in embeddedTourNames) { - foreach (var tourFile in Directory.EnumerateFiles(coreToursPath, "*.json")) - { - TryParseTourFile(tourFile, result, nonPluginFilters, aliasOnlyFilters); - } + await TryParseTourFile(embeddedTourName, result, nonPluginFilters, aliasOnlyFilters, async x=> await GetContentFromEmbeddedResource(x)); } + //collect all tour files in packages var appPlugins = _hostingEnvironment.MapPathContentRoot(Constants.SystemDirectories.AppPlugins); if (Directory.Exists(appPlugins)) { 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(); @@ -89,7 +93,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers { foreach (var tourFile in Directory.EnumerateFiles(tourDir, "*.json")) { - TryParseTourFile(tourFile, result, combinedFilters, aliasOnlyFilters, pluginName); + await TryParseTourFile(tourFile, result, combinedFilters, aliasOnlyFilters, async x => await System.IO.File.ReadAllTextAsync(x), pluginName); } } } @@ -126,14 +130,22 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers return result.Except(toursToBeRemoved).OrderBy(x => x.FileName, StringComparer.InvariantCultureIgnoreCase); } + private async Task GetContentFromEmbeddedResource(string fileName) + { + var resourceStream = GetType().Assembly.GetManifestResourceStream(fileName); + + using var reader = new StreamReader(resourceStream, Encoding.UTF8); + return await reader.ReadToEndAsync(); + } + /// /// Gets a tours for a specific doctype /// /// The documenttype alias /// A - public IEnumerable GetToursForDoctype(string doctypeAlias) + public async Task> GetToursForDoctype(string doctypeAlias) { - var tourFiles = this.GetTours(); + var tourFiles = await this.GetTours(); var doctypeAliasWithCompositions = new List { @@ -154,15 +166,16 @@ 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(); }); } - private void TryParseTourFile(string tourFile, + private async Task TryParseTourFile(string tourFile, ICollection result, List filters, List aliasOnlyFilters, + Func> fileNameToFileContent, string pluginName = null) { var fileName = Path.GetFileNameWithoutExtension(tourFile); @@ -182,7 +195,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers try { - var contents = System.IO.File.ReadAllText(tourFile); + var contents = await fileNameToFileContent(tourFile); var tours = JsonConvert.DeserializeObject(contents); var backOfficeTours = tours.Where(x => diff --git a/src/Umbraco.Web.BackOffice/Controllers/UserGroupEditorAuthorizationHelper.cs b/src/Umbraco.Web.BackOffice/Controllers/UserGroupEditorAuthorizationHelper.cs index 4b759cfaa2..ea74b47e87 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; } /// @@ -75,18 +78,15 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers /// /// Authorize that the user is not adding a section to the group that they don't have access to /// - /// - /// - /// - /// - public Attempt AuthorizeSectionChanges(IUser currentUser, - IEnumerable currentAllowedSections, + public Attempt AuthorizeSectionChanges( + IUser currentUser, + IEnumerable existingSections, IEnumerable proposedAllowedSections) { if (currentUser.IsAdmin()) return Attempt.Succeed(); - var sectionsAdded = currentAllowedSections.Except(proposedAllowedSections).ToArray(); + var sectionsAdded = proposedAllowedSections.Except(existingSections).ToArray(); var sectionAccessMissing = sectionsAdded.Except(currentUser.AllowedSections).ToArray(); return sectionAccessMissing.Length > 0 ? Attempt.Fail("Current user doesn't have access to add these sections " + string.Join(", ", sectionAccessMissing)) @@ -113,7 +113,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 +123,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..b663eff6c7 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,18 @@ 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) + public UserGroupsController( + IUserService userService, + IContentService contentService, + IEntityService entityService, + IMediaService mediaService, + IBackOfficeSecurityAccessor backofficeSecurityAccessor, + UmbracoMapper umbracoMapper, + ILocalizedTextService localizedTextService, + IShortStringHelper shortStringHelper, + AppCaches appCaches) { _userService = userService ?? throw new ArgumentNullException(nameof(userService)); _contentService = contentService ?? throw new ArgumentNullException(nameof(contentService)); @@ -46,6 +54,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,14 +64,15 @@ 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) return Unauthorized(isAuthorized.Result); //if sections were added we need to check that the current user has access to that section - isAuthorized = authHelper.AuthorizeSectionChanges(_backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser, + isAuthorized = authHelper.AuthorizeSectionChanges( + _backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser, userGroupSave.PersistedUserGroup.AllowedSections, userGroupSave.Sections); if (isAuthorized == false) @@ -78,7 +88,10 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers return Unauthorized(isAuthorized.Result); //need to ensure current user is in a group if not an admin to avoid a 401 - EnsureNonAdminUserIsInSavedUserGroup(userGroupSave); + EnsureNonAdminUserIsInSavedUserGroup(userGroupSave); + + //map the model to the persisted instance + _umbracoMapper.Map(userGroupSave, userGroupSave.PersistedUserGroup); //save the group _userService.Save(userGroupSave.PersistedUserGroup, userGroupSave.Users.ToArray()); diff --git a/src/Umbraco.Web.BackOffice/DependencyInjection/ServiceCollectionExtensions.cs b/src/Umbraco.Web.BackOffice/DependencyInjection/ServiceCollectionExtensions.cs index 5ebada2c98..3218897eee 100644 --- a/src/Umbraco.Web.BackOffice/DependencyInjection/ServiceCollectionExtensions.cs +++ b/src/Umbraco.Web.BackOffice/DependencyInjection/ServiceCollectionExtensions.cs @@ -2,6 +2,7 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; +using Umbraco.Cms.Core; using Umbraco.Cms.Core.Actions; using Umbraco.Cms.Core.Net; using Umbraco.Cms.Core.Security; @@ -11,7 +12,6 @@ using Umbraco.Cms.Web.BackOffice.Security; using Umbraco.Cms.Web.Common.AspNetCore; using Umbraco.Cms.Web.Common.Authorization; using Umbraco.Cms.Web.Common.Security; -using Constants = Umbraco.Cms.Core.Constants; namespace Umbraco.Extensions { @@ -365,16 +365,10 @@ namespace Umbraco.Extensions policy.Requirements.Add(new TreeRequirement(Constants.Trees.Languages)); }); - options.AddPolicy(AuthorizationPolicies.TreeAccessDocumentTypes, policy => - { - policy.AuthenticationSchemes.Add(backOfficeAuthenticationScheme); - policy.Requirements.Add(new TreeRequirement(Constants.Trees.Dictionary)); - }); - options.AddPolicy(AuthorizationPolicies.TreeAccessDictionary, policy => { policy.AuthenticationSchemes.Add(backOfficeAuthenticationScheme); - policy.Requirements.Add(new TreeRequirement(Constants.Trees.Dictionary, Constants.Trees.Dictionary)); + policy.Requirements.Add(new TreeRequirement(Constants.Trees.Dictionary)); }); options.AddPolicy(AuthorizationPolicies.TreeAccessDictionaryOrTemplates, policy => diff --git a/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilderExtensions.cs b/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilderExtensions.cs index c11a13e1a1..b3fce3fa8c 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; @@ -83,7 +84,6 @@ namespace Umbraco.Extensions builder.Services.ConfigureOptions(); - builder.Services.AddUnique(); builder.Services.AddUnique(); builder.Services.AddUnique(); builder.Services.AddUnique, PasswordChanger>(); @@ -185,6 +185,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.UI.NetCore/config/BackOfficeTours/getting-started.json b/src/Umbraco.Web.BackOffice/EmbeddedResources/Tours/getting-started.json similarity index 100% rename from src/Umbraco.Web.UI.NetCore/config/BackOfficeTours/getting-started.json rename to src/Umbraco.Web.BackOffice/EmbeddedResources/Tours/getting-started.json diff --git a/src/Umbraco.Web.BackOffice/Extensions/BackOfficeApplicationBuilderExtensions.cs b/src/Umbraco.Web.BackOffice/Extensions/BackOfficeApplicationBuilderExtensions.cs index d63deda88a..35f41e0af7 100644 --- a/src/Umbraco.Web.BackOffice/Extensions/BackOfficeApplicationBuilderExtensions.cs +++ b/src/Umbraco.Web.BackOffice/Extensions/BackOfficeApplicationBuilderExtensions.cs @@ -44,9 +44,6 @@ namespace Umbraco.Extensions public static IApplicationBuilder UseUmbracoPreview(this IApplicationBuilder app) { - // TODO: I'm unsure this middleware will execute before the endpoint, we'll have to see - app.UseMiddleware(); - app.UseEndpoints(endpoints => { PreviewRoutes previewRoutes = app.ApplicationServices.GetRequiredService(); diff --git a/src/Umbraco.Web.BackOffice/Filters/CheckIfUserTicketDataIsStaleAttribute.cs b/src/Umbraco.Web.BackOffice/Filters/CheckIfUserTicketDataIsStaleAttribute.cs index f2ef6b6807..d8f3374b5f 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; } @@ -141,12 +144,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/Filters/UserGroupValidateAttribute.cs b/src/Umbraco.Web.BackOffice/Filters/UserGroupValidateAttribute.cs index 1bcfbfe0f9..7edf790fb3 100644 --- a/src/Umbraco.Web.BackOffice/Filters/UserGroupValidateAttribute.cs +++ b/src/Umbraco.Web.BackOffice/Filters/UserGroupValidateAttribute.cs @@ -6,6 +6,7 @@ using Umbraco.Cms.Core.Mapping; using Umbraco.Cms.Core.Models.ContentEditing; using Umbraco.Cms.Core.Models.Membership; using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Strings; using Umbraco.Cms.Web.BackOffice.ActionResults; using Umbraco.Cms.Web.Common.ActionsResults; using Umbraco.Extensions; @@ -20,15 +21,15 @@ namespace Umbraco.Cms.Web.BackOffice.Filters private class UserGroupValidateFilter : IActionFilter { - private readonly UmbracoMapper _umbracoMapper; + private readonly IShortStringHelper _shortStringHelper; private readonly IUserService _userService; public UserGroupValidateFilter( IUserService userService, - UmbracoMapper umbracoMapper) + IShortStringHelper shortStringHelper) { _userService = userService ?? throw new ArgumentNullException(nameof(userService)); - _umbracoMapper = umbracoMapper ?? throw new ArgumentNullException(nameof(umbracoMapper)); + _shortStringHelper = shortStringHelper ?? throw new ArgumentNullException(nameof(shortStringHelper)); } public void OnActionExecuting(ActionExecutingContext context) @@ -58,13 +59,9 @@ namespace Umbraco.Cms.Web.BackOffice.Filters return; } - //map the model to the persisted instance - _umbracoMapper.Map(userGroupSave, persisted); break; case ContentSaveAction.SaveNew: - //create the persisted model from mapping the saved model - persisted = _umbracoMapper.Map(userGroupSave); - ((UserGroup) persisted).ResetIdentity(); + persisted = new UserGroup(_shortStringHelper); break; default: context.Result = 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/PropertyEditors/RichTextPreValueController.cs b/src/Umbraco.Web.BackOffice/PropertyEditors/RichTextPreValueController.cs index faa4cc83dc..3d0d746d5a 100644 --- a/src/Umbraco.Web.BackOffice/PropertyEditors/RichTextPreValueController.cs +++ b/src/Umbraco.Web.BackOffice/PropertyEditors/RichTextPreValueController.cs @@ -1,13 +1,10 @@ -using System.Collections.Generic; -using System.Xml; +using System.Linq; +using Microsoft.Extensions.Options; using Umbraco.Cms.Core; -using Umbraco.Cms.Core.Hosting; -using Umbraco.Cms.Core.IO; +using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Models.ContentEditing; using Umbraco.Cms.Web.BackOffice.Controllers; using Umbraco.Cms.Web.Common.Attributes; -using Umbraco.Extensions; -using Constants = Umbraco.Cms.Core.Constants; namespace Umbraco.Cms.Web.BackOffice.PropertyEditors { @@ -17,115 +14,35 @@ namespace Umbraco.Cms.Web.BackOffice.PropertyEditors [PluginController(Constants.Web.Mvc.BackOfficeApiArea)] public class RichTextPreValueController : UmbracoAuthorizedJsonController { - private readonly IHostingEnvironment _hostingEnvironment; + private readonly IOptions _richTextEditorSettings; - public RichTextPreValueController(IHostingEnvironment hostingEnvironment) + public RichTextPreValueController(IOptions richTextEditorSettings) { - _hostingEnvironment = hostingEnvironment; + _richTextEditorSettings = richTextEditorSettings; } - private static volatile bool _init; - private static readonly object Locker = new object(); - private static readonly Dictionary Commands = new Dictionary(); - private static readonly Dictionary Plugins = new Dictionary(); - private static readonly Dictionary ConfigOptions = new Dictionary(); - - private static string _invalidElements = ""; - private static string _validElements = ""; - public RichTextEditorConfiguration GetConfiguration() { - EnsureInit(); + var settings = _richTextEditorSettings.Value; var config = new RichTextEditorConfiguration { - Plugins = Plugins.Values, - Commands = Commands.Values, - ValidElements = _validElements, - InvalidElements = _invalidElements, - CustomConfig = ConfigOptions + Plugins = settings.Plugins.Select(x=>new RichTextEditorPlugin() + { + Name = x + }), + Commands = settings.Commands.Select(x=>new RichTextEditorCommand() + { + Alias = x.Alias, + Mode = x.Mode, + Name = x.Name + }), + ValidElements = settings.ValidElements, + InvalidElements = settings.InvalidElements, + CustomConfig = settings.CustomConfig }; return config; } - - private void EnsureInit() - { - - if (_init == false) - { - lock (Locker) - { - if (_init == false) - { - // Load config - XmlDocument xd = new XmlDocument(); - xd.Load(_hostingEnvironment.MapPathContentRoot(SystemFiles.TinyMceConfig)); - - foreach (XmlNode n in xd.DocumentElement.SelectNodes("//command")) - { - var alias = n.AttributeValue("alias").ToLower(); - - if (!Commands.ContainsKey(alias)) - Commands.Add( - alias, - new RichTextEditorCommand() - { - Name = n.AttributeValue("name") ?? alias, - Alias = alias, - Mode = Enum.Parse(n.AttributeValue("mode"), true) - } - ); - } - - - foreach (XmlNode n in xd.DocumentElement.SelectNodes("//plugin")) - { - if (!Plugins.ContainsKey(n.FirstChild.Value)) - { - - Plugins.Add( - n.FirstChild.Value.ToLower(), - new RichTextEditorPlugin() - { - Name = n.FirstChild.Value, - }); - } - } - - - foreach (XmlNode n in xd.DocumentElement.SelectNodes("//config")) - { - if (!ConfigOptions.ContainsKey(n.Attributes["key"].FirstChild.Value)) - { - var value = ""; - if (n.FirstChild != null) - value = n.FirstChild.Value; - - ConfigOptions.Add( - n.Attributes["key"].FirstChild.Value.ToLower(), - value); - } - } - - if (xd.DocumentElement.SelectSingleNode("./invalidElements") != null) - _invalidElements = xd.DocumentElement.SelectSingleNode("./invalidElements").FirstChild.Value; - if (xd.DocumentElement.SelectSingleNode("./validElements") != null) - { - string _val = xd.DocumentElement.SelectSingleNode("./validElements").FirstChild.Value.Replace("\r", "").Replace("\n", ""); - _validElements = _val; - - /*foreach (string s in _val.Split("\n".ToCharArray())) - _validElements += "'" + s + "' + \n"; - _validElements = _validElements.Substring(0, _validElements.Length - 4);*/ - } - - _init = true; - } - } - } - - } - } } 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/BackOfficeSignInManager.cs b/src/Umbraco.Web.BackOffice/Security/BackOfficeSignInManager.cs index d9ebdfaae6..5219a7bfb0 100644 --- a/src/Umbraco.Web.BackOffice/Security/BackOfficeSignInManager.cs +++ b/src/Umbraco.Web.BackOffice/Security/BackOfficeSignInManager.cs @@ -24,7 +24,7 @@ namespace Umbraco.Cms.Web.BackOffice.Security // borrowed from https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Core/src/SignInManager.cs private const string UmbracoSignInMgrXsrfKey = "XsrfId"; - private BackOfficeUserManager _userManager; + private readonly BackOfficeUserManager _userManager; private readonly IBackOfficeExternalLoginProviders _externalLogins; private readonly GlobalSettings _globalSettings; 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..755b89911c 100644 --- a/src/Umbraco.Web.BackOffice/Security/ConfigureBackOfficeSecurityStampValidatorOptions.cs +++ b/src/Umbraco.Web.BackOffice/Security/ConfigureBackOfficeSecurityStampValidatorOptions.cs @@ -1,5 +1,9 @@ -using System; +using System; +using System.Linq; +using System.Security.Claims; +using System.Threading.Tasks; using Microsoft.Extensions.Options; +using Umbraco.Extensions; namespace Umbraco.Cms.Web.BackOffice.Security { @@ -11,6 +15,19 @@ namespace Umbraco.Cms.Web.BackOffice.Security public void Configure(BackOfficeSecurityStampValidatorOptions options) { options.ValidationInterval = TimeSpan.FromMinutes(30); + + // When refreshing the principal, ensure custom claims that + // might have been set with an external identity continue + // to flow through to this new one. + options.OnRefreshingPrincipal = refreshingPrincipal => + { + ClaimsIdentity newIdentity = refreshingPrincipal.NewPrincipal.Identities.First(); + ClaimsIdentity currentIdentity = refreshingPrincipal.CurrentPrincipal.Identities.First(); + + newIdentity.MergeClaimsFromBackOfficeIdentity(currentIdentity); + + return Task.CompletedTask; + }; } } 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.BackOffice/Umbraco.Web.BackOffice.csproj b/src/Umbraco.Web.BackOffice/Umbraco.Web.BackOffice.csproj index 22799eaa63..d49eb6e4f5 100644 --- a/src/Umbraco.Web.BackOffice/Umbraco.Web.BackOffice.csproj +++ b/src/Umbraco.Web.BackOffice/Umbraco.Web.BackOffice.csproj @@ -4,6 +4,9 @@ net5.0 Library Umbraco.Cms.Web.BackOffice + Umbraco.Cms.Web.BackOffice + Umbraco CMS Back Office + Contains the Back Office assembly needed to run the back office of Umbraco Cms. This package only contains the assembly, and can be used for package development. Use the template in the Umbraco.Templates package to setup Umbraco @@ -36,4 +39,8 @@ + + + + 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/StaticServiceProvider.cs b/src/Umbraco.Web.Common/DependencyInjection/StaticServiceProvider.cs new file mode 100644 index 0000000000..c73685b41d --- /dev/null +++ b/src/Umbraco.Web.Common/DependencyInjection/StaticServiceProvider.cs @@ -0,0 +1,25 @@ +using System; +using System.ComponentModel; + +namespace Umbraco.Cms.Web.Common.DependencyInjection +{ + /// + /// INTERNAL Service locator. Should only be used if no other ways exist. + /// + /// + /// It is created with only two goals in mind + /// 1) Continue to have the same extension methods on IPublishedContent and IPublishedElement as in V8. To make migration easier. + /// 2) To have a tool to avoid breaking changes in minor versions. All methods using this should in theory be obsolete. + /// + /// Keep in mind, every time this is used, the code becomes basically untestable. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + internal static class StaticServiceProvider + { + /// + /// The service locator. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + internal static IServiceProvider Instance { get; set; } + } +} diff --git a/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilderExtensions.cs b/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilderExtensions.cs index 0126d443c9..4b3715cd52 100644 --- a/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilderExtensions.cs +++ b/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilderExtensions.cs @@ -186,9 +186,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; @@ -275,6 +280,7 @@ namespace Umbraco.Extensions builder.Services.AddUnique(); builder.Services.AddUnique(); + builder.Services.AddUnique(); builder.Services.AddUnique(); builder.Services.AddUnique(); diff --git a/src/Umbraco.Web.Common/DependencyInjection/UmbracoStartupFilter.cs b/src/Umbraco.Web.Common/DependencyInjection/UmbracoStartupFilter.cs index 3c7e47350b..f8ca73e283 100644 --- a/src/Umbraco.Web.Common/DependencyInjection/UmbracoStartupFilter.cs +++ b/src/Umbraco.Web.Common/DependencyInjection/UmbracoStartupFilter.cs @@ -1,6 +1,7 @@ using System; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Options; using Umbraco.Extensions; namespace Umbraco.Cms.Web.Common.DependencyInjection @@ -11,10 +12,16 @@ namespace Umbraco.Cms.Web.Common.DependencyInjection /// public sealed class UmbracoStartupFilter : IStartupFilter { + private readonly IOptions _options; + public UmbracoStartupFilter(IOptions options) => _options = options; + /// public Action Configure(Action next) => app => { + StaticServiceProvider.Instance = app.ApplicationServices; + _options.Value.PreUmbracoPipeline(app); + app.UseUmbraco(); next(app); }; diff --git a/src/Umbraco.Web.Common/DependencyInjection/UmbracoStartupFilterOptions.cs b/src/Umbraco.Web.Common/DependencyInjection/UmbracoStartupFilterOptions.cs new file mode 100644 index 0000000000..46ed09006b --- /dev/null +++ b/src/Umbraco.Web.Common/DependencyInjection/UmbracoStartupFilterOptions.cs @@ -0,0 +1,23 @@ +using System; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; + +namespace Umbraco.Cms.Web.Common.DependencyInjection +{ + public class UmbracoStartupFilterOptions + { + /// + /// Represents the pipeline that is executed before umbraco. By default this pipeline only adds UseDeveloperExceptionPage when the environments is Development. + /// + public Action PreUmbracoPipeline { get; set; } = app => + { + IWebHostEnvironment env = app.ApplicationServices.GetRequiredService(); + if (env.IsDevelopment()) + { + app.UseDeveloperExceptionPage(); + } + }; + } +} diff --git a/src/Umbraco.Web.Common/Extensions/ApplicationBuilderExtensions.cs b/src/Umbraco.Web.Common/Extensions/ApplicationBuilderExtensions.cs index 42b9b64ff4..1036a1f630 100644 --- a/src/Umbraco.Web.Common/Extensions/ApplicationBuilderExtensions.cs +++ b/src/Umbraco.Web.Common/Extensions/ApplicationBuilderExtensions.cs @@ -121,10 +121,12 @@ namespace Umbraco.Extensions if (!app.UmbracoCanBoot()) { + app.UseStaticFiles(); // We need static files to show the nice error page. app.UseMiddleware(); } else { + app.UseMiddleware(); app.UseMiddleware(); app.UseMiddleware(); } 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/FriendlyPublishedContentExtensions.cs b/src/Umbraco.Web.Common/Extensions/FriendlyPublishedContentExtensions.cs new file mode 100644 index 0000000000..b9cdddfdeb --- /dev/null +++ b/src/Umbraco.Web.Common/Extensions/FriendlyPublishedContentExtensions.cs @@ -0,0 +1,537 @@ +using System; +using System.Collections.Generic; +using System.Data; +using Examine; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Configuration.Models; +using Umbraco.Cms.Core.Models.PublishedContent; +using Umbraco.Cms.Core.PublishedCache; +using Umbraco.Cms.Core.Routing; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Web; +using Umbraco.Cms.Web.Common.DependencyInjection; + +namespace Umbraco.Extensions +{ + public static class FriendlyPublishedContentExtensions + { + private static IVariationContextAccessor VariationContextAccessor { get; } = + StaticServiceProvider.Instance.GetRequiredService(); + + private static IPublishedModelFactory PublishedModelFactory { get; } = + StaticServiceProvider.Instance.GetRequiredService(); + + private static IPublishedUrlProvider PublishedUrlProvider { get; } = + StaticServiceProvider.Instance.GetRequiredService(); + + private static IUserService UserService { get; } = + StaticServiceProvider.Instance.GetRequiredService(); + + private static IUmbracoContextAccessor UmbracoContextAccessor { get; } = + StaticServiceProvider.Instance.GetRequiredService(); + + private static ISiteDomainHelper SiteDomainHelper { get; } = + StaticServiceProvider.Instance.GetRequiredService(); + + private static IExamineManager ExamineManager { get; } = + StaticServiceProvider.Instance.GetRequiredService(); + + private static IFileService FileService { get; } = + StaticServiceProvider.Instance.GetRequiredService(); + + private static IOptions WebRoutingSettings { get; } = + StaticServiceProvider.Instance.GetRequiredService>(); + + private static IContentTypeService ContentTypeService { get; } = + StaticServiceProvider.Instance.GetRequiredService(); + + private static IPublishedValueFallback PublishedValueFallback { get; } = + StaticServiceProvider.Instance.GetRequiredService(); + + private static IPublishedSnapshot PublishedSnapshot => UmbracoContextAccessor.UmbracoContext?.PublishedSnapshot; + + private static IMediaTypeService MediaTypeService { get; } = + StaticServiceProvider.Instance.GetRequiredService(); + + + private static IMemberTypeService MemberTypeService { get; } = + StaticServiceProvider.Instance.GetRequiredService(); + + + /// + /// Creates a strongly typed published content model for an internal published content. + /// + /// The internal published content. + /// The strongly typed published content model. + public static IPublishedContent CreateModel( + this IPublishedContent content) + => content.CreateModel(PublishedModelFactory); + + /// + /// Gets the name of the content item. + /// + /// The content item. + /// The specific culture to get the name for. If null is used the current culture is used (Default is null). + public static string Name( + this IPublishedContent content, + string culture = null) + => content.Name(VariationContextAccessor, culture); + + /// + /// Gets the URL segment of the content item. + /// + /// The content item. + /// The specific culture to get the URL segment for. If null is used the current culture is used (Default is null). + public static string UrlSegment( + this IPublishedContent content, + string culture = null) + => content.UrlSegment(VariationContextAccessor, culture); + + /// + /// Gets the culture date of the content item. + /// + /// The content item. + /// The specific culture to get the name for. If null is used the current culture is used (Default is null). + public static DateTime CultureDate( + this IPublishedContent content, + string culture = null) + => content.CultureDate(VariationContextAccessor, culture); + + /// + /// Returns the current template Alias + /// + /// Empty string if none is set. + public static string GetTemplateAlias(this IPublishedContent content) + => content.GetTemplateAlias(FileService); + + public static bool IsAllowedTemplate(this IPublishedContent content, int templateId) + => content.IsAllowedTemplate(ContentTypeService, WebRoutingSettings.Value, templateId); + + public static bool IsAllowedTemplate( + this IPublishedContent content, + bool disableAlternativeTemplates, + bool validateAlternativeTemplates, + int templateId) + => content.IsAllowedTemplate( + ContentTypeService, + disableAlternativeTemplates, + validateAlternativeTemplates, + templateId); + + public static bool IsAllowedTemplate( + this IPublishedContent content, + bool disableAlternativeTemplates, + bool validateAlternativeTemplates, + string templateAlias) + => content.IsAllowedTemplate( + FileService, + ContentTypeService, + disableAlternativeTemplates, + validateAlternativeTemplates, + templateAlias); + + + /// + /// Gets a value indicating whether the content has a value for a property identified by its alias. + /// + /// The content. + /// The property alias. + /// The variation language. + /// The variation segment. + /// Optional fallback strategy. + /// A value indicating whether the content has a value for the property identified by the alias. + /// Returns true if HasValue is true, or a fallback strategy can provide a value. + public static bool HasValue( + this IPublishedContent content, + string alias, + string culture = null, + string segment = null, + Fallback fallback = default) + => + content.HasValue(PublishedValueFallback, alias, culture, segment, fallback); + + /// + /// Gets the value of a content's property identified by its alias, if it exists, otherwise a default value. + /// + /// The content. + /// The property alias. + /// The variation language. + /// The variation segment. + /// Optional fallback strategy. + /// The default value. + /// The value of the content's property identified by the alias, if it exists, otherwise a default value. + public static object Value(this IPublishedContent content, string alias, string culture = null, string segment = null, Fallback fallback = default, object defaultValue = default) + => content.Value(PublishedValueFallback, alias, culture, segment, fallback, defaultValue); + + /// + /// Gets the value of a content's property identified by its alias, converted to a specified type. + /// + /// The target property type. + /// The content. + /// The property alias. + /// The variation language. + /// The variation segment. + /// Optional fallback strategy. + /// The default value. + /// The value of the content's property identified by the alias, converted to the specified type. + public static T Value(this IPublishedContent content, string alias, string culture = null, string segment = null, Fallback fallback = default, T defaultValue = default) + => content.Value(PublishedValueFallback, alias, culture, segment, fallback, defaultValue); + + /// + /// Returns all DescendantsOrSelf of all content referenced + /// + /// + /// + /// The specific culture to filter for. If null is used the current culture is used. (Default is null) + /// + /// + /// This can be useful in order to return all nodes in an entire site by a type when combined with TypedContentAtRoot + /// + public static IEnumerable DescendantsOrSelfOfType( + this IEnumerable parentNodes, string docTypeAlias, string culture = null) + => parentNodes.DescendantsOrSelfOfType(VariationContextAccessor, docTypeAlias, culture); + + /// + /// Returns all DescendantsOrSelf of all content referenced + /// + /// + /// Variation context accessor. + /// The specific culture to filter for. If null is used the current culture is used. (Default is null) + /// + /// + /// This can be useful in order to return all nodes in an entire site by a type when combined with TypedContentAtRoot + /// + public static IEnumerable DescendantsOrSelf( + this IEnumerable parentNodes, + string culture = null) + where T : class, IPublishedContent + => parentNodes.DescendantsOrSelf(VariationContextAccessor, culture); + + public static IEnumerable Descendants(this IPublishedContent content, string culture = null) + => content.Descendants(VariationContextAccessor, culture); + + public static IEnumerable Descendants(this IPublishedContent content, int level, string culture = null) + => content.Descendants(VariationContextAccessor, level, culture); + + public static IEnumerable DescendantsOfType(this IPublishedContent content, + string contentTypeAlias, string culture = null) + => content.DescendantsOfType(VariationContextAccessor, contentTypeAlias, culture); + + public static IEnumerable Descendants(this IPublishedContent content, string culture = null) + where T : class, IPublishedContent + => content.Descendants(VariationContextAccessor, culture); + + public static IEnumerable Descendants(this IPublishedContent content, int level, string culture = null) + where T : class, IPublishedContent + => content.Descendants(VariationContextAccessor, level, culture); + + public static IEnumerable DescendantsOrSelf(this IPublishedContent content, string culture = null) + => content.DescendantsOrSelf(VariationContextAccessor, culture); + + + public static IEnumerable DescendantsOrSelf(this IPublishedContent content, int level, string culture = null) + => content.DescendantsOrSelf(VariationContextAccessor, level, culture); + + public static IEnumerable DescendantsOrSelfOfType(this IPublishedContent content, string contentTypeAlias, string culture = null) + => content.DescendantsOrSelfOfType(VariationContextAccessor, contentTypeAlias, culture); + + public static IEnumerable DescendantsOrSelf(this IPublishedContent content, string culture = null) + where T : class, IPublishedContent + => content.DescendantsOrSelf(VariationContextAccessor, culture); + + public static IEnumerable DescendantsOrSelf(this IPublishedContent content, int level, string culture = null) + where T : class, IPublishedContent + => content.DescendantsOrSelf(VariationContextAccessor, level, culture); + + public static IPublishedContent Descendant(this IPublishedContent content, string culture = null) + => content.Descendant(VariationContextAccessor, culture); + + public static IPublishedContent Descendant(this IPublishedContent content, int level, string culture = null) + => content.Descendant(VariationContextAccessor, level, culture); + + + public static IPublishedContent DescendantOfType(this IPublishedContent content, string contentTypeAlias, string culture = null) + => content.DescendantOfType(VariationContextAccessor, contentTypeAlias, culture); + + public static T Descendant(this IPublishedContent content, string culture = null) + where T : class, IPublishedContent + => content.Descendant(VariationContextAccessor, culture); + + + public static T Descendant(this IPublishedContent content, int level, string culture = null) + where T : class, IPublishedContent + => content.Descendant(VariationContextAccessor, level, culture); + + public static IPublishedContent DescendantOrSelf(this IPublishedContent content, string culture = null) + => content.DescendantOrSelf(VariationContextAccessor, culture); + + public static IPublishedContent DescendantOrSelf(this IPublishedContent content, int level, string culture = null) + => content.DescendantOrSelf(VariationContextAccessor, level, culture); + + public static IPublishedContent DescendantOrSelfOfType(this IPublishedContent content, string contentTypeAlias, string culture = null) + => content.DescendantOrSelfOfType(VariationContextAccessor, contentTypeAlias, culture); + + public static T DescendantOrSelf(this IPublishedContent content, string culture = null) + where T : class, IPublishedContent + => content.DescendantOrSelf(VariationContextAccessor, culture); + + public static T DescendantOrSelf(this IPublishedContent content, int level, string culture = null) + where T : class, IPublishedContent + => content.DescendantOrSelf(VariationContextAccessor, level, culture); + + + /// + /// Gets the children of the content item. + /// + /// The content item. + /// + /// The specific culture to get the URL children for. Default is null which will use the current culture in + /// + /// + /// Gets children that are available for the specified culture. + /// Children are sorted by their sortOrder. + /// + /// For culture, + /// if null is used the current culture is used. + /// If an empty string is used only invariant children are returned. + /// If "*" is used all children are returned. + /// + /// + /// If a variant culture is specified or there is a current culture in the then the Children returned + /// will include both the variant children matching the culture AND the invariant children because the invariant children flow with the current culture. + /// However, if an empty string is specified only invariant children are returned. + /// + /// + public static IEnumerable Children(this IPublishedContent content, string culture = null) + => content.Children(VariationContextAccessor, culture); + + /// + /// Gets the children of the content, filtered by a predicate. + /// + /// The content. + /// The predicate. + /// The specific culture to filter for. If null is used the current culture is used. (Default is null) + /// The children of the content, filtered by the predicate. + /// + /// Children are sorted by their sortOrder. + /// + public static IEnumerable Children(this IPublishedContent content, Func predicate, string culture = null) + => content.Children(VariationContextAccessor, predicate, culture); + + /// + /// Gets the children of the content, of any of the specified types. + /// + /// The content. + /// The specific culture to filter for. If null is used the current culture is used. (Default is null) + /// The content type alias. + /// The children of the content, of any of the specified types. + public static IEnumerable ChildrenOfType(this IPublishedContent content, string contentTypeAlias, string culture = null) + => content.ChildrenOfType(VariationContextAccessor, contentTypeAlias, culture); + + /// + /// Gets the children of the content, of a given content type. + /// + /// The content type. + /// The content. + /// The specific culture to filter for. If null is used the current culture is used. (Default is null) + /// The children of content, of the given content type. + /// + /// Children are sorted by their sortOrder. + /// + public static IEnumerable Children(this IPublishedContent content, string culture = null) + where T : class, IPublishedContent + => content.Children(VariationContextAccessor, culture); + + public static IPublishedContent FirstChild(this IPublishedContent content, string culture = null) + => content.FirstChild(VariationContextAccessor, culture); + + /// + /// Gets the first child of the content, of a given content type. + /// + public static IPublishedContent FirstChildOfType(this IPublishedContent content, string contentTypeAlias, string culture = null) + => content.FirstChildOfType(VariationContextAccessor, contentTypeAlias, culture); + + public static IPublishedContent FirstChild(this IPublishedContent content, Func predicate, string culture = null) + => content.FirstChild(VariationContextAccessor, predicate, culture); + + public static IPublishedContent FirstChild(this IPublishedContent content, Guid uniqueId, string culture = null) + => content.FirstChild(VariationContextAccessor, uniqueId, culture); + + + public static T FirstChild(this IPublishedContent content, string culture = null) + where T : class, IPublishedContent + => content.FirstChild(VariationContextAccessor, culture); + + public static T FirstChild(this IPublishedContent content, Func predicate, string culture = null) + where T : class, IPublishedContent + => content.FirstChild(VariationContextAccessor, predicate, culture); + + /// + /// Gets the siblings of the content. + /// + /// The content. + /// The specific culture to filter for. If null is used the current culture is used. (Default is null) + /// The siblings of the content. + /// + /// Note that in V7 this method also return the content node self. + /// + public static IEnumerable Siblings(this IPublishedContent content, string culture = null) + => content.Siblings(PublishedSnapshot, VariationContextAccessor, culture); + + /// + /// Gets the siblings of the content, of a given content type. + /// + /// The content. + /// The content type alias. + /// The specific culture to filter for. If null is used the current culture is used. (Default is null) + /// The siblings of the content, of the given content type. + /// + /// Note that in V7 this method also return the content node self. + /// + public static IEnumerable SiblingsOfType(this IPublishedContent content, string contentTypeAlias, string culture = null) + => content.SiblingsOfType(PublishedSnapshot, VariationContextAccessor, contentTypeAlias, culture); + + /// + /// Gets the siblings of the content, of a given content type. + /// + /// The content type. + /// The content. + /// The specific culture to filter for. If null is used the current culture is used. (Default is null) + /// The siblings of the content, of the given content type. + /// + /// Note that in V7 this method also return the content node self. + /// + public static IEnumerable Siblings(this IPublishedContent content, string culture = null) + where T : class, IPublishedContent + => content.Siblings(PublishedSnapshot, VariationContextAccessor, culture); + + /// + /// Gets the siblings of the content including the node itself to indicate the position. + /// + /// The content. + /// The specific culture to filter for. If null is used the current culture is used. (Default is null) + /// The siblings of the content including the node itself. + public static IEnumerable SiblingsAndSelf(this IPublishedContent content, string culture = null) + => content.SiblingsAndSelf(PublishedSnapshot, VariationContextAccessor, culture); + + /// + /// Gets the siblings of the content including the node itself to indicate the position, of a given content type. + /// + /// The content. + /// The specific culture to filter for. If null is used the current culture is used. (Default is null) + /// The content type alias. + /// The siblings of the content including the node itself, of the given content type. + public static IEnumerable SiblingsAndSelfOfType(this IPublishedContent content, string contentTypeAlias, string culture = null) + => content.SiblingsAndSelfOfType(PublishedSnapshot, VariationContextAccessor, contentTypeAlias, culture); + + /// + /// Gets the siblings of the content including the node itself to indicate the position, of a given content type. + /// + /// The content type. + /// The content. + /// The specific culture to filter for. If null is used the current culture is used. (Default is null) + /// The siblings of the content including the node itself, of the given content type. + public static IEnumerable SiblingsAndSelf(this IPublishedContent content, string culture = null) + where T : class, IPublishedContent + => content.SiblingsAndSelf(PublishedSnapshot, VariationContextAccessor, culture); + + + /// + /// Gets the url of the content item. + /// + /// + /// If the content item is a document, then this method returns the url of the + /// document. If it is a media, then this methods return the media url for the + /// 'umbracoFile' property. Use the MediaUrl() method to get the media url for other + /// properties. + /// The value of this property is contextual. It depends on the 'current' request uri, + /// if any. In addition, when the content type is multi-lingual, this is the url for the + /// specified culture. Otherwise, it is the invariant url. + /// + public static string Url(this IPublishedContent content, string culture = null, UrlMode mode = UrlMode.Default) + => content.Url(PublishedUrlProvider, culture, mode); + + /// + /// Gets the children of the content in a DataTable. + /// + /// The content. + /// Variation context accessor. + /// The content type service. + /// The media type service. + /// The member type service. + /// The published url provider. + /// An optional content type alias. + /// The specific culture to filter for. If null is used the current culture is used. (Default is null) + /// The children of the content. + public static DataTable ChildrenAsTable(this IPublishedContent content, string contentTypeAliasFilter = "", string culture = null) + => content.ChildrenAsTable(VariationContextAccessor, ContentTypeService, MediaTypeService, MemberTypeService, PublishedUrlProvider, contentTypeAliasFilter, culture); + + /// + /// Gets the url for a media. + /// + /// The content item. + /// The culture (use current culture by default). + /// The url mode (use site configuration by default). + /// The alias of the property (use 'umbracoFile' by default). + /// The url for the media. + /// + /// The value of this property is contextual. It depends on the 'current' request uri, + /// if any. In addition, when the content type is multi-lingual, this is the url for the + /// specified culture. Otherwise, it is the invariant url. + /// + public static string MediaUrl( + this IPublishedContent content, + string culture = null, + UrlMode mode = UrlMode.Default, + string propertyAlias = Constants.Conventions.Media.File) + => content.MediaUrl(PublishedUrlProvider, culture, mode, propertyAlias); + + /// + /// Gets the name of the content item creator. + /// + /// The content item. + public static string CreatorName(this IPublishedContent content) => + content.CreatorName(UserService); + + /// + /// Gets the name of the content item writer. + /// + /// The content item. + public static string WriterName(this IPublishedContent content) => + content.WriterName(UserService); + + /// + /// Gets the culture assigned to a document by domains, in the context of a current Uri. + /// + /// The document. + /// An optional current Uri. + /// The culture assigned to the document by domains. + /// + /// In 1:1 multilingual setup, a document contains several cultures (there is not + /// one document per culture), and domains, withing the context of a current Uri, assign + /// a culture to that document. + /// + public static string GetCultureFromDomains( + this IPublishedContent content, + Uri current = null) + => content.GetCultureFromDomains(UmbracoContextAccessor, SiteDomainHelper, current); + + + public static IEnumerable SearchDescendants( + this IPublishedContent content, + string term, + string indexName = null) + => content.SearchDescendants(ExamineManager, UmbracoContextAccessor, term, indexName); + + + public static IEnumerable SearchChildren( + this IPublishedContent content, + string term, + string indexName = null) + => content.SearchChildren(ExamineManager, UmbracoContextAccessor, term, indexName); + + + } +} diff --git a/src/Umbraco.Web.Common/Extensions/FriendlyPublishedElementExtensions.cs b/src/Umbraco.Web.Common/Extensions/FriendlyPublishedElementExtensions.cs new file mode 100644 index 0000000000..bbb896fe8b --- /dev/null +++ b/src/Umbraco.Web.Common/Extensions/FriendlyPublishedElementExtensions.cs @@ -0,0 +1,74 @@ +using Microsoft.Extensions.DependencyInjection; +using Umbraco.Cms.Core.Models.PublishedContent; +using Umbraco.Cms.Web.Common.DependencyInjection; + +namespace Umbraco.Extensions +{ + public static class FriendlyPublishedElementExtensions + { + private static IPublishedValueFallback PublishedValueFallback { get; } = + StaticServiceProvider.Instance.GetRequiredService(); + + /// + /// Gets the value of a content's property identified by its alias. + /// + /// The content. + /// The property alias. + /// The variation language. + /// The variation segment. + /// Optional fallback strategy. + /// The default value. + /// The value of the content's property identified by the alias, if it exists, otherwise a default value. + /// + /// The value comes from IPublishedProperty field Value ie it is suitable for use when rendering content. + /// If no property with the specified alias exists, or if the property has no value, returns . + /// If eg a numeric property wants to default to 0 when value source is empty, this has to be done in the converter. + /// The alias is case-insensitive. + /// + public static object Value( + this IPublishedElement content, + string alias, + string culture = null, + string segment = null, + Fallback fallback = default, + object defaultValue = default) + => content.Value(PublishedValueFallback, alias, culture, segment, fallback, defaultValue); + + /// + /// Gets the value of a content's property identified by its alias, converted to a specified type. + /// + /// The target property type. + /// The content. + /// The property alias. + /// The variation language. + /// The variation segment. + /// Optional fallback strategy. + /// The default value. + /// The value of the content's property identified by the alias, converted to the specified type. + /// + /// The value comes from IPublishedProperty field Value ie it is suitable for use when rendering content. + /// If no property with the specified alias exists, or if the property has no value, or if it could not be converted, returns default(T). + /// If eg a numeric property wants to default to 0 when value source is empty, this has to be done in the converter. + /// The alias is case-insensitive. + /// + public static T Value( + this IPublishedElement content, + string alias, + string culture = null, + string segment = null, + Fallback fallback = default, + T defaultValue = default) + => content.Value(PublishedValueFallback, alias, culture, segment, fallback, defaultValue); + + /// + /// Gets a value indicating whether the content is visible. + /// + /// The content. + /// A value indicating whether the content is visible. + /// A content is not visible if it has an umbracoNaviHide property with a value of "1". Otherwise, + /// the content is visible. + public static bool IsVisible(this IPublishedElement content) => content.IsVisible(PublishedValueFallback); + + + } +} 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.Website/Extensions/PublishedContentExtensions.cs b/src/Umbraco.Web.Common/Extensions/PublishedContentExtensions.cs similarity index 99% rename from src/Umbraco.Web.Website/Extensions/PublishedContentExtensions.cs rename to src/Umbraco.Web.Common/Extensions/PublishedContentExtensions.cs index c33d5e6ef3..c02e609372 100644 --- a/src/Umbraco.Web.Website/Extensions/PublishedContentExtensions.cs +++ b/src/Umbraco.Web.Common/Extensions/PublishedContentExtensions.cs @@ -3,12 +3,12 @@ using System.Collections.Generic; using System.Web; using Examine; using Microsoft.AspNetCore.Html; +using Umbraco.Cms.Core; using Umbraco.Cms.Core.Models.PublishedContent; using Umbraco.Cms.Core.Routing; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Web; using Umbraco.Cms.Infrastructure.Examine; -using Constants = Umbraco.Cms.Core.Constants; namespace Umbraco.Extensions { 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.Common/Middleware/BootFailedMiddleware.cs b/src/Umbraco.Web.Common/Middleware/BootFailedMiddleware.cs index 476bb272b2..4f2b35bc93 100644 --- a/src/Umbraco.Web.Common/Middleware/BootFailedMiddleware.cs +++ b/src/Umbraco.Web.Common/Middleware/BootFailedMiddleware.cs @@ -1,7 +1,10 @@ +using System.IO; +using System.Text; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Exceptions; +using Umbraco.Cms.Core.Hosting; using Umbraco.Cms.Core.Services; namespace Umbraco.Cms.Web.Common.Middleware @@ -12,10 +15,12 @@ namespace Umbraco.Cms.Web.Common.Middleware public class BootFailedMiddleware : IMiddleware { private readonly IRuntimeState _runtimeState; + private readonly IHostingEnvironment _hostingEnvironment; - public BootFailedMiddleware(IRuntimeState runtimeState) + public BootFailedMiddleware(IRuntimeState runtimeState, IHostingEnvironment hostingEnvironment) { _runtimeState = runtimeState; + _hostingEnvironment = hostingEnvironment; } public async Task InvokeAsync(HttpContext context, RequestDelegate next) @@ -23,12 +28,35 @@ namespace Umbraco.Cms.Web.Common.Middleware if (_runtimeState.Level == RuntimeLevel.BootFailed) { // short circuit - BootFailedException.Rethrow(_runtimeState.BootFailedException); + // + + if (_hostingEnvironment.IsDebugMode) + { + BootFailedException.Rethrow(_runtimeState.BootFailedException); + } + else // Print a nice error page + { + context.Response.Clear(); + context.Response.StatusCode = 500; + + var file = GetBootErrorFileName(); + + var viewContent = await File.ReadAllTextAsync(file); + await context.Response.WriteAsync(viewContent, Encoding.UTF8); + } } else { await next(context); } + + } + private string GetBootErrorFileName() + { + var fileName = _hostingEnvironment.MapPathWebRoot("~/config/errors/BootFailed.html"); + if (File.Exists(fileName)) return fileName; + + return _hostingEnvironment.MapPathWebRoot("~/umbraco/views/errors/BootFailed.html"); } } } diff --git a/src/Umbraco.Web.BackOffice/Middleware/PreviewAuthenticationMiddleware.cs b/src/Umbraco.Web.Common/Middleware/PreviewAuthenticationMiddleware.cs similarity index 53% rename from src/Umbraco.Web.BackOffice/Middleware/PreviewAuthenticationMiddleware.cs rename to src/Umbraco.Web.Common/Middleware/PreviewAuthenticationMiddleware.cs index b03769d28b..50338f70e3 100644 --- a/src/Umbraco.Web.BackOffice/Middleware/PreviewAuthenticationMiddleware.cs +++ b/src/Umbraco.Web.Common/Middleware/PreviewAuthenticationMiddleware.cs @@ -3,35 +3,47 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Umbraco.Extensions; -using Constants = Umbraco.Cms.Core.Constants; -namespace Umbraco.Cms.Web.BackOffice.Middleware +namespace Umbraco.Cms.Web.Common.Middleware { /// /// Ensures that preview pages (front-end routed) are authenticated with the back office identity appended to the principal alongside any default authentication that takes place /// public class PreviewAuthenticationMiddleware : IMiddleware { + private readonly ILogger _logger; + + public PreviewAuthenticationMiddleware(ILogger logger) => _logger = logger; + /// public async Task InvokeAsync(HttpContext context, RequestDelegate next) { var request = context.Request; - if (!request.IsClientSideRequest()) + + // do not process if client-side request + if (request.IsClientSideRequest()) + { + await next(context); + return; + } + + try { var isPreview = request.HasPreviewCookie() - && context.User != null - && !request.IsBackOfficeRequest(); + && context.User != null + && !request.IsBackOfficeRequest(); if (isPreview) { var cookieOptions = context.RequestServices.GetRequiredService>() - .Get(Constants.Security.BackOfficeAuthenticationType); + .Get(Core.Constants.Security.BackOfficeAuthenticationType); if (cookieOptions == null) { - throw new InvalidOperationException("No cookie options found with name " + Constants.Security.BackOfficeAuthenticationType); + throw new InvalidOperationException("No cookie options found with name " + Core.Constants.Security.BackOfficeAuthenticationType); } // If we've gotten this far it means a preview cookie has been set and a front-end umbraco document request is executing. @@ -40,23 +52,27 @@ namespace Umbraco.Cms.Web.BackOffice.Middleware if (request.Cookies.TryGetValue(cookieOptions.Cookie.Name, out var cookie)) { var unprotected = cookieOptions.TicketDataFormat.Unprotect(cookie); - if (unprotected != null) + var backOfficeIdentity = unprotected?.Principal.GetUmbracoIdentity(); + if (backOfficeIdentity != null) { - var backOfficeIdentity = unprotected.Principal.GetUmbracoIdentity(); - if (backOfficeIdentity != null) - { - // Ok, we've got a real ticket, now we can add this ticket's identity to the current - // Principal, this means we'll have 2 identities assigned to the principal which we can - // use to authorize the preview and allow for a back office User. - context.User.AddIdentity(backOfficeIdentity); - } + // Ok, we've got a real ticket, now we can add this ticket's identity to the current + // Principal, this means we'll have 2 identities assigned to the principal which we can + // use to authorize the preview and allow for a back office User. + context.User.AddIdentity(backOfficeIdentity); } } } } - - await next(context); + catch (Exception ex) + { + // log any errors and continue the request without preview + _logger.LogError($"Unable to perform preview authentication: {ex.Message}"); + } + finally + { + await next(context); + } } } } diff --git a/src/Umbraco.Web.Common/Umbraco.Web.Common.csproj b/src/Umbraco.Web.Common/Umbraco.Web.Common.csproj index 34a3155d21..0fe68d4cc9 100644 --- a/src/Umbraco.Web.Common/Umbraco.Web.Common.csproj +++ b/src/Umbraco.Web.Common/Umbraco.Web.Common.csproj @@ -4,6 +4,9 @@ net5.0 Library Umbraco.Cms.Web.Common + Umbraco.Cms.Web.Common + Umbraco CMS Web + Contains the Web assembly needed to run Umbraco Cms. This package only contains the assembly, and can be used for package development. Use the template in the Umbraco.Templates package to setup Umbraco diff --git a/src/Umbraco.Web.UI.Client/package-lock.json b/src/Umbraco.Web.UI.Client/package-lock.json index 452b5c2071..2542dbe03f 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", @@ -2724,7 +2734,7 @@ "cli-color": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/cli-color/-/cli-color-1.4.0.tgz", - "integrity": "sha1-fRBzj0hSaCT4/n2lGFfLD1cv4B8=", + "integrity": "sha512-xu6RvQqqrWEo6MPR1eixqGPywhYBHRs653F9jfXB2Hx4jdM/3WxiNE1vppRmxtMIfl16SFYTpYlrnqH/HsK/2w==", "dev": true, "requires": { "ansi-regex": "^2.1.1", @@ -2958,7 +2968,7 @@ "color": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/color/-/color-3.0.0.tgz", - "integrity": "sha1-2SC0Mo1TSjrIKV1o971LpsQnvpo=", + "integrity": "sha512-jCpd5+s0s0t7p3pHQKpnJ0TpQKKdleP71LWcA0aqiljpiuAkOSUFN/dyH8ZwF0hRmFlrIuRhufds1QyEP9EB+w==", "dev": true, "requires": { "color-convert": "^1.9.1", @@ -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 } } }, @@ -3832,7 +3859,7 @@ "diagnostics": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/diagnostics/-/diagnostics-1.1.1.tgz", - "integrity": "sha1-yrasM99wydmnJ0kK5DrJladpsio=", + "integrity": "sha512-8wn1PmdunLJ9Tqbx+Fx/ZEuHfJf4NKSN2ZBj7SJC/OWRWha843+WsTjqMe1B5E3p28jqBlp+mJ2fPVxPyNgYKQ==", "dev": true, "requires": { "colorspace": "1.1.x", @@ -3927,7 +3954,7 @@ "domhandler": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-2.4.2.tgz", - "integrity": "sha1-iAUJfpM9ZehVRvcm1g9euItE+AM=", + "integrity": "sha512-JiK04h0Ht5u/80fdLMCEmV4zkNh2BcoMFBmZ/91WtYZ8qVXSKjiw7fXMgFPnHcSZgOo3XdinHvmnDUeMf5R4wA==", "dev": true, "requires": { "domelementtype": "1" @@ -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", @@ -4200,7 +4229,7 @@ "debug": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", - "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "integrity": "sha1-W7WgZyYotkFJVmuhaBnmFRjGcmE=", "dev": true, "requires": { "ms": "2.0.0" @@ -4377,7 +4406,7 @@ "source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha1-dHIq8y6WFOnCh6jQu95IteLxomM=", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", "dev": true, "optional": true } @@ -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" @@ -8742,7 +8814,7 @@ "kuler": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/kuler/-/kuler-1.0.1.tgz", - "integrity": "sha1-73x4TzbJ+24W3TFQ0VJneysCKKY=", + "integrity": "sha512-J9nVUucG1p/skKul6DU3PUZrhs0LPulNaeUOox0IyXDi8S4CztTHs1gQphhuZmzXG7VOQSf6NJfKuzteQLv9gQ==", "dev": true, "requires": { "colornames": "^1.1.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", @@ -9312,7 +9386,7 @@ "memoizee": { "version": "0.4.14", "resolved": "https://registry.npmjs.org/memoizee/-/memoizee-0.4.14.tgz", - "integrity": "sha1-B6APIEaZ+alcLZ53IYJxx81hDVc=", + "integrity": "sha512-/SWFvWegAIYAO4NQMpcX+gcra0yEZu4OntmUdrBaWrJncxOqAziGFlHxc7yjKVK2uu3lpPW27P27wkR82wA8mg==", "dev": true, "requires": { "d": "1", @@ -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", @@ -9436,7 +9511,7 @@ "minimize": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/minimize/-/minimize-2.2.0.tgz", - "integrity": "sha1-ixZ28wBR2FmNdDZGvRJpCwdNpMM=", + "integrity": "sha512-IxR2XMbw9pXCxApkdD9BTcH2U4XlXhbeySUrv71rmMS9XDA8BVXEsIuFu24LtwCfBgfbL7Fuh8/ZzkO5DaTLlQ==", "dev": true, "requires": { "argh": "^0.1.4", @@ -9461,7 +9536,7 @@ "is-extendable": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", - "integrity": "sha1-p0cPnkJnM9gb2B4RVSZOOjUHyrQ=", + "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", "dev": true, "requires": { "is-plain-object": "^2.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" } @@ -14964,7 +15048,7 @@ "debug": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", - "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "integrity": "sha1-W7WgZyYotkFJVmuhaBnmFRjGcmE=", "dev": true, "requires": { "ms": "2.0.0" @@ -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" @@ -15551,7 +15646,7 @@ "text-hex": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz", - "integrity": "sha1-adycGxdEbueakr9biEu0uRJ1BvU=", + "integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==", "dev": true }, "text-table": { @@ -15637,12 +15732,13 @@ "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", "resolved": "https://registry.npmjs.org/timers-ext/-/timers-ext-0.1.7.tgz", - "integrity": "sha1-b1ethXjgej+5+R2Th9ZWR1VeJcY=", + "integrity": "sha512-b85NUNzTSdodShTIbky6ZF02e8STtVVfD+fu4aXXShEELpozH+bCpJLYMPZbsABN2wDH7fJpqIoXxJpzbf0NqQ==", "dev": true, "requires": { "es5-ext": "~0.10.46", @@ -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/components/upload/umbfileupload.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/upload/umbfileupload.directive.js index 3581aed9e0..60882a372f 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/upload/umbfileupload.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/upload/umbfileupload.directive.js @@ -20,11 +20,7 @@ function umbFileUpload() { el.val(''); }); - el.on('drag dragstart dragend dragover dragenter dragleave drop', function (e) { - e.preventDefault(); - e.stopPropagation(); - }) - .on('dragover dragenter', function () { + el.on('dragover dragenter', function () { scope.$emit("isDragover", { value: true }); }) .on('dragleave dragend drop', function () { diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/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/templatehelper.service.js b/src/Umbraco.Web.UI.Client/src/common/services/templatehelper.service.js index 2e71ef0bf5..1a2f0735ce 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/templatehelper.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/templatehelper.service.js @@ -25,7 +25,7 @@ "\t@foreach (var item in selection)\n" + "\t{\n" + "\t\t
  • \n" + - "\t\t\t@item.Name\n" + + "\t\t\t@item.Name()\n" + "\t\t
  • \n" + "\t}\n" + "\n\n"; @@ -43,11 +43,11 @@ function getAddSectionSnippet(sectionName) { return "@section " + sectionName + "\r\n{\r\n\r\n\t{0}\r\n\r\n}\r\n"; } - + function getGeneralShortcuts(){ var keys = [ - "shortcuts_generalHeader", - "buttons_undo", + "shortcuts_generalHeader", + "buttons_undo", "buttons_redo", "buttons_save" ]; @@ -61,7 +61,7 @@ labels.save = data[3]; return { - "name": labels.header, + "name": labels.header, "shortcuts": [ { "description": labels.undo, @@ -77,14 +77,14 @@ } ] }; - }); + }); } function getEditorShortcuts(){ var keys = [ - "shortcuts_editorHeader", - "shortcuts_commentLine", + "shortcuts_editorHeader", + "shortcuts_commentLine", "shortcuts_removeLine", "shortcuts_copyLineUp", "shortcuts_copyLineDown", @@ -126,7 +126,7 @@ "keys": { "win": [{ "key": "alt" }, { "key": "shift" }, { "key": "down" }], "mac": [{ "key": "cmd" }, { "key": "alt" }, { "key": "down" }] - } + } }, { "description": labels.movelineup, @@ -138,13 +138,13 @@ } ] }; - }); + }); } function getTemplateEditorShortcuts(){ var keys = [ - "template_insert", - "template_insertPageField", + "template_insert", + "template_insertPageField", "template_insertPartialView", "template_insertDictionaryItem", "template_insertMacro", @@ -198,13 +198,13 @@ } ] }; - }); + }); } function getPartialViewEditorShortcuts(){ var keys = [ - "template_insert", - "template_insertPageField", + "template_insert", + "template_insertPageField", "template_insertDictionaryItem", "template_insertMacro", "template_queryBuilder" @@ -242,7 +242,7 @@ }; }); - + } //////////// 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 @@ -
    public void Configure(IApplicationBuilder app) { - if (_env.IsDevelopment()) - { - app.UseDeveloperExceptionPage(); - } - app.UseUmbracoBackOffice(); app.UseUmbracoWebsite(); } diff --git a/src/Umbraco.Web.UI.NetCore/appsettings.Development.json b/src/Umbraco.Web.UI.NetCore/appsettings.Development.json index 983b157ef1..06afcf2a7a 100644 --- a/src/Umbraco.Web.UI.NetCore/appsettings.Development.json +++ b/src/Umbraco.Web.UI.NetCore/appsettings.Development.json @@ -18,16 +18,29 @@ }, "Umbraco": { "CMS": { - "Global": { - "Smtp": { -// "From": "your@email.here", -// "Host": "localhost", -// "Port": "25" - } - }, - "Hosting": { - "Debug": true - } + "Global": { + "Smtp": { + //"From": "your@email.here", + //"Host": "localhost", + // "Port": "25" + } + }, + "Hosting": { + "Debug": true + }, + "RichTextEditor": { + "Commands" : [ + { + "Alias": "fullscreen", + "Name": "Full Screen", + "Mode": "All" + } + ], + "Plugins": [ + "fullscreen" + ] + } } + } } diff --git a/src/Umbraco.Web.UI.NetCore/appsettings.json b/src/Umbraco.Web.UI.NetCore/appsettings.json index 382ee11590..a3e57978da 100644 --- a/src/Umbraco.Web.UI.NetCore/appsettings.json +++ b/src/Umbraco.Web.UI.NetCore/appsettings.json @@ -71,4 +71,4 @@ } } } -} \ No newline at end of file +} diff --git a/src/Umbraco.Web.UI.NetCore/config/lang/cs-CZ.user.xml b/src/Umbraco.Web.UI.NetCore/config/lang/cs-CZ.user.xml deleted file mode 100644 index d4902d563d..0000000000 --- a/src/Umbraco.Web.UI.NetCore/config/lang/cs-CZ.user.xml +++ /dev/null @@ -1,3 +0,0 @@ - - - \ No newline at end of file diff --git a/src/Umbraco.Web.UI.NetCore/config/lang/da-DK.user.xml b/src/Umbraco.Web.UI.NetCore/config/lang/da-DK.user.xml deleted file mode 100644 index 7a8ce2c28a..0000000000 --- a/src/Umbraco.Web.UI.NetCore/config/lang/da-DK.user.xml +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/src/Umbraco.Web.UI.NetCore/config/lang/de-DE.user.xml b/src/Umbraco.Web.UI.NetCore/config/lang/de-DE.user.xml deleted file mode 100644 index 7a8ce2c28a..0000000000 --- a/src/Umbraco.Web.UI.NetCore/config/lang/de-DE.user.xml +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/src/Umbraco.Web.UI.NetCore/config/lang/en-GB.user.xml b/src/Umbraco.Web.UI.NetCore/config/lang/en-GB.user.xml deleted file mode 100644 index 7a8ce2c28a..0000000000 --- a/src/Umbraco.Web.UI.NetCore/config/lang/en-GB.user.xml +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/src/Umbraco.Web.UI.NetCore/config/lang/en-US.user.xml b/src/Umbraco.Web.UI.NetCore/config/lang/en-US.user.xml deleted file mode 100644 index 7a8ce2c28a..0000000000 --- a/src/Umbraco.Web.UI.NetCore/config/lang/en-US.user.xml +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/src/Umbraco.Web.UI.NetCore/config/lang/es-ES.user.xml b/src/Umbraco.Web.UI.NetCore/config/lang/es-ES.user.xml deleted file mode 100644 index 7a8ce2c28a..0000000000 --- a/src/Umbraco.Web.UI.NetCore/config/lang/es-ES.user.xml +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/src/Umbraco.Web.UI.NetCore/config/lang/fr-FR.user.xml b/src/Umbraco.Web.UI.NetCore/config/lang/fr-FR.user.xml deleted file mode 100644 index 7a8ce2c28a..0000000000 --- a/src/Umbraco.Web.UI.NetCore/config/lang/fr-FR.user.xml +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/src/Umbraco.Web.UI.NetCore/config/lang/he-IL.user.xml b/src/Umbraco.Web.UI.NetCore/config/lang/he-IL.user.xml deleted file mode 100644 index 3a0ad355c3..0000000000 --- a/src/Umbraco.Web.UI.NetCore/config/lang/he-IL.user.xml +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/src/Umbraco.Web.UI.NetCore/config/lang/it-IT.user.xml b/src/Umbraco.Web.UI.NetCore/config/lang/it-IT.user.xml deleted file mode 100644 index 3a0ad355c3..0000000000 --- a/src/Umbraco.Web.UI.NetCore/config/lang/it-IT.user.xml +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/src/Umbraco.Web.UI.NetCore/config/lang/ja-JP.user.xml b/src/Umbraco.Web.UI.NetCore/config/lang/ja-JP.user.xml deleted file mode 100644 index 7a8ce2c28a..0000000000 --- a/src/Umbraco.Web.UI.NetCore/config/lang/ja-JP.user.xml +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/src/Umbraco.Web.UI.NetCore/config/lang/ko-KR.user.xml b/src/Umbraco.Web.UI.NetCore/config/lang/ko-KR.user.xml deleted file mode 100644 index 3a0ad355c3..0000000000 --- a/src/Umbraco.Web.UI.NetCore/config/lang/ko-KR.user.xml +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/src/Umbraco.Web.UI.NetCore/config/lang/nb-NO.user.xml b/src/Umbraco.Web.UI.NetCore/config/lang/nb-NO.user.xml deleted file mode 100644 index 3a0ad355c3..0000000000 --- a/src/Umbraco.Web.UI.NetCore/config/lang/nb-NO.user.xml +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/src/Umbraco.Web.UI.NetCore/config/lang/nl-NL.user.xml b/src/Umbraco.Web.UI.NetCore/config/lang/nl-NL.user.xml deleted file mode 100644 index 7a8ce2c28a..0000000000 --- a/src/Umbraco.Web.UI.NetCore/config/lang/nl-NL.user.xml +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/src/Umbraco.Web.UI.NetCore/config/lang/pl-PL.user.xml b/src/Umbraco.Web.UI.NetCore/config/lang/pl-PL.user.xml deleted file mode 100644 index 3a0ad355c3..0000000000 --- a/src/Umbraco.Web.UI.NetCore/config/lang/pl-PL.user.xml +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/src/Umbraco.Web.UI.NetCore/config/lang/pt-BR.user.xml b/src/Umbraco.Web.UI.NetCore/config/lang/pt-BR.user.xml deleted file mode 100644 index 3a0ad355c3..0000000000 --- a/src/Umbraco.Web.UI.NetCore/config/lang/pt-BR.user.xml +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/src/Umbraco.Web.UI.NetCore/config/lang/ru-RU.user.xml b/src/Umbraco.Web.UI.NetCore/config/lang/ru-RU.user.xml deleted file mode 100644 index 7a8ce2c28a..0000000000 --- a/src/Umbraco.Web.UI.NetCore/config/lang/ru-RU.user.xml +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/src/Umbraco.Web.UI.NetCore/config/lang/sv-SE.user.xml b/src/Umbraco.Web.UI.NetCore/config/lang/sv-SE.user.xml deleted file mode 100644 index 3a0ad355c3..0000000000 --- a/src/Umbraco.Web.UI.NetCore/config/lang/sv-SE.user.xml +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/src/Umbraco.Web.UI.NetCore/config/lang/zh-CN.user.xml b/src/Umbraco.Web.UI.NetCore/config/lang/zh-CN.user.xml deleted file mode 100644 index 8d2add98dd..0000000000 --- a/src/Umbraco.Web.UI.NetCore/config/lang/zh-CN.user.xml +++ /dev/null @@ -1,3 +0,0 @@ - - - \ No newline at end of file diff --git a/src/Umbraco.Web.UI.NetCore/config/logviewer.searches.config.js b/src/Umbraco.Web.UI.NetCore/config/logviewer.searches.config.js deleted file mode 100644 index 345fe23764..0000000000 --- a/src/Umbraco.Web.UI.NetCore/config/logviewer.searches.config.js +++ /dev/null @@ -1,42 +0,0 @@ -[ - { - "name": "Find all logs where the Level is NOT Verbose and NOT Debug", - "query": "Not(@Level='Verbose') and Not(@Level='Debug')" - }, - { - "name": "Find all logs that has an exception property (Warning, Error & Fatal with Exceptions)", - "query": "Has(@Exception)" - }, - { - "name": "Find all logs that have the property 'Duration'", - "query": "Has(Duration)" - }, - { - "name": "Find all logs that have the property 'Duration' and the duration is greater than 1000ms", - "query": "Has(Duration) and Duration > 1000" - }, - { - "name": "Find all logs that are from the namespace 'Umbraco.Core'", - "query": "StartsWith(SourceContext, 'Umbraco.Core')" - }, - { - "name": "Find all logs that use a specific log message template", - "query": "@MessageTemplate = '[Timing {TimingId}] {EndMessage} ({TimingDuration}ms)'" - }, - { - "name": "Find logs where one of the items in the SortedComponentTypes property array is equal to", - "query": "SortedComponentTypes[?] = 'Umbraco.Web.Search.ExamineComponent'" - }, - { - "name": "Find logs where one of the items in the SortedComponentTypes property array contains", - "query": "Contains(SortedComponentTypes[?], 'DatabaseServer')" - }, - { - "name": "Find all logs that the message has localhost in it with SQL like", - "query": "@Message like '%localhost%'" - }, - { - "name": "Find all logs that the message that starts with 'end' in it with SQL like", - "query": "@Message like 'end%'" - } -] diff --git a/src/Umbraco.Web.UI.NetCore/config/tinyMceConfig.Release.config b/src/Umbraco.Web.UI.NetCore/config/tinyMceConfig.Release.config deleted file mode 100644 index f6a26ee89a..0000000000 --- a/src/Umbraco.Web.UI.NetCore/config/tinyMceConfig.Release.config +++ /dev/null @@ -1,72 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - paste - anchor - charmap - table - lists - advlist - hr - autolink - directionality - tabfocus - searchreplace - - - - - font - - - - - raw - - diff --git a/src/Umbraco.Web.UI.NetCore/config/tinyMceConfig.config b/src/Umbraco.Web.UI.NetCore/config/tinyMceConfig.config deleted file mode 100644 index 7f7cb657e6..0000000000 --- a/src/Umbraco.Web.UI.NetCore/config/tinyMceConfig.config +++ /dev/null @@ -1,74 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - paste - anchor - charmap - table - lists - advlist - hr - autolink - directionality - tabfocus - searchreplace - fullscreen - - - - - font - - - - - raw - - diff --git a/src/Umbraco.Web.UI.NetCore/umbraco/config/lang/da.xml b/src/Umbraco.Web.UI.NetCore/umbraco/config/lang/da.xml index f0f292bae5..fd69fc7975 100644 --- a/src/Umbraco.Web.UI.NetCore/umbraco/config/lang/da.xml +++ b/src/Umbraco.Web.UI.NetCore/umbraco/config/lang/da.xml @@ -264,6 +264,8 @@ Titel (valgfri) Alternativ tekst (valgfri) Type + Hvilke varianter vil du udgive? + Vælg hvilke varianter, der skal gemmes. Afpublicér Afpubliceret Ikke oprettet @@ -326,7 +328,7 @@ Kopiering af mediet fejlede Oprettelse af mappen under parent med id %0% fejlede Omdøbning af mappen med id %0% fejlede - Træk dine filer ind i dropzonen for, at uploade dem til mediebiblioteketet. + Træk dine filer ind i dropzonen for, at uploade dem til mediebiblioteket. Opret et nyt medlem @@ -348,6 +350,16 @@ Det valgte medie i træet tillader ikke at medier oprettes under det. Rediger tilladelser for denne medietype. Dokumenttype uden skabelon + Dokumenttype med skabelon + Definerer en indholdsside, der kan oprettes af redaktørerne i indholdstræet, og som er kan tilgås direkte på en URL. + Dokumenttype + Definerer en indholdskomponent, der kan oprettes af redaktørerne i indholdstræet og benyttes i sammenhæng med andet indhold, men som ikke kan tilgås direkte på en URL. + Element-type + Definerer skabelonen for et sæt at egenskaber, der kan anvendes som skema i avancerede felter som f.eks. 'Block List' eller 'Nested Content'. + Komposition + Definerer et sæt genbrugbare egenskaber, der kan inkluderes i definitionen af andre dokumenttyper - f.eks. et sæt 'Almindelige side-data'. + Mappe + Benyttes til at organisere dokumenttyper, element-typer og kompositioner i dokumenttype-træet. Ny mappe Ny datatype Ny JavaScript-fil @@ -527,7 +539,7 @@ felter Indexet skal bygges igen, for at kunne læses Processen tager længere tid end forventet. Kontrollér Umbraco loggen for at se om der er sket fejl under operationen - Dette index kan ikke genbygess for det ikke har nogen + Dette index kan ikke genbygges for det ikke har nogen IIndexPopulator diff --git a/src/Umbraco.Web.UI.NetCore/umbraco/config/lang/en.xml b/src/Umbraco.Web.UI.NetCore/umbraco/config/lang/en.xml index 0792c1b70f..bd45345660 100644 --- a/src/Umbraco.Web.UI.NetCore/umbraco/config/lang/en.xml +++ b/src/Umbraco.Web.UI.NetCore/umbraco/config/lang/en.xml @@ -268,6 +268,7 @@ Statistics Title (optional) Alternative text (optional) + Caption (optional) Type Unpublish Unpublished @@ -370,6 +371,16 @@ The selected media in the tree doesn't allow for any other media to be created below it. Edit permissions for this media type Document Type without a template + Document Type with Template + The data definition for a content page that can be created by editors in the content tree and is directly accessible via a URL. + Document Type + The data definition for a content component that can be created by editors in the content tree and be picked on other pages but has no direct URL. + Element Type + Defines the schema for a repeating set of properties, for example, in a 'Block List' or 'Nested Content' property editor. + Composition + Defines a re-usable set of properties that can be included in the definition of multiple other Document Types. For example, a set of 'Common Page Settings'. + Folder + Used to organise the Document Types, Compositions and Element Types created in this Document Type tree. New folder New data type New JavaScript file @@ -798,6 +809,7 @@ Articles Videos Installing + Avatar for Blue diff --git a/src/Umbraco.Web.UI.NetCore/umbraco/config/lang/en_us.xml b/src/Umbraco.Web.UI.NetCore/umbraco/config/lang/en_us.xml index dc5664213d..7d59dbba63 100644 --- a/src/Umbraco.Web.UI.NetCore/umbraco/config/lang/en_us.xml +++ b/src/Umbraco.Web.UI.NetCore/umbraco/config/lang/en_us.xml @@ -272,6 +272,7 @@ Statistics Title (optional) Alternative text (optional) + Caption (optional) Type Unpublish Draft @@ -377,6 +378,16 @@ The selected media in the tree doesn't allow for any other media to be created below it. Edit permissions for this media type Document Type without a template + Document Type with Template + The data definition for a content page that can be created by editors in the content tree and is directly accessible via a URL. + Document Type + The data definition for a content component that can be created by editors in the content tree and be picked on other pages but has no direct URL. + Element Type + Defines the schema for a repeating set of properties, for example, in a 'Block List' or 'Nested Content' property editor. + Composition + Defines a re-usable set of properties that can be included in the definition of multiple other Document Types. For example, a set of 'Common Page Settings'. + Folder + Used to organise the Document Types, Compositions and Element Types created in this Document Type tree. New folder New data type New JavaScript file @@ -807,6 +818,7 @@ Articles Videos Installing + Avatar for Blue diff --git a/src/Umbraco.Web.UI.NetCore/umbraco/config/lang/fr.xml b/src/Umbraco.Web.UI.NetCore/umbraco/config/lang/fr.xml index 498245c441..65b3d69000 100644 --- a/src/Umbraco.Web.UI.NetCore/umbraco/config/lang/fr.xml +++ b/src/Umbraco.Web.UI.NetCore/umbraco/config/lang/fr.xml @@ -263,6 +263,7 @@ Statistiques Titre (optionnel) Texte alternatif (optionnel) + Légende (optionnel) Type Dépublier Dépublié @@ -762,6 +763,7 @@ actuel Intégrer sélectionné + Avatar de Bleu diff --git a/src/Umbraco.Web.UI.NetCore/umbraco/config/lang/nl.xml b/src/Umbraco.Web.UI.NetCore/umbraco/config/lang/nl.xml index 8bb21ee97f..aa6ef7dd36 100644 --- a/src/Umbraco.Web.UI.NetCore/umbraco/config/lang/nl.xml +++ b/src/Umbraco.Web.UI.NetCore/umbraco/config/lang/nl.xml @@ -806,6 +806,7 @@ Artikels Videos Installeren + Avatar van Blauw diff --git a/src/Umbraco.Web.UI.NetCore/umbraco/config/lang/sv.xml b/src/Umbraco.Web.UI.NetCore/umbraco/config/lang/sv.xml index f2cdc54657..511765e604 100644 --- a/src/Umbraco.Web.UI.NetCore/umbraco/config/lang/sv.xml +++ b/src/Umbraco.Web.UI.NetCore/umbraco/config/lang/sv.xml @@ -243,10 +243,10 @@ Installera Umbraco Forms - Stay - Discard changes - You have unsaved changes - Are you sure you want to navigate away from this page? - you have unsaved changes + Stanna + Ignorera ändringar + Du har ändringar som inte är sparade + Vill du verkligen lämna sidan? Du har ändringar som inte är sparade Klar @@ -741,8 +741,8 @@ Flikar - Sort order - Creation date + Sortera ordningen + Skapandedatum Sortering klar Välj i vilken ordning du vill ha sidorna genom att dra dem upp eller ner i listan. Du kan också klicka på kolumnrubrikerna för att sortera grupper av sidor @@ -815,12 +815,12 @@ Bild - Macro + Makro Lägg till - Choose layout + Välj utformning Lägg till rad - Add content - Drop content + Lägg till innehåll + Släpp innehåll Indholdet er ikke tilladt her Indholdet er tilladt her Klicka för att lägga in @@ -847,7 +847,7 @@ Alternativt fält Alternativ text - Casing + Hölje Välj fält Konvertera radbrytningar Byter radbrytningar mot html-taggen &lt;br&gt; @@ -963,7 +963,7 @@ Senast utlåst Senast inloggad Lösenordet ändrades - Login + Logga in Startnod i mediabiblioteket Begränsa media sectionen till en specifik startnod Media startnoder diff --git a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj index 02a514e1ea..003b4ead23 100644 --- a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj +++ b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj @@ -228,9 +228,9 @@ False True - 8110 + 8120 / - http://localhost:8110 + http://localhost:8120 False False diff --git a/src/Umbraco.Web.Website/Umbraco.Web.Website.csproj b/src/Umbraco.Web.Website/Umbraco.Web.Website.csproj index c38670f45d..a7c5e7a277 100644 --- a/src/Umbraco.Web.Website/Umbraco.Web.Website.csproj +++ b/src/Umbraco.Web.Website/Umbraco.Web.Website.csproj @@ -4,6 +4,9 @@ net5.0 Library Umbraco.Cms.Web.Website + Umbraco.Cms.Web.Website + Umbraco CMS Website + Contains the Website assembly needed to run Umbraco Cms. This package only contains the assembly, and can be used for package development. Use the template in the Umbraco.Templates package to setup Umbraco diff --git a/src/Umbraco.Web/AspNet/AspNetBackOfficeInfo.cs b/src/Umbraco.Web/AspNet/AspNetBackOfficeInfo.cs index b7ea9b0d6d..89222880ed 100644 --- a/src/Umbraco.Web/AspNet/AspNetBackOfficeInfo.cs +++ b/src/Umbraco.Web/AspNet/AspNetBackOfficeInfo.cs @@ -62,7 +62,7 @@ namespace Umbraco.Web var url = "http" + ssl + "://" + request.ServerVariables["SERVER_NAME"] + port + _ioHelper.ResolveUrl(_globalSettings.UmbracoPath); - return url.TrimEnd('/'); + return url.TrimEnd(Constants.CharArrays.ForwardSlash); } } } diff --git a/src/Umbraco.Web/AspNet/AspNetHostingEnvironment.cs b/src/Umbraco.Web/AspNet/AspNetHostingEnvironment.cs index d1ec9aa60d..745a3f1cca 100644 --- a/src/Umbraco.Web/AspNet/AspNetHostingEnvironment.cs +++ b/src/Umbraco.Web/AspNet/AspNetHostingEnvironment.cs @@ -57,7 +57,7 @@ namespace Umbraco.Web.Hosting // this will be the case in unit tests, we'll manually map the path var newPath = path.Replace('/', Path.DirectorySeparatorChar).Replace('\\', Path.DirectorySeparatorChar); - return newPath.StartsWith(ApplicationPhysicalPath) ? newPath : Path.Combine(ApplicationPhysicalPath, newPath.TrimStart('~', '/')); + return newPath.StartsWith(ApplicationPhysicalPath) ? newPath : Path.Combine(ApplicationPhysicalPath, newPath.TrimStart(Constants.CharArrays.TildeForwardSlash)); } public string MapPathContentRoot(string path) => MapPathWebRoot(path); diff --git a/src/Umbraco.Web/HttpContextExtensions.cs b/src/Umbraco.Web/HttpContextExtensions.cs index 1e29f76a1a..40718715c9 100644 --- a/src/Umbraco.Web/HttpContextExtensions.cs +++ b/src/Umbraco.Web/HttpContextExtensions.cs @@ -1,4 +1,5 @@ using System.Web; +using Umbraco.Cms.Core; namespace Umbraco.Web { @@ -36,7 +37,7 @@ namespace Umbraco.Web if (string.IsNullOrEmpty(ipAddress)) return request.UserHostAddress; - var addresses = ipAddress.Split(','); + var addresses = ipAddress.Split(Constants.CharArrays.Comma); if (addresses.Length != 0) return addresses[0]; diff --git a/src/Umbraco.Web/HttpCookieExtensions.cs b/src/Umbraco.Web/HttpCookieExtensions.cs index aecb124b48..7804c6b45e 100644 --- a/src/Umbraco.Web/HttpCookieExtensions.cs +++ b/src/Umbraco.Web/HttpCookieExtensions.cs @@ -37,10 +37,10 @@ namespace Umbraco.Web if (cookiesHeaderValue == null) return null; - var cookieCollection = cookiesHeaderValue.Split(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries); + var cookieCollection = cookiesHeaderValue.Split(Constants.CharArrays.Semicolon, StringSplitOptions.RemoveEmptyEntries); foreach (var cookieNameValue in cookieCollection) { - var parts = cookieNameValue.Split(new[] { '=' }, StringSplitOptions.RemoveEmptyEntries); + var parts = cookieNameValue.Split(Constants.CharArrays.EqualsChar, StringSplitOptions.RemoveEmptyEntries); if (parts.Length != 2) continue; if (parts[0].Trim().Equals(cookieName, StringComparison.InvariantCultureIgnoreCase)) return parts[1].Trim(); diff --git a/src/Umbraco.Web/ModelStateExtensions.cs b/src/Umbraco.Web/ModelStateExtensions.cs index 7a2023715e..d9bc40c12e 100644 --- a/src/Umbraco.Web/ModelStateExtensions.cs +++ b/src/Umbraco.Web/ModelStateExtensions.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Linq; using System.Web.Mvc; +using Umbraco.Cms.Core; using Umbraco.Extensions; namespace Umbraco.Web @@ -72,7 +73,7 @@ namespace Umbraco.Web //Add any variant specific errors here var variantErrors = modelState.Keys .Where(key => key.StartsWith("_Properties.")) //only choose _Properties errors - .Select(x => x.Split('.')) //split into parts + .Select(x => x.Split(Constants.CharArrays.Period)) //split into parts .Where(x => x.Length >= 4 && !x[2].IsNullOrWhiteSpace() && !x[3].IsNullOrWhiteSpace()) .Select(x => (culture: x[2], segment: x[3])) //if the culture is marked "invariant" than return the default language, this is because we can only edit invariant properties on the default language @@ -110,7 +111,7 @@ namespace Umbraco.Web .Select(x => { // Format "_" - var cs = x.Split(new[] { '_' }); + var cs = x.Split(Constants.CharArrays.Underscore); return (culture: cs[0], segment: cs[1]); }) .Where(x => !x.culture.IsNullOrWhiteSpace()) diff --git a/src/Umbraco.Web/Security/Providers/UmbracoMembershipProvider.cs b/src/Umbraco.Web/Security/Providers/UmbracoMembershipProvider.cs index 00bc7709f4..55bfe06a07 100644 --- a/src/Umbraco.Web/Security/Providers/UmbracoMembershipProvider.cs +++ b/src/Umbraco.Web/Security/Providers/UmbracoMembershipProvider.cs @@ -574,11 +574,20 @@ namespace Umbraco.Web.Security.Providers { // when upgrading from 7.2 to 7.3 trying to save will throw if (_umbracoVersion.Version >= new Version(7, 3, 0, 0)) - MemberService.Save(member, false); + { + // We need to raise event to ensure caches are updated. (e.g. the cache that uses username as key). + // Even that this is a heavy operation, because indexes are updates, we consider that okay, as it + // is still cheap to do a successful login. + MemberService.Save(member, true); + } } else { - // set the last login date without full save (fast, no locks) + // set the last login date without full save (fast, no locks). + // We do not update caches. This is to the best of our knowledge okay, as this info are only stored + // because it is required by the membership provider. + // If we one day have to revisit this, we will most likely need to spilt the events in membership info + // saved and umbraco info saved. We don't want to update indexes etc when it is just membership info that is saved MemberService.SetLastLogin(member.Username, member.LastLoginDate); } diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index 09a9ee4384..ec1db29f99 100644 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -65,6 +65,8 @@ 2.0.0-alpha.20200128.15 + + 5.0.376 @@ -96,13 +98,17 @@ - + 3.5.4 runtime; build; native; contentfiles; analyzers all + + 1.0.5 + all + @@ -224,4 +230,28 @@ - \ No newline at end of file + + + + + + + + + + + + + + + + + + diff --git a/src/Umbraco.Web/UmbracoDbProviderFactoryCreator.cs b/src/Umbraco.Web/UmbracoDbProviderFactoryCreator.cs index 04dba05a90..24847d3a81 100644 --- a/src/Umbraco.Web/UmbracoDbProviderFactoryCreator.cs +++ b/src/Umbraco.Web/UmbracoDbProviderFactoryCreator.cs @@ -1,6 +1,8 @@ using System; using System.Data.Common; using System.Data.SqlServerCe; +using Microsoft.Extensions.Options; +using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Infrastructure.Migrations.Install; using Umbraco.Cms.Infrastructure.Persistence; using Umbraco.Cms.Infrastructure.Persistence.SqlSyntax; @@ -29,9 +31,9 @@ namespace Umbraco.Web switch (providerName) { case Constants.DbProviderNames.SqlCe: - return new SqlCeSyntaxProvider(); + return new SqlCeSyntaxProvider(Options.Create(new GlobalSettings())); case Constants.DbProviderNames.SqlServer: - return new SqlServerSyntaxProvider(); + return new SqlServerSyntaxProvider(Options.Create(new GlobalSettings())); default: throw new InvalidOperationException($"Unknown provider name \"{providerName}\""); } diff --git a/src/umbraco.sln b/src/umbraco.sln index 840c7213e6..0041474014 100644 --- a/src/umbraco.sln +++ b/src/umbraco.sln @@ -37,10 +37,9 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{B5BD12C1 EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "NuSpecs", "NuSpecs", "{227C3B55-80E5-4E7E-A802-BE16C5128B9D}" ProjectSection(SolutionItems) = preProject - ..\build\NuSpecs\UmbracoCms.Core.nuspec = ..\build\NuSpecs\UmbracoCms.Core.nuspec ..\build\NuSpecs\UmbracoCms.nuspec = ..\build\NuSpecs\UmbracoCms.nuspec ..\build\NuSpecs\UmbracoCms.SqlCe.nuspec = ..\build\NuSpecs\UmbracoCms.SqlCe.nuspec - ..\build\NuSpecs\UmbracoCms.Web.nuspec = ..\build\NuSpecs\UmbracoCms.Web.nuspec + ..\build\NuSpecs\UmbracoCms.Examine.Lucene.nuspec = ..\build\NuSpecs\UmbracoCms.Examine.Lucene.nuspec EndProjectSection EndProject Project("{E24C65DC-7377-472B-9ABA-BC803B73C61A}") = "Umbraco.Web.UI.Client", "http://localhost:3961", "{3819A550-DCEC-4153-91B4-8BA9F7F0B9B4}"